Compare commits

..
Author SHA1 Message Date
Matthew Russell e1b09b406e feat: redo orderbook with no scroll 2023-05-24 08:00:03 -07:00
83 changed files with 1265 additions and 1384 deletions
+3 -2
View File
@@ -14,7 +14,8 @@ What we need to achieve and who for
## Tasks
- [ ]
- [ ]
- [ ] What do we need to do first
- [ ] and then what?
- [ ] Etc.
## Additional details / background info
+4 -7
View File
@@ -22,14 +22,11 @@ So that
## Tasks
- [ ] UX (if needed)
- [ ] Design (if needed)
- [ ] Explore and sketch
- [ ] Team and stakeholder review
- [ ] Specs reviewed and created or adjusted
- [ ] Implementation
- [ ] Testing (unit and/or e2e)
- [ ] Code review
- [ ] QA review
- [ ] Visual Design
- [ ] Team review
- [ ] Etc.
## Sketch
@@ -61,7 +61,7 @@ jobs:
CIDv0: ${{ env.IPFS_V0 }}
CIDv1: ${{ env.IPFS_V1 }}
You can always access the latest IPFS release by visiting [console.vega.xyz](https://console.vega.xyz).
You can always access the latest IPFS release by visiting [vega.trading](https://vega.trading).
You can also access Trading directly from an IPFS gateway.
BEWARE: The Trading interface uses [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported. You should always use an IPFS gateway that enforces [origin separation](https://ipfs.github.io/public-gateway-checker/).
+7 -26
View File
@@ -5,7 +5,6 @@ on:
branches:
- release/*
- develop
- main
tags:
- v*
pull_request:
@@ -171,7 +170,6 @@ jobs:
- publish-dist
- lint-test-build
if: ${{ github.event_name == 'pull_request' }}
timeout-minutes: 60
name: '(CD) comment preview links'
steps:
- name: Find Comment
@@ -181,29 +179,6 @@ jobs:
issue-number: ${{ github.event.pull_request.number }}
body-includes: Previews
- name: Wait for deployments
run: |
# https://stackoverflow.com/questions/3183444/check-for-valid-link-url
regex='(https?|ftp|file)://[-[:alnum:]\+&@#/%?=~_|!:,.;]*[-[:alnum:]\+&@#/%=~_|]'
if [[ "${{ needs.lint-test-build.outputs.preview_governance }}" =~ $regex ]]; then
until curl -L --fail "${{ needs.lint-test-build.outputs.preview_governance }}"; do
echo "waiting for governance preview"
sleep 5
done
fi
if [[ "${{ needs.lint-test-build.outputs.preview_explorer }}" =~ $regex ]]; then
until curl -L --fail "${{ needs.lint-test-build.outputs.preview_explorer }}"; do
echo "waiting for explorer preview"
sleep 5
done
fi
if [[ "${{ needs.lint-test-build.outputs.preview_trading }}" =~ $regex ]]; then
until curl -L --fail "${{ needs.lint-test-build.outputs.preview_trading }}"; do
echo "waiting for trading preview"
sleep 5
done
fi
- name: Create comment
uses: peter-evans/create-or-update-comment@v3
if: ${{ steps.fc.outputs.comment-id == 0 }}
@@ -215,9 +190,15 @@ jobs:
* explorer: ${{ needs.lint-test-build.outputs.preview_explorer }}
* trading: ${{ needs.lint-test-build.outputs.preview_trading }}
# Report single result at the end, to avoid mess with required checks in PR
cypress-check:
name: '(CI) cypress - check'
runs-on: ubuntu-latest
needs: cypress
steps:
- run: echo Done!
# Report single result at the end, to avoid mess with required checks in PR
cypress-result:
if: ${{ always() }}
needs: cypress
runs-on: ubuntu-22.04
+22 -59
View File
@@ -200,19 +200,8 @@ jobs:
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
- name: Check out ipfs-redirect
- name: Update vega.trading DNS to redirect to the new console
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
uses: actions/checkout@v3
with:
repository: 'vegaprotocol/ipfs-redirect'
path: 'ipfs-redirect'
fetch-depth: '0'
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
- name: Update console.vega.xyz DNS to redirect to the new console
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
env:
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
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
@@ -221,52 +210,26 @@ jobs:
new_hash=$(cat ${{ matrix.app }}-ipfs-hash)
new_cid=$(ipfs cid format -v 1 -b base32 $new_hash)
ls -al ipfs-redirect
# 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="https://${new_cid}.${new_console_url_type}.${new_console_url_domain}/"
echo "new_console_url=${new_console_url}"
echo $new_hash > ipfs-redirect/cidv0.txt
echo $new_cid > ipfs-redirect/cidv1.txt
# Update record in DNSimple
# docs: https://developer.dnsimple.com/v2/zones/records/#updateZoneRecord
dnsimple_account_id=84895
dnsimple_zone_name=vega.trading
dnsimple_record_id=44409591
# see: https://dnsimple.com/a/84895/domains/vega.trading/records/44409591/edit
(
cd ipfs-redirect
git status
cat .git/config
git config --global user.email "vega-ci-bot@vega.xyz"
git config --global user.name "vega-ci-bot"
branch_name="update-hash-${{ github.ref }}"
git checkout -b "$branch_name"
commit_msg="Automated hash update from ${{ github.ref }}"
git add cidv0.txt cidv1.txt
git commit -m "$commit_msg"
git push -u origin "$branch_name"
pr_url="$(gh pr create --title "${commit_msg}" --body 'automated pull request to update CIDs')"
echo $pr_url
# once auto merge get's enabled on documentation repo let's do follow up
sleep 5
gh pr merge "${pr_url}" --delete-branch --squash --admin
)
# # 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="https://${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=console.vega.xyz
# dnsimple_record_id=44409591
# # see: https://dnsimple.com/a/84895/domains/console.vega.xyz/records/44409591/edit
# curl -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}
curl -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}
+23 -30
View File
@@ -43,17 +43,7 @@ jobs:
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
- name: Check out ipfs-redirect
uses: actions/checkout@v3
with:
repository: 'vegaprotocol/ipfs-redirect'
path: 'ipfs-redirect'
fetch-depth: '0'
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
- name: Update console.vega.xyz DNS to redirect to the new console
env:
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
- name: Update vega.trading DNS to redirect to the new console
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
@@ -61,24 +51,27 @@ jobs:
which ipfs
new_hash=$(cat ipfs-hash)
new_cid=$(ipfs cid format -v 1 -b base32 $new_hash)
git config --global user.email "vega-ci-bot@vega.xyz"
git config --global user.name "vega-ci-bot"
echo $new_hash > ipfs-redirect/cidv0.txt
echo $new_cid > ipfs-redirect/cidv1.txt
(
cd ipfs-redirect
# 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="https://${new_cid}.${new_console_url_type}.${new_console_url_domain}/"
echo "new_console_url=${new_console_url}"
git status
branch_name="rollback-to-$new_hash"
git checkout -b "$branch_name"
commit_msg="hash rollback to $new_hash"
git add cidv0.txt cidv1.txt
git commit -m "$commit_msg"
git push -u origin "$branch_name" --force-with-lease
pr_url="$(gh pr create --title "${commit_msg}" --body 'automated pull request to update CIDs')"
echo $pr_url
# once auto merge get's enabled on documentation repo let's do follow up
sleep 5
gh pr merge "${pr_url}" --delete-branch --squash --admin
)
# Update record in DNSimple
# docs: https://developer.dnsimple.com/v2/zones/records/#updateZoneRecord
dnsimple_account_id=84895
dnsimple_zone_name=vega.trading
dnsimple_record_id=44409591
# see: https://dnsimple.com/a/84895/domains/vega.trading/records/44409591/edit
curl -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}
+1 -1
View File
@@ -10,4 +10,4 @@ NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_GOVERNANCE_URL=https://governance.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz/
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
NX_VEGA_CONSOLE_URL=https://vega.trading
@@ -33,9 +33,8 @@ const proposalDetailsTitle = '[data-testid="proposal-title"]';
const proposalDetailsDescription = '[data-testid="proposal-description"]';
const openProposals = '[data-testid="open-proposals"]';
const viewProposalButton = '[data-testid="view-proposal-btn"]';
const proposalDescriptionToggle = 'proposal-description-toggle';
const voteBreakdownToggle = 'vote-breakdown-toggle';
const proposalTermsToggle = 'proposal-json-toggle';
const proposalTermsToggle = 'proposal-terms-toggle';
describe(
'Governance flow for proposal details',
@@ -74,8 +73,6 @@ describe(
'contain.text',
rawProposal.rationale.title
);
cy.getByTestId(proposalDescriptionToggle).click();
cy.getByTestId('proposal-description-toggle');
cy.get(proposalDetailsDescription)
.find('p')
.should('have.text', proposalDescription);
@@ -85,7 +82,7 @@ describe(
cy.get('code.language-json')
.should('exist')
.within(() => {
cy.get('.hljs-attr').eq(0).should('have.text', '"id"');
cy.get('.hljs-string').eq(0).should('have.text', '"ProposalTerms"');
});
});
@@ -112,9 +109,16 @@ describe(
closingDate
);
});
getProposalInformationFromTable('Proposed on')
.invoke('text')
.should('not.be.empty');
cy.wrap(
formatDateWithLocalTimezone(
new Date(createTenDigitUnixTimeStampForSpecifiedDays(0) * 1000)
)
).then((proposalDate) => {
getProposalInformationFromTable('Proposed on').should(
'have.text',
proposalDate
);
});
});
it('Newly created proposal details - shows default status set to fail', function () {
@@ -7,6 +7,7 @@ import {
import {
clickOnValidatorFromList,
closeStakingDialog,
stakingPageAssociateTokens,
stakingValidatorPageAddStake,
waitForBeginningOfEpoch,
} from '../../support/staking.functions';
@@ -30,17 +31,19 @@ context('rewards - flow', { tags: '@slow' }, function () {
cy.visit('/');
waitForSpinner();
depositAsset(vegaAssetAddress, '1000', 18);
cy.validatorsSelfDelegate();
ethereumWalletConnect();
cy.connectVegaWallet();
vegaWalletTeardown();
cy.associateTokensToVegaWallet('6000');
cy.VegaWalletTopUpRewardsPool(30, 200);
navigateTo(navigation.validators);
vegaWalletTeardown();
stakingPageAssociateTokens('6000');
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
'contain',
'6,000.0',
txTimeout
);
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('3000');
closeStakingDialog();
@@ -24,7 +24,6 @@ const ethWalletContainer = '[data-testid="ethereum-wallet"]';
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
const vegaWalletUnstakedBalance =
'[data-testid="vega-wallet-balance-unstaked"]';
const currencyTitle = '[data-testid="currency-title"]:visible';
const txTimeout = Cypress.env('txTimeout');
const vegaWalletPublicKeyShort = Cypress.env('vegaWalletPublicKeyShort');
const ethWalletAssociateButton = '[data-testid="associate-btn"]:visible';
@@ -80,11 +79,14 @@ context(
//0005-ETXN-005
stakingPageAssociateTokens('2', { skipConfirmation: true });
cy.get(currencyTitle, txTimeout).should('have.length.above', 4);
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
6
);
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
cy.get(currencyTitle, txTimeout).should('have.length.at.least', 6);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
// 0005-ETXN-002
verifyEthWalletAssociatedBalance('2.0');
@@ -115,11 +117,14 @@ context(
verifyEthWalletTotalAssociatedBalance('6,002.00');
cy.get('button').contains('Select a validator to nominate').click();
stakingPageDisassociateTokens('2');
cy.get(currencyTitle, txTimeout).should('have.length.above', 4);
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
6
);
validateWalletCurrency('Associated', '2.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '0.00');
cy.get(currencyTitle, txTimeout).should('have.length.at.least', 6);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
cy.get(
'[data-testid="eth-wallet-associated-balances"]:visible',
txTimeout
@@ -220,11 +225,14 @@ context(
skipConfirmation: true,
});
cy.get(currencyTitle, txTimeout).should('have.length.above', 4);
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
6
);
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
cy.get(currencyTitle, txTimeout).should('have.length.at.least', 6);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
verifyEthWalletAssociatedBalance('2.0');
verifyEthWalletTotalAssociatedBalance('2.0');
cy.get(vegaWallet)
@@ -240,11 +248,14 @@ context(
type: 'contract',
skipConfirmation: true,
});
cy.get(currencyTitle, txTimeout).should('have.length.above', 4);
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
6
);
validateWalletCurrency('Associated', '2.00');
validateWalletCurrency('Pending association', '1.00');
validateWalletCurrency('Total associated after pending', '1.00');
cy.get(currencyTitle, txTimeout).should('have.length.at.least', 6);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
verifyEthWalletAssociatedBalance('1.0');
verifyEthWalletTotalAssociatedBalance('1.0');
});
@@ -328,11 +339,14 @@ context(
// 1004-ASSO-004
it('Pending association outside of app is shown', function () {
vegaWalletAssociate('2');
cy.get(currencyTitle, txTimeout).should('have.length.above', 4);
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
6
);
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
cy.get(currencyTitle, txTimeout).should('have.length.at.least', 6);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
validateWalletCurrency('Associated', '2.00');
});
@@ -341,11 +355,14 @@ context(
cy.wrap(validateWalletCurrency('Associated', '2.00')).then(() => {
vegaWalletDisassociate('2');
});
cy.get(currencyTitle, txTimeout).should('have.length.above', 4);
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
6
);
validateWalletCurrency('Associated', '2.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '0.00');
cy.get(currencyTitle, txTimeout).should('have.length.at.least', 6);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
validateWalletCurrency('Associated', '0.00');
});
@@ -15,7 +15,13 @@ const balanceAvailable = 'BALANCE_AVAILABLE_value';
const withdrawalThreshold = 'WITHDRAWAL_THRESHOLD_value';
const delayTime = 'DELAY_TIME_value';
const submitWithdrawalButton = 'submit-withdrawal';
const dialogTitle = 'dialog-title';
const dialogClose = 'dialog-close';
const txExplorerLink = 'tx-block-explorer';
const withdrawalAssetSymbol = 'withdrawal-asset-symbol';
const withdrawalAmount = 'withdrawal-amount';
const withdrawalRecipient = 'withdrawal-recipient';
const withdrawFundsButton = 'withdraw-funds';
const completeWithdrawalButton = 'complete-withdrawal';
const tableTxHash = '[col-id="txHash"]';
const tableAssetSymbol = '[col-id="asset.symbol"]';
@@ -24,16 +30,14 @@ const tableReceiverAddress = '[col-id="details.receiverAddress"]';
const tableWithdrawnTimeStamp = '[col-id="withdrawnTimestamp"]';
const tableWithdrawnStatus = '[col-id="status"]';
const tableCreatedTimeStamp = '[col-id="createdTimestamp"]';
const toast = 'toast';
const toastContent = 'toast-content';
const toastPanel = 'toast-panel';
const toastClose = 'toast-close';
const withdrawalDialogContent = 'dialog-content';
const toastCompleteWithdrawal = 'toast-complete-withdrawal';
const usdtName = 'USDC (local)';
const usdcEthAddress = '0x1b8a1B6CBE5c93609b46D1829Cc7f3Cb8eeE23a0';
const usdcSymbol = 'tUSDC';
const usdtSelectValue =
'993ed98f4f770d91a796faab1738551193ba45c62341d20597df70fea6704ede';
const truncatedWithdrawalEthAddress = '0xEe7D…22d94F';
const formValidationError = 'input-error-text';
const txTimeout = Cypress.env('txTimeout');
@@ -103,35 +107,29 @@ context(
cy.getByTestId(amountInput).click().type('120');
cy.getByTestId(submitWithdrawalButton).click();
});
cy.contains('Awaiting network confirmation').should('be.visible');
// 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();
});
cy.getByTestId(dialogTitle, txTimeout).should(
'have.text',
'Transaction complete'
);
cy.getByTestId(txExplorerLink)
.should('have.attr', 'href')
.and('contain', '/txs/');
cy.getByTestId(withdrawalAssetSymbol).should('have.text', usdcSymbol);
cy.getByTestId(withdrawalAmount).should('have.text', '120.00');
cy.getByTestId(withdrawalRecipient)
.should('have.text', truncatedWithdrawalEthAddress)
.and('have.attr', 'href')
.and('contain', `/address/${Cypress.env('ethWalletPublicKey')}`);
cy.getByTestId(withdrawFundsButton).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');
});
cy.getByTestId(dialogTitle, txTimeout).should(
'have.text',
'Withdraw asset complete'
);
cy.getByTestId(dialogClose).click();
// withdrawal history for complete withdrawal displayed
cy.get(tableWithdrawnStatus)
.eq(1, txTimeout)
@@ -170,17 +168,13 @@ context(
cy.getByTestId(submitWithdrawalButton).click();
});
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'Funds unlocked')
.within(() => {
cy.getByTestId('external-link').should('exist');
cy.getByTestId(toastPanel).should(
'contain.text',
'Withdraw 110.00 tUSDC'
);
cy.getByTestId(toastClose).click();
});
cy.contains('Awaiting network confirmation').should('be.visible');
// assert withdrawal request
cy.getByTestId(dialogTitle, txTimeout).should(
'have.text',
'Transaction complete'
);
cy.getByTestId(dialogClose).click();
cy.get(tableTxHash)
.eq(1)
.should('have.text', 'Complete withdrawal')
@@ -195,75 +189,28 @@ context(
cy.get(tableCreatedTimeStamp).should('not.be.empty');
});
ethereumWalletConnect();
cy.getByTestId(completeWithdrawalButton).first().click();
cy.getByTestId(toast)
.last(txTimeout)
cy.getByTestId(completeWithdrawalButton).click();
cy.getByTestId(toastContent)
.last()
.should('contain.text', 'Awaiting confirmation')
.within(() => {
cy.getByTestId('external-link').should('exist');
});
cy.getByTestId(toast)
.first(txTimeout)
cy.getByTestId(toastContent)
.first()
.should('contain.text', 'The withdrawal has been approved.')
.within(() => {
cy.getByTestId(toastPanel).should('contain.text', '110.00', 'tUSDC');
});
cy.getByTestId(toast)
.last(txTimeout)
cy.getByTestId(toastContent)
.last()
.should('contain.text', 'Transaction confirmed')
.within(() => {
cy.getByTestId('external-link').should('exist');
});
});
it('Should be able to see withdrawal details from toast', function () {
cy.getByTestId(withdraw).click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(withdrawalThreshold).should(
'have.text',
'100,000.00000T'
);
cy.getByTestId(amountInput).click().type('50');
cy.getByTestId(submitWithdrawalButton).click();
});
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'Funds unlocked')
.within(() => {
cy.getByTestId('external-link').should('exist');
cy.getByTestId(toastPanel).should(
'contain.text',
'Withdraw 50.00 tUSDC'
);
cy.contains('save your withdrawal details').click();
});
cy.getByTestId(withdrawalDialogContent)
.last()
.within(() => {
cy.getByTestId('assetSource_value').should(
'have.text',
'0x1b8a1B6CBE5c93609b46D1829Cc7f3Cb8eeE23a0'
);
cy.getByTestId('amount_value').should('have.text', '5000000');
cy.getByTestId('nonce_value').invoke('text').should('not.be.empty');
cy.getByTestId('signatures_value')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('targetAddress_value').should(
'have.text',
Cypress.env('ethWalletPublicKey')
);
cy.getByTestId('creation_value')
.invoke('text')
.should('not.be.empty');
});
cy.getByTestId(dialogClose).click();
});
// Skipping test due to bug #3882
it.skip('Unable to withdraw asset on pub key view', function () {
it('Unable to withdraw asset on pub key view', function () {
const vegaWalletPubKey = Cypress.env('vegaWalletPublicKey');
const expectedErrorTxt = `You are connected in a view only state for public key: ${vegaWalletPubKey}. In order to send transactions you must connect to a real wallet.`;
@@ -277,11 +224,10 @@ context(
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(amountInput).click().type('100');
cy.pause();
cy.getByTestId(submitWithdrawalButton).click();
});
cy.getByTestId(withdrawalDialogContent)
cy.getByTestId('dialog-content')
.last()
.within(() => {
cy.get('h1').should('have.text', 'Transaction failed');
@@ -128,7 +128,6 @@ context(
.first()
.find('[data-testid="view-proposal-btn"]')
.click();
cy.url().should('contain', '/protocol-upgrades/v1');
cy.getByTestId('protocol-upgrade-proposal').within(() => {
cy.get('h1').should('have.text', 'Vega Release v1');
cy.getByTestId('protocol-upgrade-block-height').should(
@@ -221,7 +221,7 @@ export function ensureSpecifiedUnstakedTokensAreAssociated(
}
export function closeStakingDialog() {
cy.getByTestId('dialog-title', txTimeout).should(
cy.getByTestId('dialog-title').should(
'contain.text',
'At the beginning of the next epoch'
);
@@ -5,7 +5,7 @@ const capsuleWalletConnectButton = '[data-testid="web3-connector-Unknown"]';
export function ethereumWalletConnect() {
cy.highlight('Connecting Eth Wallet');
cy.get(connectToEthButton, { timeout: 60000 }).within(() => {
cy.get(connectToEthButton).within(() => {
cy.contains('Connect Ethereum wallet to associate $VEGA')
.should('be.visible')
.click();
@@ -8,7 +8,6 @@ const mockData = {
{
asset: 'tDAI',
totalAmount: '5',
decimals: 6,
rewardTypes: {
ACCOUNT_TYPE_GLOBAL_REWARD: {
amount: '0',
@@ -1,5 +1,6 @@
import { formatNumber, toBigNum } from '@vegaprotocol/utils';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import {
rowGridItemStyles,
RewardsTable,
@@ -12,8 +13,7 @@ interface EpochIndividualRewardsGridProps {
}
interface RewardItemProps {
amount: string;
decimals: number;
value: string;
percentageOfTotal?: string;
dataTestId: string;
last?: boolean;
@@ -21,14 +21,15 @@ interface RewardItemProps {
const DisplayReward = ({
reward,
decimals,
percentageOfTotal,
}: {
reward: string;
decimals: number;
percentageOfTotal?: string;
}) => {
const { t } = useTranslation();
const {
appState: { decimals },
} = useAppState();
if (Number(reward) === 0) {
return <span className="text-vega-dark-300">-</span>;
@@ -63,8 +64,7 @@ const DisplayReward = ({
};
const RewardItem = ({
amount,
decimals,
value,
percentageOfTotal,
dataTestId,
last,
@@ -72,11 +72,7 @@ const RewardItem = ({
<div data-testid={dataTestId} className={rowGridItemStyles(last)}>
<div className="h-full w-5 absolute right-0 top-0 bg-gradient-to-r from-transparent to-black pointer-events-none" />
<div className="overflow-auto p-5">
<DisplayReward
reward={amount}
decimals={decimals}
percentageOfTotal={percentageOfTotal}
/>
<DisplayReward reward={value} percentageOfTotal={percentageOfTotal} />
</div>
<div className="h-full w-5 absolute left-0 top-0 bg-gradient-to-l from-transparent to-black pointer-events-none" />
</div>
@@ -90,7 +86,7 @@ export const EpochIndividualRewardsTable = ({
dataTestId="epoch-individual-rewards-table"
epoch={Number(data.epoch)}
>
{data.rewards.map(({ asset, rewardTypes, totalAmount, decimals }, i) => (
{data.rewards.map(({ asset, rewardTypes, totalAmount }, i) => (
<div className="contents" key={i}>
<div
data-testid="individual-rewards-asset"
@@ -102,19 +98,13 @@ export const EpochIndividualRewardsTable = ({
([key, { amount, percentageOfTotal }]) => (
<RewardItem
key={key}
amount={amount}
decimals={decimals}
value={amount}
percentageOfTotal={percentageOfTotal}
dataTestId={key}
/>
)
)}
<RewardItem
dataTestId="total"
amount={totalAmount}
decimals={decimals}
last={true}
/>
<RewardItem dataTestId="total" value={totalAmount} last={true} />
</div>
))}
</RewardsTable>
@@ -8,7 +8,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '100',
percentageOfTotal: '0.1',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD', name: 'USD', decimals: 6 },
asset: { id: 'usd', symbol: 'USD', name: 'USD' },
party: { id: 'blah' },
epoch: { id: '1' },
};
@@ -18,7 +18,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '50',
percentageOfTotal: '0.05',
receivedAt: new Date(),
asset: { id: 'eur', symbol: 'EUR', name: 'EUR', decimals: 5 },
asset: { id: 'eur', symbol: 'EUR', name: 'EUR' },
party: { id: 'blah' },
epoch: { id: '2' },
};
@@ -28,7 +28,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '200',
percentageOfTotal: '0.2',
receivedAt: new Date(),
asset: { id: 'gbp', symbol: 'GBP', name: 'GBP', decimals: 7 },
asset: { id: 'gbp', symbol: 'GBP', name: 'GBP' },
party: { id: 'blah' },
epoch: { id: '2' },
};
@@ -38,7 +38,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '100',
percentageOfTotal: '0.1',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD', name: 'USD', decimals: 6 },
asset: { id: 'usd', symbol: 'USD', name: 'USD' },
party: { id: 'blah' },
epoch: { id: '1' },
};
@@ -48,7 +48,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '150',
percentageOfTotal: '0.15',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD', name: 'USD', decimals: 6 },
asset: { id: 'usd', symbol: 'USD', name: 'USD' },
party: { id: 'blah' },
epoch: { id: '3' },
};
@@ -58,7 +58,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '50',
percentageOfTotal: '0.05',
receivedAt: new Date(),
asset: { id: 'eur', symbol: 'EUR', name: 'EUR', decimals: 5 },
asset: { id: 'eur', symbol: 'EUR', name: 'EUR' },
party: { id: 'blah' },
epoch: { id: '2' },
};
@@ -99,7 +99,6 @@ describe('generateEpochIndividualRewardsList', () => {
rewards: [
{
asset: 'USD',
decimals: 6,
totalAmount: '100',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
@@ -168,7 +167,6 @@ describe('generateEpochIndividualRewardsList', () => {
{
asset: 'GBP',
totalAmount: '200',
decimals: 7,
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
@@ -199,7 +197,6 @@ describe('generateEpochIndividualRewardsList', () => {
{
asset: 'EUR',
totalAmount: '50',
decimals: 5,
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
@@ -235,7 +232,6 @@ describe('generateEpochIndividualRewardsList', () => {
{
asset: 'USD',
totalAmount: '200',
decimals: 6,
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
@@ -283,7 +279,6 @@ describe('generateEpochIndividualRewardsList', () => {
rewards: [
{
asset: 'USD',
decimals: 6,
totalAmount: '150',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
@@ -320,7 +315,6 @@ describe('generateEpochIndividualRewardsList', () => {
{
asset: 'GBP',
totalAmount: '200',
decimals: 7,
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
@@ -351,7 +345,6 @@ describe('generateEpochIndividualRewardsList', () => {
{
asset: 'EUR',
totalAmount: '50',
decimals: 5,
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
@@ -397,7 +390,6 @@ describe('generateEpochIndividualRewardsList', () => {
{
asset: 'USD',
totalAmount: '200',
decimals: 6,
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
@@ -9,7 +9,6 @@ export interface EpochIndividualReward {
rewards: {
asset: string;
totalAmount: string;
decimals: number;
rewardTypes: {
[key in AccountType]?: {
amount: string;
@@ -54,7 +53,6 @@ export const generateEpochIndividualRewardsList = ({
const epochIndividualRewards = rewards.reduce((acc, reward) => {
const epochId = reward.epoch.id;
const assetName = reward.asset.name;
const assetDecimals = reward.asset.decimals;
const rewardType = reward.rewardType;
const amount = reward.amount;
const percentageOfTotal = reward.percentageOfTotal;
@@ -75,7 +73,6 @@ export const generateEpochIndividualRewardsList = ({
if (!asset) {
asset = {
asset: assetName,
decimals: assetDecimals,
totalAmount: '0',
rewardTypes: Object.fromEntries(emptyRowAccountTypes),
};
@@ -52,7 +52,6 @@ const assetRewards: Map<
assetRewards.set(assetId, {
assetId,
name: 'tDAI TEST',
decimals: 6,
rewards,
totalAmount: '295',
});
@@ -1,5 +1,6 @@
import { formatNumber, toBigNum } from '@vegaprotocol/utils';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import {
rowGridItemStyles,
RewardsTable,
@@ -11,19 +12,16 @@ interface EpochTotalRewardsGridProps {
}
interface RewardItemProps {
amount: string;
decimals: number;
value: string;
dataTestId: string;
last?: boolean;
}
const DisplayReward = ({
reward,
decimals,
}: {
reward: string;
decimals: number;
}) => {
const DisplayReward = ({ reward }: { reward: string }) => {
const {
appState: { decimals },
} = useAppState();
if (Number(reward) === 0) {
return <span className="text-vega-dark-300">-</span>;
}
@@ -35,16 +33,11 @@ const DisplayReward = ({
);
};
const RewardItem = ({
amount,
decimals,
dataTestId,
last,
}: RewardItemProps) => (
const RewardItem = ({ value, dataTestId, last }: RewardItemProps) => (
<div data-testid={dataTestId} className={rowGridItemStyles(last)}>
<div className="h-full w-5 absolute right-0 top-0 bg-gradient-to-r from-transparent to-black pointer-events-none" />
<div className="overflow-auto p-5">
<DisplayReward reward={amount} decimals={decimals} />
<DisplayReward reward={value} />
</div>
<div className="h-full w-5 absolute left-0 top-0 bg-gradient-to-l from-transparent to-black pointer-events-none" />
</div>
@@ -56,25 +49,15 @@ export const EpochTotalRewardsTable = ({
return (
<RewardsTable dataTestId="epoch-total-rewards-table" epoch={data.epoch}>
{Array.from(data.assetRewards.values()).map(
({ name, rewards, totalAmount, decimals }, i) => (
({ name, rewards, totalAmount }, i) => (
<div className="contents" key={i}>
<div data-testid="asset" className={`${rowGridItemStyles()} p-5`}>
{name}
</div>
{Array.from(rewards.values()).map(({ rewardType, amount }, i) => (
<RewardItem
key={i}
dataTestId={rewardType}
amount={amount}
decimals={decimals}
/>
<RewardItem key={i} dataTestId={rewardType} value={amount} />
))}
<RewardItem
dataTestId="total"
amount={totalAmount}
decimals={decimals}
last={true}
/>
<RewardItem dataTestId="total" value={totalAmount} last={true} />
</div>
)
)}
@@ -56,14 +56,12 @@ describe('generateEpochAssetRewardsList', () => {
node: {
id: '1',
name: 'Asset 1',
decimals: 18,
},
},
{
node: {
id: '2',
name: 'Asset 2',
decimals: 6,
},
},
],
@@ -103,7 +101,6 @@ describe('generateEpochAssetRewardsList', () => {
{
node: {
epoch: 1,
decimals: 18,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '123',
@@ -131,7 +128,6 @@ describe('generateEpochAssetRewardsList', () => {
'1',
{
assetId: '1',
decimals: 0,
name: '',
rewards: new Map([
[
@@ -200,14 +196,12 @@ describe('generateEpochAssetRewardsList', () => {
node: {
id: '1',
name: 'Asset 1',
decimals: 18,
},
},
{
node: {
id: '2',
name: 'Asset 2',
decimals: 6,
},
},
],
@@ -218,7 +212,6 @@ describe('generateEpochAssetRewardsList', () => {
node: {
epoch: 1,
assetId: '1',
decimals: 18,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
@@ -227,7 +220,6 @@ describe('generateEpochAssetRewardsList', () => {
node: {
epoch: 1,
assetId: '1',
decimals: 18,
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '100',
},
@@ -236,7 +228,6 @@ describe('generateEpochAssetRewardsList', () => {
node: {
epoch: 2,
assetId: '1',
decimals: 18,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '5',
},
@@ -263,7 +254,6 @@ describe('generateEpochAssetRewardsList', () => {
'1',
{
assetId: '1',
decimals: 18,
name: 'Asset 1',
rewards: new Map([
[
@@ -329,7 +319,6 @@ describe('generateEpochAssetRewardsList', () => {
'1',
{
assetId: '1',
decimals: 18,
name: 'Asset 1',
rewards: new Map([
[
@@ -398,14 +387,12 @@ describe('generateEpochAssetRewardsList', () => {
node: {
id: '1',
name: 'Asset 1',
decimals: 18,
},
},
{
node: {
id: '2',
name: 'Asset 2',
decimals: 6,
},
},
],
@@ -416,7 +403,6 @@ describe('generateEpochAssetRewardsList', () => {
node: {
epoch: 1,
assetId: '1',
decimals: 18,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
@@ -425,7 +411,6 @@ describe('generateEpochAssetRewardsList', () => {
node: {
epoch: 1,
assetId: '1',
decimals: 18,
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '100',
},
@@ -434,7 +419,6 @@ describe('generateEpochAssetRewardsList', () => {
node: {
epoch: 2,
assetId: '1',
decimals: 18,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '6',
},
@@ -443,7 +427,6 @@ describe('generateEpochAssetRewardsList', () => {
node: {
epoch: 2,
assetId: '1',
decimals: 18,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '27',
},
@@ -452,7 +435,6 @@ describe('generateEpochAssetRewardsList', () => {
node: {
epoch: 3,
assetId: '1',
decimals: 18,
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '15',
},
@@ -485,7 +467,6 @@ describe('generateEpochAssetRewardsList', () => {
{
assetId: '1',
name: 'Asset 1',
decimals: 18,
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
@@ -550,7 +531,6 @@ describe('generateEpochAssetRewardsList', () => {
'1',
{
assetId: '1',
decimals: 18,
name: 'Asset 1',
rewards: new Map([
[
@@ -628,7 +608,6 @@ describe('generateEpochAssetRewardsList', () => {
'1',
{
assetId: '1',
decimals: 18,
name: 'Asset 1',
rewards: new Map([
[
@@ -22,7 +22,6 @@ export type AggregatedEpochRewardSummary = {
name: EpochSummaryWithNamedReward['name'];
rewards: Map<RewardType, RewardItem>;
totalAmount: string;
decimals: number;
};
export type EpochTotalSummary = {
@@ -92,7 +91,6 @@ export const generateEpochTotalRewardsList = ({
assetId: reward.assetId,
name: matchingAsset?.name || '',
rewards: rewards || new Map(emptyRowAccountTypes),
decimals: matchingAsset?.decimals || 0,
totalAmount: (
Number(reward.amount) + Number(assetWithRewards?.totalAmount || 0)
).toString(),
@@ -4,7 +4,6 @@ fragment RewardFields on Reward {
id
symbol
name
decimals
}
party {
id
@@ -68,7 +67,6 @@ query EpochAssetsRewards(
node {
id
name
decimals
}
}
}
@@ -3,7 +3,7 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type RewardFieldsFragment = { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } };
export type RewardFieldsFragment = { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string, name: string }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } };
export type DelegationFieldsFragment = { __typename?: 'Delegation', amount: string, epoch: number };
@@ -16,7 +16,7 @@ export type RewardsQueryVariables = Types.Exact<{
}>;
export type RewardsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number } } | null> | null } | null } | null };
export type RewardsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string, name: string }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number } } | null> | null } | null } | null };
export type EpochRewardSummaryFieldsFragment = { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType };
@@ -26,7 +26,7 @@ export type EpochAssetsRewardsQueryVariables = Types.Exact<{
}>;
export type EpochAssetsRewardsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string, decimals: number } } | null> | null } | null, epochRewardSummaries?: { __typename?: 'EpochRewardSummaryConnection', edges?: Array<{ __typename?: 'EpochRewardSummaryEdge', node: { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType } } | null> | null } | null };
export type EpochAssetsRewardsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string } } | null> | null } | null, epochRewardSummaries?: { __typename?: 'EpochRewardSummaryConnection', edges?: Array<{ __typename?: 'EpochRewardSummaryEdge', node: { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType } } | null> | null } | null };
export type EpochFieldsFragment = { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } };
@@ -42,7 +42,6 @@ export const RewardFieldsFragmentDoc = gql`
id
symbol
name
decimals
}
party {
id
@@ -144,7 +143,6 @@ export const EpochAssetsRewardsDocument = gql`
node {
id
name
decimals
}
}
}
@@ -6,4 +6,4 @@ NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\"}
NX_ETHEREUM_PROVIDER_URL=https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
NX_VEGA_CONSOLE_URL=https://vega.trading
@@ -55,10 +55,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
validateMarketDataRow(3, 'Quote Unit', 'BTC');
});
// TODO: fix this test
// New volume check logic, added by https://github.com/vegaprotocol/frontend-monorepo/pull/3870 has caused the
// 24hr volume assertion to fail as it now reads 'Unknown'
it.skip('market volume displayed', () => {
it('market volume displayed', () => {
cy.getByTestId(marketTitle).contains('Market volume').click();
validateMarketDataRow(0, '24 Hour Volume', '1');
validateMarketDataRow(1, 'Open Interest', '-');
@@ -1,7 +1,6 @@
import { checkSorting } from '@vegaprotocol/cypress';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { marketsDataQuery } from '@vegaprotocol/mock';
import { positionsQuery } from '@vegaprotocol/mock';
beforeEach(() => {
cy.mockTradingPage();
@@ -17,27 +16,9 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
});
it('renders positions on portfolio page', () => {
cy.mockGQL((req) => {
const positions = positionsQuery();
if (positions.positions?.edges) {
positions.positions.edges.push(
...positions.positions.edges.map((edge) => ({
...edge,
node: {
...edge.node,
party: {
...edge.node.party,
id: 'vega-1',
},
},
}))
);
}
aliasGQLQuery(req, 'Positions', positions);
});
cy.visit('/#/portfolio');
cy.getByTestId('Positions').click();
validatePositionsDisplayed(true);
validatePositionsDisplayed();
});
describe('renders position among some graphql errors', () => {
it('rows should be displayed despite errors', () => {
@@ -75,9 +56,7 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
cy.getByTestId('tab-positions')
.first()
.within(() => {
cy.get(
'[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]'
)
cy.get('[row-id="market-2"]')
.eq(1)
.within(() => {
emptyCells.forEach((cell) => {
@@ -124,11 +103,9 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
const marketsSortedDefault = [
'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)',
'SUSPENDED MARKET',
];
const marketsSortedAsc = ['ACTIVE MARKET', 'Apple Monthly (30 Jun 2022)'];
const marketsSortedDesc = [
'SUSPENDED MARKET',
'Apple Monthly (30 Jun 2022)',
'ACTIVE MARKET',
];
@@ -142,13 +119,9 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
});
it('sorting by notional', () => {
cy.visit('/#/markets/market-0');
const marketsSortedDefault = [
'276,761.40348',
'46,126.90058',
'1,688.20',
];
const marketsSortedAsc = ['1,688.20', '46,126.90058', '276,761.40348'];
const marketsSortedDesc = ['276,761.40348', '46,126.90058', '1,688.20'];
const marketsSortedDefault = ['276,761.40348', '46,126.90058'];
const marketsSortedAsc = ['46,126.90058', '276,761.40348'];
const marketsSortedDesc = ['276,761.40348', '46,126.90058'];
cy.getByTestId('Positions').click();
checkSorting(
'notional',
@@ -159,9 +132,9 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
});
it('sorting by unrealisedPNL', () => {
cy.visit('/#/markets/market-0');
const marketsSortedDefault = ['8.95', '-0.22519', '8.95'];
const marketsSortedAsc = ['-0.22519', '8.95', '8.95'];
const marketsSortedDesc = ['8.95', '8.95', '-0.22519'];
const marketsSortedDefault = ['8.95', '-0.22519'];
const marketsSortedAsc = ['-0.22519', '8.95'];
const marketsSortedDesc = ['8.95', '-0.22519'];
cy.getByTestId('Positions').click();
checkSorting(
'unrealisedPNL',
@@ -172,7 +145,7 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
});
});
function validatePositionsDisplayed(multiKey = false) {
function validatePositionsDisplayed() {
cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions').within(() => {
cy.get('[col-id="marketName"]')
@@ -192,11 +165,10 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
cy.wrap($prices).invoke('text').should('not.be.empty');
});
if (!multiKey) {
cy.get('[col-id="currentLeverage"]').should('contain.text', '2.846.1');
cy.get('[col-id="marginAccountBalance"]') // margin allocated
.should('contain.text', '0.01');
}
cy.get('[col-id="currentLeverage"]').should('contain.text', '2.846.1');
cy.get('[col-id="marginAccountBalance"]') // margin allocated
.should('contain.text', '0.01');
cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => {
cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty');
@@ -212,6 +184,6 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
cy.get('.ag-popup').should('contain.text', 'Mark price x open volume');
});
cy.getByTestId('close-position').should('be.visible').and('have.length', 3);
cy.getByTestId('close-position').should('be.visible').and('have.length', 2);
}
});
+1 -1
View File
@@ -6,7 +6,7 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_NETWORKS={\"MAINNET\":\"https://vega.trading\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
+1 -1
View File
@@ -5,7 +5,7 @@ NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_CONFIG_URL=''
NX_VEGA_ENV=CUSTOM
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_NETWORKS={\"MAINNET\":\"https://vega.trading\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
NX_VEGA_URL=http://localhost:3008/graphql
NX_VEGA_WALLET_URL=http://localhost:1789
+1 -1
View File
@@ -6,7 +6,7 @@ NX_TRADING_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.se
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
NX_VEGA_ENV=DEVNET
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_NETWORKS={\"MAINNET\":\"https://vega.trading\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
+3 -3
View File
@@ -5,13 +5,13 @@ NX_TRADING_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.se
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
NX_VEGA_ENV=MAINNET
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_NETWORKS={\"MAINNET\":\"https://vega.trading\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.vega.xyz
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
NX_VEGA_CONSOLE_URL=https://vega.trading
# TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.20.14-core-0.71.4
NX_APP_VERSION=v0.20.12-core-0.71.4
+1 -1
View File
@@ -6,7 +6,7 @@ NX_TRADING_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.se
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_NETWORKS={\"MAINNET\":\"https://vega.trading\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.stagnet1.vega.rocks
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
+1 -1
View File
@@ -6,7 +6,7 @@ NX_TRADING_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.se
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
NX_VEGA_ENV=TESTNET
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_NETWORKS={\"MAINNET\":\"https://vega.trading\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
+1 -1
View File
@@ -6,7 +6,7 @@ NX_TRADING_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.se
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/testnet2/testnet2.toml
NX_VEGA_ENV=VALIDATOR_TESTNET
NX_VEGA_EXPLORER_URL=https://explorer.validators-testnet.vega.rocks
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\",\"TESTNET\":\"https://console.fairground.wtf\"}
NX_VEGA_NETWORKS={\"MAINNET\":\"https://vega.trading\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\",\"TESTNET\":\"https://console.fairground.wtf\"}
NX_VEGA_TOKEN_URL=https://governance.validators-testnet.vega.rocks
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
@@ -56,7 +56,13 @@ export const HeaderStats = ({ market }: HeaderStatsProps) => {
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat heading={t('Volume (24h)')} testId="market-volume">
<HeaderStat
heading={t('Volume (24h)')}
testId="market-volume"
description={t(
'The total number of contracts traded in the last 24 hours.'
)}
>
<Last24hVolume
marketId={market?.id}
positionDecimalPlaces={market?.positionDecimalPlaces}
@@ -175,10 +175,6 @@ describe('Closed', () => {
__typename: 'Market',
id: marketId,
},
party: {
__typename: 'Party',
id: pubKey,
},
};
};
const position = createPosition();
@@ -186,14 +182,18 @@ describe('Closed', () => {
request: {
query: PositionsDocument,
variables: {
partyIds: [pubKey],
partyId: pubKey,
},
},
result: {
data: {
positions: {
__typename: 'PositionConnection',
edges: [{ __typename: 'PositionEdge', node: position }],
party: {
__typename: 'Party',
id: pubKey,
positionsConnection: {
__typename: 'PositionConnection',
edges: [{ __typename: 'PositionEdge', node: position }],
},
},
},
},
+6 -4
View File
@@ -64,7 +64,7 @@ export const Closed = () => {
});
const { data: positionData } = usePositionsQuery({
variables: {
partyIds: pubKey ? [pubKey] : [],
partyId: pubKey || '',
},
skip: !pubKey,
});
@@ -72,9 +72,11 @@ export const Closed = () => {
// find a position for each market and add the realised pnl to
// a normalized object
const rowData = compact(marketData).map((market) => {
const position = positionData?.positions?.edges?.find((edge) => {
return edge.node.market.id === market.id;
});
const position = positionData?.party?.positionsConnection?.edges?.find(
(edge) => {
return edge.node.market.id === market.id;
}
);
const instrument = market.tradableInstrument.instrument;
@@ -54,7 +54,6 @@ export const Portfolio = () => {
onMarketClick={onMarketClick}
noBottomPlaceholder
storeKey="portfolioPositions"
allKeys
/>
</VegaWalletContainer>
</Tab>
@@ -89,9 +89,6 @@ const cacheConfig: InMemoryCacheConfig = {
Party: {
keyFields: false,
},
Position: {
keyFields: ['market', ['id'], 'party', ['id']],
},
Fees: {
keyFields: false,
},
@@ -1,5 +1,4 @@
import { useMemo, useState } from 'react';
import { gt, prerelease } from 'semver';
import {
ReleasesFeed,
useEnvironment,
@@ -16,10 +15,7 @@ import {
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
// v0.20.12-core-0.71.4 -> v0.20.12
// we need to strip the "core" suffix in order to determine whether a release
// is a pre-release (candidate); example: v.0.21.0-beta.1-core-0.71.4
const parseTagName = (tagName: string) => tagName.replace(/-core-[\d.]+$/i, '');
const CANONICAL_URL = 'https://vega.trading';
type UpgradeBannerProps = {
showVersionChange: boolean;
@@ -27,24 +23,34 @@ type UpgradeBannerProps = {
export const UpgradeBanner = ({ showVersionChange }: UpgradeBannerProps) => {
const [visible, setVisible] = useState(true);
const { data } = useReleases(ReleasesFeed.FrontEnd);
const { APP_VERSION, VEGA_ENV, VEGA_NETWORKS } = useEnvironment();
const CANONICAL_URL = VEGA_NETWORKS[VEGA_ENV] || 'https://console.vega.xyz';
const newest = useMemo(() => {
if (!APP_VERSION || !data) return undefined;
const newer = data.filter((r) => gt(r.tagName, APP_VERSION));
const { APP_VERSION, VEGA_ENV } = useEnvironment();
/**
* Filtering out the release candidates.
*/
const latest = useMemo(() => {
const valid =
// filter pre-releases on mainnet
VEGA_ENV === Networks.MAINNET
? newer?.filter((r) => !prerelease(parseTagName(r.tagName)))
: newer;
return valid.sort((a, b) => (gt(a.tagName, b.tagName) ? -1 : 1))[0];
}, [APP_VERSION, VEGA_ENV, data]);
? data?.filter((r) => !/-rc$/i.test(r.tagName))
: data;
if (!visible || !newest) {
return null;
}
return valid && valid.length > 0 ? valid[0] : undefined;
}, [VEGA_ENV, data]);
if (!APP_VERSION) return null;
if (!visible || !latest || latest.tagName === APP_VERSION) return null;
const versionChange = (
<span>
<span className="line-through text-vega-light-300 dark:text-vega-dark-300">
{APP_VERSION}
</span>{' '}
<VegaIcon size={14} name={VegaIconNames.ARROW_RIGHT} />{' '}
<span className="text-vega-orange-500 dark:text-vega-yellow-500">
<ExternalLink href={latest.htmlUrl}>{latest.tagName}</ExternalLink>
</span>
</span>
);
return (
<NotificationBanner
@@ -53,24 +59,13 @@ export const UpgradeBanner = ({ showVersionChange }: UpgradeBannerProps) => {
setVisible(false);
}}
>
<div className="uppercase mb-1">
<ExternalLink href={CANONICAL_URL}>
{t('Upgrade to the latest version of Console')}
</ExternalLink>
<div className="uppercase ">
{t('Upgrade to the latest version of Console')}{' '}
{showVersionChange && versionChange}
</div>
<div data-testid="bookmark-message">
<a
className="underline"
href={newest.htmlUrl}
rel="noreferrer nofollow noopener"
target="_blank"
>
{t("View what's changed")}
</a>{' '}
{t(' or bookmark')}{' '}
<a className="underline" href={CANONICAL_URL}>
{t('console.vega.xyz')}
</a>{' '}
{t('Bookmark')}{' '}
<ExternalLink href={CANONICAL_URL}>{t('vega.trading')}</ExternalLink>
<CopyWithTooltip text={CANONICAL_URL}>
<button title={t('Copy %s', CANONICAL_URL)}>
<span className="sr-only">{t('Copy %s', CANONICAL_URL)}</span>
@@ -36,12 +36,11 @@ describe('NodeHealth', () => {
describe('NodeUrl', () => {
it('renders correct part of node url', () => {
const node = 'https://api.n99.somenetwork.vega.xyz';
const expectedText = node.split('.').slice(1).join('.');
render(<NodeUrl url={node} />);
expect(
screen.getByText('api.n99.somenetwork.vega.xyz')
).toBeInTheDocument();
expect(screen.getByText(expectedText)).toBeInTheDocument();
});
});
+2 -1
View File
@@ -65,8 +65,9 @@ interface NodeUrlProps {
}
export const NodeUrl = ({ url }: NodeUrlProps) => {
// get base url from api url, api sub domain
const urlObj = new URL(url);
const nodeUrl = urlObj.hostname;
const nodeUrl = urlObj.origin.replace(/^[^.]+\./g, '');
return <span title={t('Connected node')}>{nodeUrl}</span>;
};
@@ -26,7 +26,7 @@ export const MarketMarkPrice = ({
{
dataProvider: marketDataProvider,
variables: { marketId: marketId || '' },
skip: !inView || !marketId,
skip: !inView,
},
THROTTLE_UPDATE_TIME
);
@@ -53,6 +53,7 @@ const MobileWalletButton = ({
setDrawerOpen(!drawerOpen);
}
}, [drawerOpen, fetchPubKeys, isConnected, openVegaWalletDialog]);
console.log(drawerOpen, isYellow);
const iconClass = drawerOpen
? 'hidden'
+11 -2
View File
@@ -1,9 +1,10 @@
import { useRef, useMemo, memo } from 'react';
import { useRef, useMemo, memo, useCallback } from 'react';
import { t } from '@vegaprotocol/i18n';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import type { AccountFields } from './accounts-data-provider';
import { aggregatedAccountsDataProvider } from './accounts-data-provider';
import type { PinnedAsset } from './accounts-table';
import { AccountTable } from './accounts-table';
@@ -35,8 +36,16 @@ export const AccountManager = ({
dataProvider: aggregatedAccountsDataProvider,
variables,
});
const bottomPlaceholderProps = useBottomPlaceholder({
const setId = useCallback(
(data: AccountFields, id: string) => ({
...data,
asset: { ...data.asset, id },
}),
[]
);
const bottomPlaceholderProps = useBottomPlaceholder<AccountFields>({
gridRef,
setId,
disabled: noBottomPlaceholder,
});
+1 -5
View File
@@ -154,11 +154,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
{...props}
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No accounts')}
getRowId={({
data,
}: {
data: AccountFields & { isLastPlaceholder?: boolean; id?: string };
}) => (data.isLastPlaceholder && data.id ? data.id : data.asset.id)}
getRowId={({ data }: { data: AccountFields }) => data.asset.id}
ref={ref}
tooltipShowDelay={500}
rowData={props.rowData?.filter(
+24 -54
View File
@@ -164,9 +164,7 @@ interface DataProviderParams<
Data,
SubscriptionData,
Delta,
Variables extends OperationVariables | undefined = undefined,
SubscriptionVariables extends OperationVariables | undefined = Variables,
QueryVariables extends OperationVariables | undefined = Variables
Variables extends OperationVariables | undefined = undefined
> {
query: Query<QueryData>;
subscriptionQuery?: Query<SubscriptionData>;
@@ -183,10 +181,6 @@ interface DataProviderParams<
resetDelay?: number;
additionalContext?: Record<string, unknown>;
errorPolicyGuard?: (graphqlErrors: GraphQLErrors) => boolean;
getQueryVariables?: (variables: Variables) => QueryVariables;
getSubscriptionVariables?: (
variables: Variables
) => SubscriptionVariables | SubscriptionVariables[];
}
/**
@@ -205,9 +199,7 @@ function makeDataProviderInternal<
Data,
SubscriptionData,
Delta,
Variables extends OperationVariables | undefined = undefined,
QueryVariables extends OperationVariables | undefined = Variables,
SubscriptionVariables extends OperationVariables | undefined = Variables
Variables extends OperationVariables | undefined = undefined
>({
query,
subscriptionQuery,
@@ -219,16 +211,12 @@ function makeDataProviderInternal<
resetDelay,
additionalContext,
errorPolicyGuard,
getQueryVariables,
getSubscriptionVariables,
}: DataProviderParams<
QueryData,
Data,
SubscriptionData,
Delta,
Variables,
QueryVariables,
SubscriptionVariables
Variables
>): Subscribe<Data, Delta, Variables> {
// list of callbacks passed through subscribe call
const callbacks: UpdateCallback<Data, Delta>[] = [];
@@ -242,7 +230,7 @@ function makeDataProviderInternal<
let loading = true;
let loaded = false;
let client: ApolloClient<object>;
let subscription: Subscription[] | undefined;
let subscription: Subscription | undefined;
let pageInfo: PageInfo | null = null;
let totalCount: number | undefined;
@@ -275,7 +263,7 @@ function makeDataProviderInternal<
.query<QueryData>({
query,
variables: {
...(getQueryVariables ? getQueryVariables(variables) : variables),
...variables,
...(pagination && {
// let the variables pagination be prior to provider param
pagination: {
@@ -355,33 +343,6 @@ function makeDataProviderInternal<
}
};
const subscriptionSubscribe = () => {
if (!subscriptionQuery || !getDelta || !update) {
return;
}
const subscriptionVariables = getSubscriptionVariables
? getSubscriptionVariables(variables)
: variables;
subscription = ([] as (OperationVariables | undefined)[])
.concat(subscriptionVariables)
.map((variables) =>
client
.subscribe<SubscriptionData>({
query: subscriptionQuery,
variables,
fetchPolicy,
})
.subscribe(onNext, onError)
);
};
const subscriptionUnsubscribe = () => {
if (subscription) {
subscription.forEach((subscription) => subscription.unsubscribe());
}
subscription = undefined;
};
const initialFetch = async (isUpdate = false) => {
if (!client) {
return;
@@ -431,7 +392,10 @@ function makeDataProviderInternal<
}
// if error will occur data provider stops subscription
error = e as Error;
subscriptionUnsubscribe();
if (subscription) {
subscription.unsubscribe();
}
subscription = undefined;
} finally {
loading = false;
notifyAll({ isUpdate });
@@ -475,7 +439,10 @@ function makeDataProviderInternal<
const onError = (e: Error) => {
error = e;
subscriptionUnsubscribe();
if (subscription) {
subscription.unsubscribe();
subscription = undefined;
}
notifyAll();
};
@@ -493,7 +460,13 @@ function makeDataProviderInternal<
return;
}
if (subscriptionQuery && getDelta && update) {
subscriptionSubscribe();
subscription = client
.subscribe<SubscriptionData>({
query: subscriptionQuery,
variables,
fetchPolicy,
})
.subscribe(onNext, onError);
}
await initialFetch();
};
@@ -502,7 +475,8 @@ function makeDataProviderInternal<
if (!subscription) {
return;
}
subscriptionUnsubscribe();
subscription.unsubscribe();
subscription = undefined;
data = null;
error = undefined;
loading = false;
@@ -616,18 +590,14 @@ export function makeDataProvider<
Data,
SubscriptionData,
Delta,
Variables extends OperationVariables | undefined = undefined,
SubscriptionVariables extends OperationVariables | undefined = Variables,
QueryVariables extends OperationVariables | undefined = Variables
Variables extends OperationVariables | undefined = undefined
>(
params: DataProviderParams<
QueryData,
Data,
SubscriptionData,
Delta,
Variables,
SubscriptionVariables,
QueryVariables
Variables
>
): Subscribe<Data, Delta, Variables> {
const getInstance = memoize<Data, Delta, Variables>(() =>
@@ -25,7 +25,7 @@ export const PriceChangeCell = memo(
ref={ref}
className={`${signedNumberCssClass(
change
)} flex items-center gap-2 font-mono text-ui-small`}
)} flex items-center gap-2 justify-end font-mono text-ui-small`}
>
<Arrow value={change} />
<span data-testid="price-change-percentage">
@@ -4,34 +4,41 @@ import type { AgGridReact } from 'ag-grid-react';
import type { IsFullWidthRowParams, RowHeightParams } from 'ag-grid-community';
const NO_HOVER_CSS_RULE = { 'no-hover': 'data?.isLastPlaceholder' };
const ROW_ID = 'bottom-placeholder';
const ROW_ID = 'bottomPlaceholder';
const fullWidthCellRenderer = () => null;
const isFullWidthRow = (params: IsFullWidthRowParams) =>
params.rowNode.data?.isLastPlaceholder;
interface Props {
interface Props<T> {
gridRef: RefObject<AgGridReact>;
setId?: (data: T, id: string) => T;
disabled?: boolean;
}
// eslint-disable-next-line @typescript-eslint/ban-types
export const useBottomPlaceholder = ({ gridRef, disabled }: Props) => {
export const useBottomPlaceholder = <T extends {}>({
gridRef,
setId,
disabled,
}: Props<T>) => {
const onBodyScrollEnd = useCallback(() => {
const rowCont = gridRef.current?.api.getDisplayedRowCount() ?? 0;
if (rowCont) {
const lastRow = gridRef.current?.api.getDisplayedRowAtIndex(rowCont - 1);
if (lastRow && lastRow.data) {
const placeholderRow = {
...lastRow.data,
isLastPlaceholder: true,
id: ROW_ID,
};
const placeholderRow = setId
? setId({ ...lastRow.data, isLastPlaceholder: true }, ROW_ID)
: {
...lastRow.data,
isLastPlaceholder: true,
id: ROW_ID,
};
const transaction = gridRef.current?.api.getRowNode(ROW_ID)
? { update: [placeholderRow] }
: { add: [placeholderRow] };
gridRef.current?.api.applyTransaction(transaction);
}
}
}, [gridRef]);
}, [gridRef, setId]);
const onRowsChanged = useCallback(() => {
const placeholderNode = gridRef.current?.api.getRowNode(ROW_ID);
@@ -40231,7 +40231,7 @@ export const GITHUB_VEGA_FRONTEND_RELEASES_DATA = [
'https://api.github.com/repos/vegaprotocol/frontend-monorepo/tarball/v0.20.6-core-0.71.4-3',
zipball_url:
'https://api.github.com/repos/vegaprotocol/frontend-monorepo/zipball/v0.20.6-core-0.71.4-3',
body: 'Release to test process of deploying and rolling back on IPFS\r\nRelease to test process of deploying and rolling back on IPFS\r\n\r\n---\r\n\r\n# Deployments\r\n* https://explorer.vega.xyz\r\n* https://governance.vega.xyz\r\n\r\n# IPFS releases\r\nTye IPFS hash of this release of the Trading app is:\r\n\r\nCIDv0: QmWjf5upWhe7euiZXHYZjtS7tpLNbJMt1TDBLj7B1BSRW9\r\nCIDv1: bafybeid4yjdxgrvceib4xbvuglcspkpd4evhaggkx2na7mvvg5vnjbyqai\r\n\r\nYou can always access the latest IPFS release by visiting [console.vega.xyz](https://console.vega.xyz).\r\n\r\nYou can also access Trading directly from an IPFS gateway.\r\nBEWARE: The Trading interface uses [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported. You should always use an IPFS gateway that enforces [origin separation](https://ipfs.github.io/public-gateway-checker/).\r\n\r\nYour settings are not remembered across different URLs.\r\n\r\nIPFS gateways:\r\n\r\nhttps://bafybeid4yjdxgrvceib4xbvuglcspkpd4evhaggkx2na7mvvg5vnjbyqai.ipfs.dweb.link/\r\nhttps://bafybeid4yjdxgrvceib4xbvuglcspkpd4evhaggkx2na7mvvg5vnjbyqai.ipfs.cf-ipfs.com/\r\nipfs://QmWjf5upWhe7euiZXHYZjtS7tpLNbJMt1TDBLj7B1BSRW9/\r\n\r\n',
body: 'Release to test process of deploying and rolling back on IPFS\r\nRelease to test process of deploying and rolling back on IPFS\r\n\r\n---\r\n\r\n# Deployments\r\n* https://explorer.vega.xyz\r\n* https://governance.vega.xyz\r\n\r\n# IPFS releases\r\nTye IPFS hash of this release of the Trading app is:\r\n\r\nCIDv0: QmWjf5upWhe7euiZXHYZjtS7tpLNbJMt1TDBLj7B1BSRW9\r\nCIDv1: bafybeid4yjdxgrvceib4xbvuglcspkpd4evhaggkx2na7mvvg5vnjbyqai\r\n\r\nYou can always access the latest IPFS release by visiting [vega.trading](https://vega.trading).\r\n\r\nYou can also access Trading directly from an IPFS gateway.\r\nBEWARE: The Trading interface uses [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported. You should always use an IPFS gateway that enforces [origin separation](https://ipfs.github.io/public-gateway-checker/).\r\n\r\nYour settings are not remembered across different URLs.\r\n\r\nIPFS gateways:\r\n\r\nhttps://bafybeid4yjdxgrvceib4xbvuglcspkpd4evhaggkx2na7mvvg5vnjbyqai.ipfs.dweb.link/\r\nhttps://bafybeid4yjdxgrvceib4xbvuglcspkpd4evhaggkx2na7mvvg5vnjbyqai.ipfs.cf-ipfs.com/\r\nipfs://QmWjf5upWhe7euiZXHYZjtS7tpLNbJMt1TDBLj7B1BSRW9/\r\n\r\n',
},
{
url: 'https://api.github.com/repos/vegaprotocol/frontend-monorepo/releases/103336462',
+1 -1
View File
@@ -37,7 +37,7 @@ const ConsoleLinks = {
...EmptyLinks,
[Networks.STAGNET1]: 'https://trading.stagnet1.vega.rocks',
[Networks.TESTNET]: 'https://console.fairground.wtf',
[Networks.MAINNET]: 'https://console.vega.xyz',
[Networks.MAINNET]: 'https://vega.trading',
};
const TokenLinks = {
+1 -1
View File
@@ -27,7 +27,7 @@ const GithubReleaseSchema = z.object({
const GithubReleasesSchema = z.array(GithubReleaseSchema);
export type ReleaseInfo = {
type ReleaseInfo = {
id: number;
name: string;
tagName: string;
+2 -1
View File
@@ -5,6 +5,7 @@ import { t } from '@vegaprotocol/i18n';
import { FillsTable } from './fills-table';
import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
import { useFillsList } from './use-fills-list';
import type { Trade } from './fills-data-provider';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
interface FillsManagerProps {
@@ -64,7 +65,7 @@ export const FillsManager = ({
}, []);
const { isFullWidthRow, fullWidthCellRenderer, rowClassRules, getRowHeight } =
useBottomPlaceholder({
useBottomPlaceholder<Trade>({
gridRef,
});
+305 -1
View File
@@ -1,4 +1,7 @@
import throttle from 'lodash/throttle';
import groupBy from 'lodash/groupBy';
import orderBy from 'lodash/orderBy';
import uniqBy from 'lodash/uniqBy';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { Orderbook } from './orderbook';
import { useDataProvider } from '@vegaprotocol/data-provider';
@@ -6,12 +9,13 @@ import { marketDepthProvider } from './market-depth-provider';
import { marketDataProvider, marketProvider } from '@vegaprotocol/markets';
import type { MarketData } from '@vegaprotocol/markets';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useMarketDepthQuery } from './__generated__/MarketDepth';
import type {
PriceLevelFieldsFragment,
MarketDepthUpdateSubscription,
MarketDepthQuery,
MarketDepthQueryVariables,
} from './__generated__/MarketDepth';
import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth';
import {
compactRows,
updateCompactedRows,
@@ -20,12 +24,312 @@ import {
} from './orderbook-data';
import type { OrderbookData } from './orderbook-data';
import { useOrderStore } from '@vegaprotocol/orders';
import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer';
import { MarketDepthUpdateDocument } from './__generated__/MarketDepth';
import classNames from 'classnames';
interface OrderbookManagerProps {
marketId: string;
}
export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
const [resolution, setResolution] = useState(0);
const precision = [1, 2, 5, 10, 20, 50, 100, 1000, 2000][resolution];
const { data, loading, error } = useBook(marketId);
if (error) return <div>{error.message}</div>;
if (loading || !data?.market?.depth) return <div>Loading...</div>;
return (
<div className="h-full overflow-hidden grid grid-rows-[min-content_1fr_min-content_1fr] text-xs">
<div className="flex items-center gap-1">
<button
className="p-2 border"
onClick={() => setResolution((x) => x + 1)}
>
-
</button>
<div>{precision}</div>
<button
className="p-2 border"
onClick={() =>
setResolution((x) => {
if (x <= 0) return 0;
return x - 1;
})
}
>
+
</button>
</div>
<div>
<ReactVirtualizedAutoSizer>
{({ width, height }) => {
const sell = data?.market?.depth.sell || [];
const rowCount = Math.floor(height / 16);
// group all prices that round to the same aggregated price
//
// { '1200': { __typename: 'PriceLevel', price: '1230' }}
const lookup = groupBy(sell, (lvl) =>
roundToCeil(Number(lvl.price), precision)
);
const aggregatedRows = Object.entries(lookup).map(
([price, group]) => {
const volume = group.reduce(
(sum, lvl) => sum + Number(lvl.volume),
0
);
const numberOfOrders = group.reduce(
(sum, lvl) => sum + Number(lvl.numberOfOrders),
0
);
return {
price: String(price),
volume: String(volume),
numberOfOrders: String(numberOfOrders),
};
}
);
const rows = orderBy(
aggregatedRows,
(lvl) => Number(lvl.price),
'desc'
);
// calc and add cumulative volume
const rowsWithCumulativeVol: Array<{
price: string;
volume: string;
cumulativeVolume: number;
}> = [];
let cumulativeVolume = 0;
// sell orders are rendered desc, but cumulative volume
// needs to be calculated from lowest price up
for (let i = rows.length - 1; i >= 0; i--) {
const lvl = rows[i];
cumulativeVolume = cumulativeVolume + Number(lvl.price);
rowsWithCumulativeVol.unshift({
...lvl,
cumulativeVolume,
});
}
const rowsToFit = rowsWithCumulativeVol.slice(
rowsWithCumulativeVol.length - rowCount
);
return (
<div
style={{ width, height }}
className="flex flex-col justify-end"
>
{rowsToFit.map((s) => (
<Row
key={s.price}
{...s}
totalVolume={cumulativeVolume}
side="sell"
/>
))}
</div>
);
}}
</ReactVirtualizedAutoSizer>
</div>
<div className="text-center">Mid</div>
<div>
<ReactVirtualizedAutoSizer>
{({ width, height }) => {
const buy = data?.market?.depth.buy || [];
const rowCount = Math.floor(height / 16);
const lookup = groupBy(buy, (lvl) =>
roundToFloor(Number(lvl.price), precision)
);
const aggregatedRows = Object.entries(lookup).map(
([price, group]) => {
const volume = group.reduce(
(sum, lvl) => sum + Number(lvl.volume),
0
);
const numberOfOrders = group.reduce(
(sum, lvl) => sum + Number(lvl.numberOfOrders),
0
);
return {
price: String(price),
volume: String(volume),
numberOfOrders: String(numberOfOrders),
};
}
);
const rows = orderBy(
aggregatedRows,
(lvl) => Number(lvl.price),
'desc'
);
const rowsWithCumulativeVol: Array<{
price: string;
volume: string;
cumulativeVolume: number;
}> = [];
let cumulativeVolume = 0;
rows.forEach((r) => {
cumulativeVolume = cumulativeVolume + Number(r.volume);
rowsWithCumulativeVol.push({
...r,
cumulativeVolume,
});
});
const rowsToFit = rowsWithCumulativeVol.slice(0, rowCount);
return (
<div style={{ width, height }}>
{rowsToFit.map((b) => (
<Row
key={b.price}
{...b}
totalVolume={cumulativeVolume}
side="buy"
/>
))}
</div>
);
}}
</ReactVirtualizedAutoSizer>
</div>
</div>
);
};
const Row = ({
price,
volume,
cumulativeVolume,
totalVolume,
side,
}: {
price: string;
volume: string;
cumulativeVolume: number;
totalVolume: number;
side: 'buy' | 'sell';
}) => {
return (
<div className="relative text-right font-mono">
<Bar
cumulativeVolume={cumulativeVolume}
totalVolume={totalVolume}
side={side}
/>
<div className="relative grid grid-cols-3 z-10">
<div>{price}</div>
<div>{volume}</div>
<div>{cumulativeVolume}</div>
</div>
</div>
);
};
const Bar = ({
cumulativeVolume,
totalVolume,
side,
}: {
cumulativeVolume: number;
totalVolume: number;
side: 'buy' | 'sell';
}) => {
const classes = classNames('absolute right-0 h-full z-0', {
'bg-vega-green-300': side === 'buy',
'bg-vega-pink-300': side === 'sell',
});
const pct = (cumulativeVolume / totalVolume) * 100;
return (
<div style={{ width: pct + '%', minWidth: '2px' }} className={classes} />
);
};
const useBook = (marketId: string) => {
const { data, loading, error, subscribeToMore } = useMarketDepthQuery({
variables: {
marketId,
},
});
useEffect(() => {
if (!marketId) return;
const unsub = subscribeToMore({
document: MarketDepthUpdateDocument,
variables: { marketId },
updateQuery: (
prev: MarketDepthQuery,
{
subscriptionData,
}: { subscriptionData: { data: MarketDepthUpdateSubscription } }
) => {
if (!subscriptionData.data.marketsDepthUpdate) {
return prev;
}
const currBuy = prev.market?.depth.buy || [];
const currSell = prev.market?.depth.sell || [];
const update = subscriptionData.data.marketsDepthUpdate[0];
const newBuy = update.buy || [];
const newSell = update.sell || [];
const buy = orderBy(
uniqBy([...newBuy, ...currBuy], 'price'),
(lvl) => Number(lvl.price),
'desc'
).filter((lvl) => lvl.volume !== '0');
const sell = orderBy(
uniqBy([...newSell, ...currSell], 'price'),
(lvl) => Number(lvl.price),
'asc'
).filter((lvl) => lvl.volume !== '0');
return {
market: {
id: marketId,
__typename: 'Market',
...prev.market,
depth: {
__typename: 'MarketDepth',
sell,
buy,
sequenceNumber: update.sequenceNumber,
},
},
};
},
});
return () => {
unsub();
};
}, [marketId, subscribeToMore]);
return { data, loading, error };
};
const roundToCeil = (x: number, m: number) => {
return Math.ceil(x / m) * m;
};
const roundToFloor = (x: number, m: number) => {
return Math.ceil(x / m) * m;
};
export const OrderbookManager2 = ({ marketId }: OrderbookManagerProps) => {
const [resolution, setResolution] = useState(1);
const variables = { marketId };
const resolutionRef = useRef(resolution);
+2 -51
View File
@@ -75,56 +75,7 @@ export const OrderbookRow = React.memo(
);
}
);
OrderbookRow.displayName = 'OrderbookRow';
export const OrderbookContinuousRow = React.memo(
({
ask,
bid,
cumulativeAsk,
cumulativeBid,
cumulativeRelativeAsk,
cumulativeRelativeBid,
decimalPlaces,
positionDecimalPlaces,
indicativeVolume,
price,
relativeAsk,
relativeBid,
onClick,
}: OrderbookRowProps) => {
const type = bid ? 'bid' : 'ask';
const value = bid || ask;
const relativeValue = bid ? relativeBid : relativeAsk;
return (
<>
<VolCell
testId={`bid-ask-vol-${price}`}
value={value}
valueFormatted={addDecimalsFixedFormatNumber(
value,
positionDecimalPlaces
)}
relativeValue={relativeValue}
type={type}
/>
<PriceCell
testId={`price-${price}`}
value={BigInt(price)}
onClick={() => onClick && onClick(addDecimal(price, decimalPlaces))}
valueFormatted={addDecimalsFixedFormatNumber(price, decimalPlaces)}
/>
<CumulativeVol
testId={`cumulative-vol-${price}`}
positionDecimalPlaces={positionDecimalPlaces}
bid={cumulativeBid}
ask={cumulativeAsk}
relativeAsk={cumulativeRelativeAsk}
relativeBid={cumulativeRelativeBid}
indicativeVolume={indicativeVolume}
/>
</>
);
}
);
OrderbookContinuousRow.displayName = 'OrderbookContinuousRow';
export default OrderbookRow;
@@ -224,37 +224,4 @@ describe('Orderbook', () => {
expect(onResolutionChange.mock.calls[0][0]).toBe(10);
expect(onClickSpy).toBeCalledWith('122.99');
});
it('should have three or four columns', async () => {
window.innerHeight = 11 * rowHeight;
const { rerender } = render(
<Orderbook
decimalPlaces={decimalPlaces}
positionDecimalPlaces={0}
fillGaps
{...generateMockData({
...params,
overlap: 0,
})}
onResolutionChange={onResolutionChange}
/>
);
await waitFor(() => {
expect(screen.queryByText('Bid / Ask vol')).toBeInTheDocument();
});
rerender(
<Orderbook
decimalPlaces={decimalPlaces}
positionDecimalPlaces={0}
fillGaps
{...generateMockData(params)}
onResolutionChange={onResolutionChange}
/>
);
await waitFor(() => {
expect(screen.getByText('Bid vol')).toBeInTheDocument();
expect(screen.getByText('Ask vol')).toBeInTheDocument();
});
await expect(screen.queryByText('Bid / Ask vol')).not.toBeInTheDocument();
});
});
+44 -77
View File
@@ -1,12 +1,5 @@
import colors from 'tailwindcss/colors';
import {
useEffect,
useRef,
useState,
useCallback,
Fragment,
useMemo,
} from 'react';
import { useEffect, useRef, useState, useCallback, Fragment } from 'react';
import classNames from 'classnames';
import {
addDecimalsFixedFormatNumber,
@@ -18,7 +11,7 @@ import {
useThemeSwitcher,
} from '@vegaprotocol/react-helpers';
import * as Schema from '@vegaprotocol/types';
import { OrderbookRow, OrderbookContinuousRow } from './orderbook-row';
import { OrderbookRow } from './orderbook-row';
import { createRow } from './orderbook-data';
import { Checkbox, Icon, Splash, TinyScroll } from '@vegaprotocol/ui-toolkit';
import type { OrderbookData, OrderbookRowData } from './orderbook-data';
@@ -479,75 +472,39 @@ export const Orderbook = ({
);
useResizeObserver(gridElement.current, gridResizeHandler);
useResizeObserver(rootElement.current, rootElementResizeHandler);
const isContinuousMode =
marketTradingMode === Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS;
const tableHeader = useMemo(() => {
return (
<div
className={classNames(
'absolute top-0 grid auto-rows-[17px] gap-2 text-right border-b pt-2 bg-white dark:bg-black z-10 border-default w-full',
isContinuousMode ? 'grid-cols-3' : 'grid-cols-4'
)}
ref={headerElement}
>
{isContinuousMode ? (
<div>{t('Bid / Ask vol')}</div>
) : (
<>
<div>{t('Bid vol')}</div>
<div>{t('Ask vol')}</div>
</>
)}
<div>{t('Price')}</div>
<div className="pr-1 whitespace-nowrap overflow-hidden text-ellipsis">
{t('Cumulative vol')}
</div>
const tableBody =
data && data.length !== 0 ? (
<div className="grid grid-cols-4 gap-1 text-right auto-rows-[17px]">
{data.map((data, i) => (
<OrderbookRow
key={data.price}
price={(BigInt(data.price) / BigInt(resolution)).toString()}
onClick={onClick}
decimalPlaces={decimalPlaces - Math.log10(resolution)}
positionDecimalPlaces={positionDecimalPlaces}
bid={data.bid}
relativeBid={data.relativeBid}
cumulativeBid={data.cumulativeVol.bid}
cumulativeRelativeBid={data.cumulativeVol.relativeBid}
ask={data.ask}
relativeAsk={data.relativeAsk}
cumulativeAsk={data.cumulativeVol.ask}
cumulativeRelativeAsk={data.cumulativeVol.relativeAsk}
indicativeVolume={
marketTradingMode !==
Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS &&
indicativePrice === data.price
? indicativeVolume
: undefined
}
/>
))}
</div>
);
}, [isContinuousMode]);
const OrderBookRowComponent = isContinuousMode
? OrderbookContinuousRow
: OrderbookRow;
const tableBody = data?.length ? (
<div
className={classNames(
'grid grid-cols-4 gap-1 text-right auto-rows-[17px]',
isContinuousMode ? 'grid-cols-3' : 'grid-cols-4'
)}
>
{data.map((data, i) => (
<OrderBookRowComponent
key={data.price}
price={(BigInt(data.price) / BigInt(resolution)).toString()}
onClick={onClick}
decimalPlaces={decimalPlaces - Math.log10(resolution)}
positionDecimalPlaces={positionDecimalPlaces}
bid={data.bid}
relativeBid={data.relativeBid}
cumulativeBid={data.cumulativeVol.bid}
cumulativeRelativeBid={data.cumulativeVol.relativeBid}
ask={data.ask}
relativeAsk={data.relativeAsk}
cumulativeAsk={data.cumulativeVol.ask}
cumulativeRelativeAsk={data.cumulativeVol.relativeAsk}
indicativeVolume={
marketTradingMode !==
Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS &&
indicativePrice === data.price
? indicativeVolume
: undefined
}
/>
))}
</div>
) : null;
) : null;
const c = theme === 'dark' ? colors.neutral[600] : colors.neutral[300];
const gradientStyles = isContinuousMode
? `linear-gradient(${c},${c}) 33.4% 0/1px 100% no-repeat, linear-gradient(${c},${c}) 66.7% 0/1px 100% no-repeat`
: `linear-gradient(${c},${c}) 25% 0/1px 100% no-repeat, linear-gradient(${c},${c}) 50% 0/1px 100% no-repeat, linear-gradient(${c},${c}) 75% 0/1px 100% no-repeat`;
const gradientStyles = `linear-gradient(${c},${c}) 24.6% 0/1px 100% no-repeat, linear-gradient(${c},${c}) 50% 0/1px 100% no-repeat, linear-gradient(${c},${c}) 75.2% 0/1px 100% no-repeat`;
const resolutions = new Array(decimalPlaces + 1)
.fill(null)
@@ -574,11 +531,21 @@ export const Orderbook = ({
/* eslint-disable jsx-a11y/no-static-element-interactions */
return (
<div
className="h-full relative pl-1 text-xs"
className="h-full relative pl-2 text-xs"
ref={rootElement}
onDoubleClick={() => setDebug(!debug)}
>
{tableHeader}
<div
className="absolute top-0 grid grid-cols-4 auto-rows-[17px] gap-2 text-right border-b pt-2 bg-white dark:bg-black z-10 border-default w-full"
ref={headerElement}
>
<div>{t('Bid vol')}</div>
<div>{t('Ask vol')}</div>
<div>{t('Price')}</div>
<div className="pr-1 whitespace-nowrap overflow-hidden text-ellipsis">
{t('Cumulative vol')}
</div>
</div>
<TinyScroll
className="h-full overflow-auto relative"
onScroll={onScroll}
+1 -1
View File
@@ -167,4 +167,4 @@ export function useMarketDataLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
}
export type MarketDataQueryHookResult = ReturnType<typeof useMarketDataQuery>;
export type MarketDataLazyQueryHookResult = ReturnType<typeof useMarketDataLazyQuery>;
export type MarketDataQueryResult = Apollo.QueryResult<MarketDataQuery, MarketDataQueryVariables>;
export type MarketDataQueryResult = Apollo.QueryResult<MarketDataQuery, MarketDataQueryVariables>;
@@ -1,15 +1,12 @@
import type { RefObject } from 'react';
import { useInView } from 'react-intersection-observer';
import { isNumeric } from '@vegaprotocol/utils';
import { useFiveDaysAgo, useYesterday } from '@vegaprotocol/react-helpers';
import { useYesterday } from '@vegaprotocol/react-helpers';
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
import { PriceChangeCell } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types';
import type { CandleClose } from '@vegaprotocol/types';
import { marketCandlesProvider } from '../../market-candles-provider';
import type { MarketCandlesFieldsFragment } from '../../__generated__/market-candles';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
interface Props {
marketId?: string;
@@ -27,71 +24,30 @@ export const Last24hPriceChange = ({
inViewRoot,
}: Props) => {
const [ref, inView] = useInView({ root: inViewRoot?.current });
const fiveDaysAgo = useFiveDaysAgo();
const yesterday = useYesterday();
const { data, error } = useThrottledDataProvider({
dataProvider: marketCandlesProvider,
variables: {
marketId: marketId || '',
interval: Schema.Interval.INTERVAL_I1H,
since: new Date(fiveDaysAgo).toISOString(),
since: new Date(yesterday).toISOString(),
},
skip: !marketId || !inView,
});
const fiveDaysCandles = data?.filter((candle) => Boolean(candle));
const candles = fiveDaysCandles?.filter((candle) =>
isCandleLessThan24hOld(candle, yesterday)
);
const oneDayCandles =
candles
const candles =
data
?.map((candle) => candle?.close)
.filter((c): c is CandleClose => c !== null) || initialValue;
if (
fiveDaysCandles &&
fiveDaysCandles.length > 0 &&
(!oneDayCandles || oneDayCandles?.length === 0)
) {
return (
<Tooltip
description={
<span className="justify-start">
{t(
'24 hour change is unavailable at this time. The price change in the last 120 hours is:'
)}{' '}
<PriceChangeCell
candles={fiveDaysCandles.map((c) => c.close) || []}
decimalPlaces={decimalPlaces}
/>
</span>
}
>
<span ref={ref}>{t('Unknown')} </span>
</Tooltip>
);
}
if (error || !isNumeric(decimalPlaces)) {
return <span ref={ref}>-</span>;
}
return (
<PriceChangeCell
candles={oneDayCandles || []}
candles={candles || []}
decimalPlaces={decimalPlaces}
ref={ref}
/>
);
};
export const isCandleLessThan24hOld = (
candle: MarketCandlesFieldsFragment | undefined,
yesterday: number
) => {
if (!candle?.open) {
return false;
}
const candleDate = new Date(candle.close);
return candleDate > new Date(yesterday);
};
@@ -3,12 +3,9 @@ import { useInView } from 'react-intersection-observer';
import { marketCandlesProvider } from '../../market-candles-provider';
import { calcCandleVolume } from '../../market-utils';
import { addDecimalsFormatNumber, isNumeric } from '@vegaprotocol/utils';
import { useFiveDaysAgo, useYesterday } from '@vegaprotocol/react-helpers';
import { useYesterday } from '@vegaprotocol/react-helpers';
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
import * as Schema from '@vegaprotocol/types';
import { isCandleLessThan24hOld } from '../last-24h-price-change';
import { t } from '@vegaprotocol/i18n';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
interface Props {
marketId?: string;
@@ -26,7 +23,6 @@ export const Last24hVolume = ({
initialValue,
}: Props) => {
const yesterday = useYesterday();
const fiveDaysAgo = useFiveDaysAgo();
const [ref, inView] = useInView({ root: inViewRoot?.current });
const { data } = useThrottledDataProvider({
@@ -34,66 +30,20 @@ export const Last24hVolume = ({
variables: {
marketId: marketId || '',
interval: Schema.Interval.INTERVAL_I1H,
since: new Date(fiveDaysAgo).toISOString(),
since: new Date(yesterday).toISOString(),
},
skip: !(inView && marketId),
});
const fiveDaysCandles = data?.filter((candle) => Boolean(candle));
const oneDayCandles = fiveDaysCandles?.filter((candle) =>
isCandleLessThan24hOld(candle, yesterday)
);
if (
fiveDaysCandles &&
fiveDaysCandles.length > 0 &&
(!oneDayCandles || oneDayCandles?.length === 0)
) {
const candleVolume = calcCandleVolume(fiveDaysCandles);
const candleVolumeValue =
candleVolume && isNumeric(positionDecimalPlaces)
const candleVolume = data ? calcCandleVolume(data) : initialValue;
return (
<span ref={ref}>
{candleVolume && isNumeric(positionDecimalPlaces)
? addDecimalsFormatNumber(
candleVolume,
positionDecimalPlaces,
formatDecimals
)
: '-';
return (
<Tooltip
description={
<div>
<span className="flex flex-col">
{t(
'24 hour change is unavailable at this time. The volume change in the last 120 hours is %s',
[candleVolumeValue]
)}
</span>
</div>
}
>
<span ref={ref}>{t('Unknown')} </span>
</Tooltip>
);
}
const candleVolume = oneDayCandles
? calcCandleVolume(oneDayCandles)
: initialValue;
return (
<Tooltip
description={t(
'The total number of contracts traded in the last 24 hours.'
)}
>
<span ref={ref}>
{candleVolume && isNumeric(positionDecimalPlaces)
? addDecimalsFormatNumber(
candleVolume,
positionDecimalPlaces,
formatDecimals
)
: '-'}
</span>
</Tooltip>
: '-'}
</span>
);
};
@@ -104,7 +104,7 @@ export const OracleFullProfile = ({
<div className="mb-2">{message}</div>
<div className="mb-2">
<ReactMarkdown
className="react-markdown-container [word-break:break-word]"
className="react-markdown-container"
skipHtml={true}
disallowedElements={['img']}
linkTarget="_blank"
+1 -1
View File
@@ -187,7 +187,7 @@ const marketFieldsFragments: MarketFieldsFragment[] = [
name: 'Apple Monthly (30 Jun 2022)',
product: {
settlementAsset: {
id: 'asset-id',
id: 'asset-2',
name: '',
symbol: 'tUSDC',
decimals: 5,
@@ -1,23 +1,42 @@
import compact from 'lodash/compact';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import type { MarketFieldsFragment } from '@vegaprotocol/markets';
import { useMarketsQuery } from '@vegaprotocol/markets';
import { useAssetsQuery } from '@vegaprotocol/assets';
import { AgGridLazy } from '@vegaprotocol/datagrid';
import type { OrdersUpdateSubscription } from '../order-data-provider';
import { useOrdersUpdateSubscription } from '../order-data-provider';
import type { ArrayElement } from 'type-fest/source/internal';
import type { GridReadyEvent, FilterChangedEvent } from 'ag-grid-community';
import { OrderListTable } from '../order-list/order-list';
import { useHasAmendableOrder } from '../../order-hooks/use-has-amendable-order';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider';
import {
normalizeOrderAmendment,
useVegaTransactionStore,
} from '@vegaprotocol/wallet';
import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet';
import { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { Order } from '../order-data-provider';
import { OrderStatus } from '@vegaprotocol/types';
import { orderBy, uniqBy } from 'lodash';
import type { ColDef, GridApi } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils';
export enum Filter {
'Open',
'Closed',
'Rejected',
}
const FilterStatusValue = {
[Filter.Open]: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED],
[Filter.Closed]: [
OrderStatus.STATUS_CANCELLED,
OrderStatus.STATUS_EXPIRED,
OrderStatus.STATUS_FILLED,
OrderStatus.STATUS_PARTIALLY_FILLED,
OrderStatus.STATUS_STOPPED,
],
[Filter.Rejected]: [OrderStatus.STATUS_REJECTED],
};
export interface OrderListManagerProps {
partyId: string;
marketId?: string;
@@ -29,209 +48,164 @@ export interface OrderListManagerProps {
storeKey?: string;
}
const useMarkets = () => {
const { data, loading, error } = useMarketsQuery();
const CancelAllOrdersButton = ({ onClick }: { onClick: () => void }) => (
<div className="dark:bg-black/75 bg-white/75 h-auto flex justify-end px-[11px] py-2 absolute bottom-0 right-3 rounded">
<Button
variant="primary"
size="sm"
onClick={onClick}
data-testid="cancelAll"
>
{t('Cancel all')}
</Button>
</div>
);
const markets = data?.marketsConnection?.edges.map((e) => e.node) || [];
const marketLookup: Record<string, MarketFieldsFragment> = {};
markets.forEach((m) => {
marketLookup[m.id] = m;
export const OrderListManager = ({
partyId,
marketId,
onMarketClick,
onOrderTypeClick,
isReadOnly,
enforceBottomPlaceholder,
filter,
storeKey,
}: OrderListManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [hasData, setHasData] = useState(false);
const [editOrder, setEditOrder] = useState<Order | null>(null);
const create = useVegaTransactionStore((state) => state.create);
const hasAmendableOrder = useHasAmendableOrder(marketId);
const { data, error, loading, reload } = useDataProvider({
dataProvider: ordersWithMarketProvider,
variables:
filter === Filter.Open
? { partyId, filter: { liveOnly: true } }
: { partyId },
});
return {
markets: marketLookup,
data,
loading,
error,
};
};
const {
onFilterChanged: bottomPlaceholderOnFilterChanged,
...bottomPlaceholderProps
} = useBottomPlaceholder<Order>({
gridRef,
disabled: !enforceBottomPlaceholder && !isReadOnly && !hasAmendableOrder,
});
const useAssets = () => {
const { data, loading, error } = useAssetsQuery();
return {
assets: compact(data?.assetsConnection?.edges || []).map((e) => e.node),
data,
loading,
error,
};
};
type Order = ArrayElement<OrdersUpdateSubscription['orders']>;
const useOrders = (partyId?: string) => {
const loadRef = useRef(true);
const [orders, setOrders] = useState<Order[]>([]);
const { loading, error } = useOrdersUpdateSubscription({
variables: {
partyId: partyId || '',
},
skip: !partyId,
fetchPolicy: 'no-cache', // dont cache as we are caching here in the hook
onData: ({ data }) => {
const update = data.data?.orders || [];
if (loadRef.current) {
setOrders(update);
loadRef.current = false;
return;
}
if (!update.length) return;
// update orders
setOrders((curr) => {
// this might not be required all timestamps seem to be the same now
const sortedIncoming = orderBy(
update,
(o) => {
if (o.updatedAt) {
return new Date(o.updatedAt).getTime();
}
return new Date(o.createdAt).getTime();
},
'desc'
);
const combined = uniqBy([...sortedIncoming, ...curr], 'id');
return combined.filter((o) => isOrderActive(o));
const cancel = useCallback(
(order: Order) => {
if (!order.market) return;
create({
orderCancellation: {
orderId: order.id,
marketId: order.market.id,
},
});
},
});
[create]
);
return {
orders,
loading,
error,
};
};
const onGridReady = useCallback(
({ api }: GridReadyEvent) => {
if (filter !== undefined) {
api.setFilterModel({
status: {
value: FilterStatusValue[filter],
},
});
}
},
[filter]
);
const isOrderActive = (o: { status: OrderStatus }) => {
return [
OrderStatus.STATUS_ACTIVE,
OrderStatus.STATUS_PARKED,
OrderStatus.STATUS_PARTIALLY_FILLED,
].includes(o.status);
};
const onFilterChanged = useCallback(
(event: FilterChangedEvent) => {
const rowCount = gridRef.current?.api?.getModel().getRowCount();
setHasData((rowCount ?? 0) > 0);
bottomPlaceholderOnFilterChanged?.();
},
[bottomPlaceholderOnFilterChanged]
);
export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
const loadRef = useRef(true);
const [gridApi, setGridApi] = useState<GridApi | null>(null);
const { markets } = useMarkets();
const { assets } = useAssets();
// Example using react state
//
// Pass orders as rowData, because we use getRowId the grid should update 'naturally'
const { orders } = useOrders(partyId);
useEffect(() => {
setHasData((gridRef.current?.api?.getModel().getRowCount() ?? 0) > 0);
}, [data]);
// Example with gridApi.applyTransaction
//
// in this example the entire order state is in the grid as we
// dont cache in apollo and we discard the data once applied to the grid
// useOrdersUpdateSubscription({
// variables: {
// partyId: partyId || '',
// },
// skip: !gridApi || !partyId,
// fetchPolicy: 'no-cache', // dont cache as we are caching here in the hook
// onData: ({ data }) => {
// console.log(gridApi);
// if (!gridApi) {
// throw new Error('Grid not ready');
// }
// const incoming = data.data?.orders || [];
// if (loadRef.current) {
// loadRef.current = false;
// gridApi.applyTransaction({
// add: incoming,
// });
// return;
// }
// if (!incoming.length) return;
// const add: Order[] = [];
// const remove: Order[] = [];
// const update: Order[] = [];
// incoming.forEach((o) => {
// const exists = gridApi.getRowNode(o.id);
// if (exists) {
// if (isOrderActive(o)) {
// update.push(o);
// } else {
// remove.push(o);
// }
// } else {
// if (isOrderActive(o)) {
// add.push(o);
// }
// }
// });
// gridApi.applyTransaction({
// add,
// remove,
// update,
// });
// },
// });
const coldefs = useMemo<ColDef[]>(() => {
return [
{
field: 'marketId',
valueGetter: ({ data }) => {
const market = markets[data.marketId];
return market.tradableInstrument.instrument.code;
},
sortable: true,
filter: true,
},
{
field: 'status',
},
{
field: 'price',
},
{
field: 'side',
},
{
headerName: 'Created',
colId: 'createdAt',
sort: 'desc', // default sort by latest
valueGetter: ({ data }) => new Date(data.createdAt),
valueFormatter: ({ value }) => getDateTimeFormat().format(value),
},
{
field: 'updatedAt',
},
];
}, [markets]);
if (!Object.keys(markets).length) {
return <div>No markets</div>;
}
if (!assets.length) {
return <div>No assets</div>;
}
const cancelAll = useCallback(() => {
create({
orderCancellation: {},
});
}, [create]);
return (
<AgGridLazy
rowData={orders}
onGridReady={(event) => {
setGridApi(event.api);
}}
style={{ width: '100%', height: '100%' }}
columnDefs={coldefs}
getRowId={(params) => {
return params.data.id;
}}
/>
<>
<div className="h-full relative">
<OrderListTable
rowData={data as Order[]}
ref={gridRef}
filter={filter}
onGridReady={onGridReady}
cancel={cancel}
setEditOrder={setEditOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
onFilterChanged={onFilterChanged}
isReadOnly={isReadOnly}
blockLoadDebounceMillis={100}
storeKey={storeKey}
suppressLoadingOverlay
suppressNoRowsOverlay
suppressAutoSize
{...bottomPlaceholderProps}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
loading={loading}
error={error}
data={data}
noDataMessage={t('No orders')}
noDataCondition={(data) => !hasData}
reload={reload}
/>
</div>
</div>
{!isReadOnly && hasAmendableOrder && (
<CancelAllOrdersButton onClick={cancelAll} />
)}
{editOrder && (
<OrderEditDialog
isOpen={Boolean(editOrder)}
onChange={(isOpen) => {
if (!isOpen) setEditOrder(null);
}}
order={editOrder}
onSubmit={(fields) => {
if (!editOrder.market) {
return;
}
const orderAmendment = normalizeOrderAmendment(
editOrder,
editOrder.market,
fields.limitPrice,
fields.size
);
const originalOrder: OrderTxUpdateFieldsFragment = {
type: editOrder.type,
id: editOrder.id,
status: editOrder.status,
createdAt: editOrder.createdAt,
size: editOrder.size,
price: editOrder.price,
timeInForce: editOrder.timeInForce,
expiresAt: editOrder.expiresAt,
side: editOrder.side,
marketId: editOrder.market.id,
};
create({ orderAmendment }, originalOrder);
setEditOrder(null);
}}
/>
)}
</>
);
};
@@ -25,8 +25,8 @@ jest.mock('@vegaprotocol/react-helpers', () => ({
const defaultProps: OrderListTableProps = {
rowData: [],
onEdit: jest.fn(),
onCancel: jest.fn(),
setEditOrder: jest.fn(),
cancel: jest.fn(),
isReadOnly: false,
};
@@ -154,8 +154,8 @@ describe('OrderListTable', () => {
render(
generateJsx({
rowData: [order],
onEdit: mockEdit,
onCancel: mockCancel,
setEditOrder: mockEdit,
cancel: mockCancel,
})
);
});
@@ -179,8 +179,8 @@ describe('OrderListTable', () => {
render(
generateJsx({
rowData: [order],
onEdit: mockEdit,
onCancel: mockCancel,
setEditOrder: mockEdit,
cancel: mockCancel,
isReadOnly: true,
})
);
@@ -18,8 +18,8 @@ const Template: Story = (args) => {
<div style={{ height: 1000 }}>
<OrderListTable
rowData={args.data}
onCancel={cancel}
onEdit={() => {
cancel={cancel}
setEditOrder={() => {
return;
}}
isReadOnly={false}
@@ -47,8 +47,8 @@ const Template2: Story = (args) => {
<div style={{ height: 1000 }}>
<OrderListTable
rowData={args.data}
onCancel={cancel}
onEdit={setEditOrder}
cancel={cancel}
setEditOrder={setEditOrder}
isReadOnly={false}
/>
</div>
@@ -32,8 +32,8 @@ import { Filter } from '../order-list-manager';
export type OrderListTableProps = TypedDataAgGrid<Order> & {
marketId?: string;
onCancel: (order: Order) => void;
onEdit: (order: Order) => void;
cancel: (order: Order) => void;
setEditOrder: (order: Order) => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
filter?: Filter;
@@ -46,7 +46,14 @@ export const OrderListTable = memo<
>(
forwardRef<AgGridReact, OrderListTableProps>(
(
{ onCancel, onEdit, onMarketClick, onOrderTypeClick, filter, ...props },
{
cancel,
setEditOrder,
onMarketClick,
onOrderTypeClick,
filter,
...props
},
ref
) => {
const showAllActions =
@@ -268,11 +275,15 @@ export const OrderListTable = memo<
/>
<AgGridColumn
colId="amend"
field="id"
{...COL_DEFS.actions}
minWidth={showAllActions ? 120 : COL_DEFS.actions.minWidth}
maxWidth={showAllActions ? 120 : COL_DEFS.actions.minWidth}
cellRenderer={({ data }: { data?: Order }) => {
if (!data) return null;
cellRenderer={({
data,
value,
}: VegaICellRendererParams<Order, 'id'>) => {
if (!value || !data) return null;
return (
<div className="flex gap-2 items-center justify-end">
@@ -280,19 +291,19 @@ export const OrderListTable = memo<
<>
<ButtonLink
data-testid="edit"
onClick={() => onEdit(data)}
onClick={() => setEditOrder(data)}
>
{t('Edit')}
</ButtonLink>
<ButtonLink
data-testid="cancel"
onClick={() => onCancel(data)}
onClick={() => cancel(data)}
>
{t('Cancel')}
</ButtonLink>
</>
)}
<OrderActionsDropdown id={data?.id} />
<OrderActionsDropdown id={value} />
</div>
);
}}
+8 -9
View File
@@ -9,16 +9,16 @@ fragment PositionFields on Position {
market {
id
}
party {
id
}
}
query Positions($partyIds: [ID!]!) {
positions(filter: { partyIds: $partyIds }) {
edges {
node {
...PositionFields
query Positions($partyId: ID!) {
party(id: $partyId) {
id
positionsConnection {
edges {
node {
...PositionFields
}
}
}
}
@@ -34,7 +34,6 @@ subscription PositionsSubscription($partyId: ID!) {
marketId
lossSocializationAmount
positionStatus
partyId
}
}
+13 -14
View File
@@ -3,21 +3,21 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type PositionFieldsFragment = { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string }, party: { __typename?: 'Party', id: string } };
export type PositionFieldsFragment = { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string } };
export type PositionsQueryVariables = Types.Exact<{
partyIds: Array<Types.Scalars['ID']> | Types.Scalars['ID'];
partyId: Types.Scalars['ID'];
}>;
export type PositionsQuery = { __typename?: 'Query', positions?: { __typename?: 'PositionConnection', edges?: Array<{ __typename?: 'PositionEdge', node: { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string }, party: { __typename?: 'Party', id: string } } }> | null } | null };
export type PositionsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, positionsConnection?: { __typename?: 'PositionConnection', edges?: Array<{ __typename?: 'PositionEdge', node: { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string } } }> | null } | null } | null };
export type PositionsSubscriptionSubscriptionVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type PositionsSubscriptionSubscription = { __typename?: 'Subscription', positions: Array<{ __typename?: 'PositionUpdate', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, marketId: string, lossSocializationAmount: string, positionStatus: Types.PositionStatus, partyId: string }> };
export type PositionsSubscriptionSubscription = { __typename?: 'Subscription', positions: Array<{ __typename?: 'PositionUpdate', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, marketId: string, lossSocializationAmount: string, positionStatus: Types.PositionStatus }> };
export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } };
@@ -57,9 +57,6 @@ export const PositionFieldsFragmentDoc = gql`
market {
id
}
party {
id
}
}
`;
export const MarginFieldsFragmentDoc = gql`
@@ -77,11 +74,14 @@ export const MarginFieldsFragmentDoc = gql`
}
`;
export const PositionsDocument = gql`
query Positions($partyIds: [ID!]!) {
positions(filter: {partyIds: $partyIds}) {
edges {
node {
...PositionFields
query Positions($partyId: ID!) {
party(id: $partyId) {
id
positionsConnection {
edges {
node {
...PositionFields
}
}
}
}
@@ -100,7 +100,7 @@ export const PositionsDocument = gql`
* @example
* const { data, loading, error } = usePositionsQuery({
* variables: {
* partyIds: // value for 'partyIds'
* partyId: // value for 'partyId'
* },
* });
*/
@@ -126,7 +126,6 @@ export const PositionsSubscriptionDocument = gql`
marketId
lossSocializationAmount
positionStatus
partyId
}
}
`;
+2 -14
View File
@@ -7,14 +7,12 @@ export const PositionsContainer = ({
onMarketClick,
noBottomPlaceholder,
storeKey,
allKeys,
}: {
onMarketClick?: (marketId: string) => void;
noBottomPlaceholder?: boolean;
storeKey?: string;
allKeys?: boolean;
}) => {
const { pubKey, pubKeys, isReadOnly } = useVegaWallet();
const { pubKey, isReadOnly } = useVegaWallet();
if (!pubKey) {
return (
@@ -23,19 +21,9 @@ export const PositionsContainer = ({
</Splash>
);
}
const partyIds = [pubKey];
if (allKeys && pubKeys) {
partyIds.push(
...pubKeys
.map(({ publicKey }) => publicKey)
.filter((publicKey) => publicKey !== pubKey)
);
}
return (
<PositionsManager
partyIds={partyIds}
partyId={pubKey}
onMarketClick={onMarketClick}
isReadOnly={isReadOnly}
noBottomPlaceholder={noBottomPlaceholder}
@@ -1,7 +1,10 @@
import * as Schema from '@vegaprotocol/types';
import type { Account } from '@vegaprotocol/accounts';
import type { MarketWithData } from '@vegaprotocol/markets';
import type { PositionFieldsFragment } from './__generated__/Positions';
import type {
PositionFieldsFragment,
MarginFieldsFragment,
} from './__generated__/Positions';
import { getMetrics, rejoinPositionData } from './positions-data-providers';
import { PositionStatus } from '@vegaprotocol/types';
@@ -76,9 +79,6 @@ const positions: PositionFieldsFragment[] = [
__typename: 'Market',
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
},
party: {
id: 'partyId',
},
lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
@@ -93,9 +93,6 @@ const positions: PositionFieldsFragment[] = [
__typename: 'Market',
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
},
party: {
id: 'partyId',
},
lossSocializationAmount: '100',
positionStatus: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
},
@@ -116,8 +113,6 @@ const marketsData = [
product: {
settlementAsset: {
symbol: 'tDAI',
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
decimals: 5,
},
},
},
@@ -145,8 +140,6 @@ const marketsData = [
product: {
settlementAsset: {
symbol: 'tDAI',
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
decimals: 5,
},
},
},
@@ -162,23 +155,65 @@ const marketsData = [
},
] as MarketWithData[];
const margins: MarginFieldsFragment[] = [
{
__typename: 'MarginLevels',
maintenanceLevel: '0',
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
market: {
__typename: 'Market',
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
},
asset: {
__typename: 'Asset',
id: 'tDAI-id',
},
},
{
__typename: 'MarginLevels',
maintenanceLevel: '0',
searchLevel: '0',
initialLevel: '0',
collateralReleaseLevel: '0',
market: {
__typename: 'Market',
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
},
asset: {
__typename: 'Asset',
id: 'tDAI-id',
},
},
];
describe('getMetrics && rejoinPositionData', () => {
it('returns positions metrics', () => {
const positionsRejoined = rejoinPositionData(positions, marketsData);
const positionsRejoined = rejoinPositionData(
positions,
marketsData,
margins
);
const metrics = getMetrics(positionsRejoined, accounts || null);
expect(metrics.length).toEqual(2);
});
it('calculates metrics', () => {
const positionsRejoined = rejoinPositionData(positions, marketsData);
const positionsRejoined = rejoinPositionData(
positions,
marketsData,
margins
);
const metrics = getMetrics(positionsRejoined, accounts || null);
expect(metrics[0].assetSymbol).toEqual('tDAI');
expect(metrics[0].averageEntryPrice).toEqual('8993727');
expect(metrics[0].capitalUtilisation).toEqual(4);
expect(metrics[0].currentLeverage).toBeCloseTo(1.02);
expect(metrics[0].marketDecimalPlaces).toEqual(5);
expect(metrics[0].positionDecimalPlaces).toEqual(0);
expect(metrics[0].decimals).toEqual(5);
expect(metrics[0].lowMarginLevel).toEqual(false);
expect(metrics[0].markPrice).toEqual('9431775');
expect(metrics[0].marketId).toEqual(
'5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8'
@@ -190,6 +225,7 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[0].notional).toEqual('943177500');
expect(metrics[0].openVolume).toEqual('100');
expect(metrics[0].realisedPNL).toEqual('0');
expect(metrics[0].searchPrice).toEqual('9098238');
expect(metrics[0].totalBalance).toEqual('926178496');
expect(metrics[0].unrealisedPNL).toEqual('43804770');
expect(metrics[0].updatedAt).toEqual('2022-07-28T14:53:54.725477Z');
@@ -200,10 +236,12 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[1].assetSymbol).toEqual('tDAI');
expect(metrics[1].averageEntryPrice).toEqual('840158');
expect(metrics[1].capitalUtilisation).toEqual(0);
expect(metrics[1].currentLeverage).toBeCloseTo(0.097);
expect(metrics[1].marketDecimalPlaces).toEqual(5);
expect(metrics[1].positionDecimalPlaces).toEqual(0);
expect(metrics[1].decimals).toEqual(5);
expect(metrics[1].lowMarginLevel).toEqual(false);
expect(metrics[1].markPrice).toEqual('869762');
expect(metrics[1].marketId).toEqual(
'10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e'
@@ -213,6 +251,7 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[1].notional).toEqual('86976200');
expect(metrics[1].openVolume).toEqual('-100');
expect(metrics[1].realisedPNL).toEqual('0');
expect(metrics[1].searchPrice).toEqual('902503');
expect(metrics[1].totalBalance).toEqual('896098819');
expect(metrics[1].unrealisedPNL).toEqual('-9112700');
expect(metrics[1].updatedAt).toEqual('2022-07-28T15:09:34.441143Z');
@@ -19,41 +19,66 @@ import type {
PositionsQuery,
PositionFieldsFragment,
PositionsSubscriptionSubscription,
MarginFieldsFragment,
PositionsQueryVariables,
PositionsSubscriptionSubscriptionVariables,
} from './__generated__/Positions';
import {
PositionsDocument,
PositionsSubscriptionDocument,
} from './__generated__/Positions';
import { marginsDataProvider } from './margin-data-provider';
import type { PositionStatus } from '@vegaprotocol/types';
export interface Position {
assetId: string;
assetSymbol: string;
type PositionMarginLevel = Pick<
MarginFieldsFragment,
'maintenanceLevel' | 'searchLevel' | 'initialLevel'
>;
interface PositionRejoined {
realisedPNL: string;
openVolume: string;
unrealisedPNL: string;
averageEntryPrice: string;
updatedAt?: string | null;
market: MarketMaybeWithData | null;
margins: PositionMarginLevel | null;
lossSocializationAmount: string | null;
status: PositionStatus;
}
export interface Position {
marketName: string;
averageEntryPrice: string;
marginAccountBalance: string;
capitalUtilisation: number;
currentLeverage: number | undefined;
decimals: number;
lossSocializationAmount: string;
marginAccountBalance: string;
marketDecimalPlaces: number;
positionDecimalPlaces: number;
totalBalance: string;
assetSymbol: string;
assetId: string;
lowMarginLevel: boolean;
marketId: string;
marketName: string;
marketTradingMode: Schema.MarketTradingMode;
markPrice: string | undefined;
notional: string | undefined;
openVolume: string;
partyId: string;
positionDecimalPlaces: number;
realisedPNL: string;
status: PositionStatus;
totalBalance: string;
unrealisedPNL: string;
searchPrice: string | undefined;
updatedAt: string | null;
lossSocializationAmount: string;
status: PositionStatus;
}
export interface Data {
party: PositionsQuery['party'] | null;
positions: Position[] | null;
}
export const getMetrics = (
data: ReturnType<typeof rejoinPositionData> | null,
data: PositionRejoined[] | null,
accounts: Account[] | null
): Position[] => {
if (!data || !data?.length) {
@@ -62,32 +87,25 @@ export const getMetrics = (
const metrics: Position[] = [];
data.forEach((position) => {
const market = position.market;
if (!market) {
return;
}
const marketData = market?.data;
const marginLevel = position.margins;
const marginAccount = accounts?.find((account) => {
return account.market?.id === market?.id;
});
const {
decimals,
id: assetId,
symbol: assetSymbol,
} = market.tradableInstrument.instrument.product.settlementAsset;
if (!marginAccount || !marginLevel || !market) {
return;
}
const generalAccount = accounts?.find(
(account) =>
account.asset.id === assetId &&
account.asset.id === marginAccount.asset.id &&
account.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
);
const decimals = marginAccount.asset.decimals;
const { positionDecimalPlaces, decimalPlaces: marketDecimalPlaces } =
market;
const openVolume = toBigNum(position.openVolume, positionDecimalPlaces);
const marginAccountBalance = toBigNum(
marginAccount?.balance ?? 0,
decimals
);
const marginAccountBalance = toBigNum(marginAccount.balance ?? 0, decimals);
const generalAccountBalance = toBigNum(
generalAccount?.balance ?? 0,
decimals
@@ -108,30 +126,54 @@ export const getMetrics = (
? new BigNumber(0)
: notional.dividedBy(totalBalance)
: undefined;
const capitalUtilisation = totalBalance.isEqualTo(0)
? new BigNumber(0)
: marginAccountBalance.dividedBy(totalBalance).multipliedBy(100);
const marginSearch = toBigNum(marginLevel.searchLevel, decimals);
const marginInitial = toBigNum(marginLevel.initialLevel, decimals);
const searchPrice = markPrice
? marginSearch
.minus(marginAccountBalance)
.dividedBy(openVolume)
.plus(markPrice)
: undefined;
const lowMarginLevel =
marginAccountBalance.isLessThan(
marginSearch.plus(marginInitial.minus(marginSearch).dividedBy(2))
) && generalAccountBalance.isLessThan(marginInitial.minus(marginSearch));
metrics.push({
assetId,
assetSymbol,
averageEntryPrice: position.averageEntryPrice,
currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined,
decimals,
lossSocializationAmount: position.lossSocializationAmount || '0',
marginAccountBalance: marginAccount?.balance ?? '0',
marketDecimalPlaces,
marketId: market.id,
marketName: market.tradableInstrument.instrument.name,
averageEntryPrice: position.averageEntryPrice,
marginAccountBalance: marginAccount.balance,
capitalUtilisation: Math.round(capitalUtilisation.toNumber()),
currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined,
marketDecimalPlaces,
positionDecimalPlaces,
decimals,
assetSymbol:
market.tradableInstrument.instrument.product.settlementAsset.symbol,
assetId: market.tradableInstrument.instrument.product.settlementAsset.id,
totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(),
lowMarginLevel,
marketId: market.id,
marketTradingMode: market.tradingMode,
markPrice: marketData ? marketData.markPrice : undefined,
notional: notional
? notional.multipliedBy(10 ** marketDecimalPlaces).toFixed(0)
: undefined,
openVolume: position.openVolume,
partyId: position.party.id,
positionDecimalPlaces,
realisedPNL: position.realisedPNL,
status: position.positionStatus,
totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(),
unrealisedPNL: position.unrealisedPNL,
searchPrice: searchPrice
? searchPrice.multipliedBy(10 ** marketDecimalPlaces).toFixed(0)
: undefined,
updatedAt: position.updatedAt || null,
lossSocializationAmount: position.lossSocializationAmount || '0',
status: position.status,
});
});
return metrics;
@@ -144,8 +186,7 @@ export const update = (
return produce(data || [], (draft) => {
deltas.forEach((delta) => {
const index = draft.findIndex(
(node) =>
node.market.id === delta.marketId && node.party.id === delta.partyId
(node) => node.market.id === delta.marketId
);
if (index !== -1) {
const currNode = draft[index];
@@ -167,39 +208,49 @@ export const update = (
__typename: 'Market',
id: delta.marketId,
},
party: {
id: delta.partyId,
},
});
}
});
});
};
const getSubscriptionVariables = (
variables: PositionsQueryVariables
): PositionsSubscriptionSubscriptionVariables[] =>
([] as string[]).concat(variables.partyIds).map((partyId) => ({ partyId }));
const positionsDataProvider = makeDataProvider<
export const positionsDataProvider = makeDataProvider<
PositionsQuery,
PositionFieldsFragment[],
PositionsSubscriptionSubscription,
PositionsSubscriptionSubscription['positions'],
PositionsQueryVariables,
PositionsSubscriptionSubscriptionVariables
PositionsQueryVariables
>({
query: PositionsDocument,
subscriptionQuery: PositionsSubscriptionDocument,
update,
getData: (responseData: PositionsQuery | null) =>
removePaginationWrapper(responseData?.positions?.edges) || [],
removePaginationWrapper(responseData?.party?.positionsConnection?.edges) ||
[],
getDelta: (subscriptionData: PositionsSubscriptionSubscription) =>
subscriptionData.positions,
getSubscriptionVariables,
});
const positionDataProvider = makeDerivedDataProvider<
const upgradeMarginsConnection = (
marketId: string,
margins: MarginFieldsFragment[] | null
) => {
if (marketId && margins) {
const index =
margins.findIndex((node) => node.market.id === marketId) ?? -1;
if (index >= 0) {
const marginLevel = margins[index];
return {
maintenanceLevel: marginLevel.maintenanceLevel,
searchLevel: marginLevel.searchLevel,
initialLevel: marginLevel.initialLevel,
};
}
}
return null;
};
export const positionDataProvider = makeDerivedDataProvider<
PositionFieldsFragment,
never,
PositionsQueryVariables & MarketDataQueryVariables
@@ -207,7 +258,7 @@ const positionDataProvider = makeDerivedDataProvider<
[
(callback, client, variables) =>
positionsDataProvider(callback, client, {
partyIds: variables.partyIds,
partyId: variables?.partyId || '',
}),
],
(data, variables) =>
@@ -227,18 +278,22 @@ export const openVolumeDataProvider = makeDerivedDataProvider<
export const rejoinPositionData = (
positions: PositionFieldsFragment[] | null,
marketsData: MarketMaybeWithData[] | null
):
| (Omit<PositionFieldsFragment, 'market'> & {
market: MarketMaybeWithData | null;
})[]
| null => {
if (positions && marketsData) {
marketsData: MarketMaybeWithData[] | null,
margins: MarginFieldsFragment[] | null
): PositionRejoined[] | null => {
if (positions && marketsData && margins) {
return positions.map((node) => {
return {
...node,
realisedPNL: node.realisedPNL,
openVolume: node.openVolume,
unrealisedPNL: node.unrealisedPNL,
averageEntryPrice: node.averageEntryPrice,
updatedAt: node.updatedAt,
market:
marketsData?.find((market) => market.id === node.market.id) || null,
margins: upgradeMarginsConnection(node.market.id, margins),
lossSocializationAmount: node.lossSocializationAmount,
status: node.positionStatus,
};
});
}
@@ -252,17 +307,13 @@ export const positionsMetricsProvider = makeDerivedDataProvider<
>(
[
positionsDataProvider,
(callback, client, variables) =>
accountsDataProvider(callback, client, {
partyId: Array.isArray(variables.partyIds)
? variables.partyIds[0]
: variables.partyIds,
}),
accountsDataProvider,
(callback, client) =>
allMarketsWithDataProvider(callback, client, undefined),
marginsDataProvider,
],
([positions, accounts, marketsData], variables) => {
const positionsData = rejoinPositionData(positions, marketsData);
([positions, accounts, marketsData, margins], variables) => {
const positionsData = rejoinPositionData(positions, marketsData, margins);
if (!variables) {
return [];
}
+12 -10
View File
@@ -1,5 +1,6 @@
import { useCallback, useRef, useState } from 'react';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import type { Position } from './positions-data-providers';
import { usePositionsData } from './use-positions-data';
import { PositionsTable } from './positions-table';
import type { AgGridReact } from 'ag-grid-react';
@@ -7,10 +8,9 @@ import * as Schema from '@vegaprotocol/types';
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
import { t } from '@vegaprotocol/i18n';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
import { useVegaWallet } from '@vegaprotocol/wallet';
interface PositionsManagerProps {
partyIds: string[];
partyId: string;
onMarketClick?: (marketId: string) => void;
isReadOnly: boolean;
noBottomPlaceholder?: boolean;
@@ -18,15 +18,14 @@ interface PositionsManagerProps {
}
export const PositionsManager = ({
partyIds,
partyId,
onMarketClick,
isReadOnly,
noBottomPlaceholder,
storeKey,
}: PositionsManagerProps) => {
const { pubKeys, pubKey } = useVegaWallet();
const gridRef = useRef<AgGridReact | null>(null);
const { data, error, loading, reload } = usePositionsData(partyIds, gridRef);
const { data, error, loading, reload } = usePositionsData(partyId, gridRef);
const [dataCount, setDataCount] = useState(data?.length ?? 0);
const create = useVegaTransactionStore((store) => store.create);
const onClose = ({
@@ -59,19 +58,23 @@ export const PositionsManager = ({
},
});
const bottomPlaceholderProps = useBottomPlaceholder({
const setId = useCallback((data: Position, id: string) => {
return {
...data,
marketId: id,
};
}, []);
const bottomPlaceholderProps = useBottomPlaceholder<Position>({
gridRef,
setId,
disabled: noBottomPlaceholder,
});
const updateRowCount = useCallback(() => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, []);
return (
<div className="h-full relative">
<PositionsTable
pubKey={pubKey}
pubKeys={pubKeys}
rowData={error ? [] : data}
ref={gridRef}
onMarketClick={onMarketClick}
@@ -83,7 +86,6 @@ export const PositionsManager = ({
onRowDataUpdated={updateRowCount}
{...bottomPlaceholderProps}
storeKey={storeKey}
multipleKeys={partyIds.length > 1}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
+15 -14
View File
@@ -8,27 +8,29 @@ import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import type { ICellRendererParams } from 'ag-grid-community';
const singleRow: Position = {
partyId: 'partyId',
assetId: 'asset-id',
assetSymbol: 'BTC',
averageEntryPrice: '133',
currentLeverage: 1.1,
decimals: 2,
lossSocializationAmount: '0',
marginAccountBalance: '12345600',
marketDecimalPlaces: 1,
marketId: 'string',
marketName: 'ETH/BTC (31 july 2022)',
averageEntryPrice: '133',
capitalUtilisation: 11,
currentLeverage: 1.1,
marketDecimalPlaces: 1,
positionDecimalPlaces: 0,
decimals: 2,
totalBalance: '123456',
assetSymbol: 'BTC',
assetId: 'asset-id',
lowMarginLevel: false,
marketId: 'string',
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
markPrice: '123',
notional: '12300',
openVolume: '100',
positionDecimalPlaces: 0,
realisedPNL: '123',
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
totalBalance: '123456',
unrealisedPNL: '456',
searchPrice: '0',
updatedAt: '2022-07-27T15:02:58.400Z',
marginAccountBalance: '12345600',
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
lossSocializationAmount: '0',
};
const singleRowData = [singleRow];
@@ -173,7 +175,6 @@ it('displays close button', async () => {
render(
<PositionsTable
rowData={singleRowData}
pubKey={singleRowData[0].partyId}
onClose={() => {
return;
}}
@@ -15,51 +15,69 @@ const Template: Story = (args) => (
export const Primary = Template.bind({});
const longPosition: Position = {
assetId: 'BTC',
assetSymbol: 'BTC',
marketName: 'BTC/USD (31 july 2022)',
averageEntryPrice: '1134564',
capitalUtilisation: 10,
currentLeverage: 11,
decimals: 2,
lossSocializationAmount: '0',
marginAccountBalance: new BigNumber('0').toString(),
marketDecimalPlaces: 2,
positionDecimalPlaces: 2,
// generalAccountBalance: '0',
totalBalance: '45353',
assetSymbol: 'BTC',
// leverageInitial: '0',
// leverageMaintenance: '0',
// leverageRelease: '0',
// leverageSearch: '0',
lowMarginLevel: false,
marginAccountBalance: new BigNumber('0').toString(),
// marginMaintenance: '0',
// marginSearch: '0',
// marginInitial: '0',
marketId: 'marketId1',
marketName: 'BTC/USD (31 july 2022)',
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
markPrice: '1131894',
notional: '46667989',
openVolume: '4123',
positionDecimalPlaces: 2,
realisedPNL: '45',
status: Schema.PositionStatus.POSITION_STATUS_UNSPECIFIED,
totalBalance: '45353',
unrealisedPNL: '45',
searchPrice: '1132123',
updatedAt: '2022-07-27T15:02:58.400Z',
partyId: 'partyId',
lossSocializationAmount: '0',
status: Schema.PositionStatus.POSITION_STATUS_UNSPECIFIED,
};
const shortPosition: Position = {
assetId: 'ETH',
assetSymbol: 'ETH',
marketName: 'ETH/USD (31 august 2022)',
averageEntryPrice: '23976',
capitalUtilisation: 87,
currentLeverage: 7,
decimals: 2,
lossSocializationAmount: '0',
marginAccountBalance: new BigNumber('0').toString(),
marketDecimalPlaces: 2,
positionDecimalPlaces: 2,
// generalAccountBalance: '0',
totalBalance: '3856',
assetSymbol: 'ETH',
// leverageInitial: '0',
// leverageMaintenance: '0',
// leverageRelease: '0',
// leverageSearch: '0',
lowMarginLevel: false,
marginAccountBalance: new BigNumber('0').toString(),
// marginMaintenance: '0',
// marginSearch: '0',
// marginInitial: '0',
marketId: 'marketId2',
marketName: 'ETH/USD (31 august 2022)',
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
markPrice: '24123',
notional: '836344',
openVolume: '-3467',
positionDecimalPlaces: 2,
realisedPNL: '0',
status: Schema.PositionStatus.POSITION_STATUS_UNSPECIFIED,
totalBalance: '3856',
unrealisedPNL: '0',
searchPrice: '0',
updatedAt: '2022-07-26T14:01:34.800Z',
partyId: 'partyId',
lossSocializationAmount: '0',
status: Schema.PositionStatus.POSITION_STATUS_UNSPECIFIED,
};
Primary.args = {
+51 -89
View File
@@ -42,7 +42,6 @@ import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import { DocsLinks } from '@vegaprotocol/environment';
import { PositionTableActions } from './position-actions-dropdown';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void;
@@ -50,9 +49,6 @@ interface Props extends TypedDataAgGrid<Position> {
style?: CSSProperties;
isReadOnly: boolean;
storeKey?: string;
multipleKeys?: boolean;
pubKeys?: VegaWalletContextShape['pubKeys'];
pubKey?: VegaWalletContextShape['pubKey'];
}
export interface AmountCellProps {
@@ -87,18 +83,7 @@ export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
AmountCell.displayName = 'AmountCell';
export const PositionsTable = forwardRef<AgGridReact, Props>(
(
{
onClose,
onMarketClick,
multipleKeys,
isReadOnly,
pubKeys,
pubKey,
...props
},
ref
) => {
({ onClose, onMarketClick, ...props }, ref) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return (
<AgGrid
@@ -122,21 +107,6 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}}
{...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"
@@ -297,61 +267,55 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}}
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)
<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}
/>
<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;
}
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}
/>
)}
return addDecimalsFormatNumber(
data.marginAccountBalance,
data.decimals
);
}}
minWidth={100}
/>
<AgGridColumn
headerName={t('Realised PNL')}
field="realisedPNL"
@@ -421,15 +385,13 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}}
minWidth={150}
/>
{onClose && !isReadOnly ? (
{onClose && !props.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 ? (
{data?.openVolume && data?.openVolume !== '0' ? (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}
+10 -18
View File
@@ -12,12 +12,16 @@ export const positionsQuery = (
override?: PartialDeep<PositionsQuery>
): PositionsQuery => {
const defaultResult: PositionsQuery = {
positions: {
__typename: 'PositionConnection',
edges: positionFields.map((node) => ({
__typename: 'PositionEdge',
node,
})),
party: {
__typename: 'Party',
id: 'vega-0', // VEGA PUBLIC KEY
positionsConnection: {
__typename: 'PositionConnection',
edges: positionFields.map((node) => ({
__typename: 'PositionEdge',
node,
})),
},
},
};
@@ -55,10 +59,6 @@ const positionFields: PositionFieldsFragment[] = [
id: 'market-0',
__typename: 'Market',
},
party: {
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
__typename: 'Party',
},
lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
@@ -73,10 +73,6 @@ const positionFields: PositionFieldsFragment[] = [
id: 'market-1',
__typename: 'Market',
},
party: {
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
__typename: 'Party',
},
lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
@@ -91,10 +87,6 @@ const positionFields: PositionFieldsFragment[] = [
id: 'market-2',
__typename: 'Market',
},
party: {
id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
__typename: 'Party',
},
lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
+1 -1
View File
@@ -14,7 +14,7 @@ export const useOpenVolume = (
useDataProvider({
dataProvider: openVolumeDataProvider,
update,
variables: { partyIds: partyId ? [partyId] : [], marketId },
variables: { partyId: partyId || '', marketId },
skip: !partyId,
});
return openVolume;
@@ -59,7 +59,7 @@ describe('usePositionData Hook', () => {
.mockImplementation((id: string) =>
mockData.find((position) => position.marketId === id)
);
const partyIds = ['partyId'];
const partyId = 'partyId';
const anUpdatedOne = {
marketId: 'market-1',
openVolume: '1',
@@ -75,14 +75,14 @@ describe('usePositionData Hook', () => {
};
it('should return proper data', async () => {
const { result } = renderHook(() => usePositionsData(partyIds, gridRef), {
const { result } = renderHook(() => usePositionsData(partyId, gridRef), {
wrapper: MockedProvider,
});
expect(result.current.data?.length ?? 0).toEqual(5);
});
it('should call mockRefreshInfiniteCache', async () => {
renderHook(() => usePositionsData(partyIds, gridRef), {
renderHook(() => usePositionsData(partyId, gridRef), {
wrapper: MockedProvider,
});
await waitFor(() => {
@@ -99,7 +99,7 @@ describe('usePositionData Hook', () => {
data: mockData,
loading: false,
};
const { result } = renderHook(() => usePositionsData(partyIds, gridRef), {
const { result } = renderHook(() => usePositionsData(partyId, gridRef), {
wrapper: MockedProvider,
});
expect(result.current.data).toEqual([]);
+9 -18
View File
@@ -9,22 +9,15 @@ import { useDataProvider } from '@vegaprotocol/data-provider';
import type { GetRowsParams } from '@vegaprotocol/datagrid';
import isEqual from 'lodash/isEqual';
export const getRowId = ({
data,
}: {
data: Position & { isLastPlaceholder?: boolean; id?: string };
}) =>
data.isLastPlaceholder && data.id
? data.id
: `${data.partyId}-${data.marketId}`;
export const getRowId = ({ data }: { data: Position }) => data.marketId;
export const usePositionsData = (
partyIds: string[],
partyId: string,
gridRef: RefObject<AgGridReact>
) => {
const variables = useMemo<PositionsQueryVariables>(
() => ({ partyIds }),
[partyIds]
() => ({ partyId }),
[partyId]
);
const dataRef = useRef<Position[] | null>(null);
const update = useCallback(
@@ -35,16 +28,14 @@ export const usePositionsData = (
const update: Position[] = [];
const add: Position[] = [];
data?.forEach((row) => {
const rowNode = gridRef.current?.api?.getRowNode(
getRowId({ data: row })
);
data?.forEach((d) => {
const rowNode = gridRef.current?.api?.getRowNode(d.marketId);
if (rowNode) {
if (!isEqual(rowNode.data, row)) {
update.push(row);
if (!isEqual(rowNode.data, d)) {
update.push(d);
}
} else {
add.push(row);
add.push(d);
}
});
gridRef.current?.api?.applyTransaction({
@@ -1,5 +1,5 @@
import { act } from 'react-dom/test-utils';
import { createAgo, now } from './use-yesterday';
import { now, useYesterday } from './use-yesterday';
import { renderHook } from '@testing-library/react';
describe('now', () => {
@@ -25,41 +25,33 @@ describe('now', () => {
);
});
describe('createAgo', () => {
beforeEach(() => {
jest.useFakeTimers().setSystemTime(new Date('1970-01-30T14:36:20.100Z'));
describe('useYesterday', () => {
beforeAll(() => {
jest.useFakeTimers().setSystemTime(new Date('1970-01-05T14:36:20.100Z'));
});
afterAll(() => {
jest.useRealTimers();
});
it.each([
['yesterday', 24 * 60 * 60 * 1000, '1970-01-29T14:35:00.000Z'],
['2 days ago', 2 * 24 * 60 * 60 * 1000, '1970-01-28T14:35:00.000Z'],
['5 days ago', 5 * 24 * 60 * 60 * 1000, '1970-01-25T14:35:00.000Z'],
['20 days ago', 20 * 24 * 60 * 60 * 1000, '1970-01-10T14:35:00.000Z'],
])('returns %s timestamp rounded by 5 minutes', (_, ago, expectedTime) => {
const { result, rerender } = renderHook(() =>
createAgo(ago)(5 * 60 * 1000)
it('returns yesterday timestamp rounded by 5 minutes', () => {
const { result, rerender } = renderHook(() => useYesterday());
expect(result.current).toEqual(
new Date('1970-01-04T14:35:00.000Z').getTime()
);
expect(result.current).toEqual(new Date(expectedTime).getTime());
rerender();
rerender();
rerender();
expect(result.current).toEqual(new Date(expectedTime).getTime());
expect(result.current).toEqual(
new Date('1970-01-04T14:35:00.000Z').getTime()
);
});
it.each([
['yesterday', 24 * 60 * 60 * 1000, '1970-01-29T14:40:00.000Z'],
['2 days ago', 2 * 24 * 60 * 60 * 1000, '1970-01-28T14:40:00.000Z'],
['5 days ago', 5 * 24 * 60 * 60 * 1000, '1970-01-25T14:40:00.000Z'],
['20 days ago', 20 * 24 * 60 * 60 * 1000, '1970-01-10T14:40:00.000Z'],
])('updates %s timestamp after 5 minutes', (_, ago, expectedTime) => {
const { result, rerender } = renderHook(() =>
createAgo(ago)(5 * 60 * 1000)
);
it('updates yesterday timestamp after 5 minutes', () => {
const { result, rerender } = renderHook(() => useYesterday());
act(() => {
jest.advanceTimersByTime(5 * 60 * 1000);
rerender();
});
expect(result.current).toEqual(new Date(expectedTime).getTime());
expect(result.current).toEqual(
new Date('1970-01-04T14:40:00.000Z').getTime()
);
});
});
+12 -20
View File
@@ -1,30 +1,22 @@
import { useEffect, useRef } from 'react';
const MINUTE = 60 * 1000;
const DAY = 24 * 60 * 60 * 1000;
const DEFAULT_ROUND_BY_MS = 5 * 60 * 1000;
const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000;
export const now = (roundBy = 1) => {
return Math.floor((Math.round(Date.now() / 1000) * 1000) / roundBy) * roundBy;
};
export const createAgo =
(ago: number) =>
(roundBy = 5 * MINUTE) => {
const timestamp = useRef<number>(now(roundBy) - ago);
useEffect(() => {
const i = setInterval(() => {
timestamp.current = now(roundBy) - ago;
}, roundBy);
return () => clearInterval(i);
}, [roundBy]);
return timestamp.current;
};
/**
* Returns the yesterday's timestamp rounded by given number (in milliseconds; 5 minutes by default)
*/
export const useYesterday = createAgo(DAY);
/**
* Returns the five days ago timestamp rounded by given number (in milliseconds; 5 minutes by default)
*/
export const useFiveDaysAgo = createAgo(5 * DAY);
export const useYesterday = (roundBy = DEFAULT_ROUND_BY_MS) => {
const yesterday = useRef<number>(now(roundBy) - TWENTY_FOUR_HOURS_MS);
useEffect(() => {
const i = setInterval(() => {
yesterday.current = now(roundBy) - TWENTY_FOUR_HOURS_MS;
}, roundBy);
return () => clearInterval(i);
}, [roundBy]);
return yesterday.current;
};
-2
View File
@@ -86,7 +86,6 @@
"recharts": "^2.1.2",
"recursive-key-filter": "^1.0.2",
"regenerator-runtime": "0.13.7",
"semver": "^7.5.1",
"toml": "^3.0.0",
"tslib": "^2.0.0",
"uuid": "^8.3.2",
@@ -152,7 +151,6 @@
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5",
"@types/react-window-infinite-loader": "^1.0.6",
"@types/semver": "^7.5.0",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "5.35.1",
"@typescript-eslint/parser": "5.35.1",
-12
View File
@@ -7554,11 +7554,6 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
"@types/semver@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a"
integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==
"@types/serve-index@^1.9.1":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278"
@@ -22060,13 +22055,6 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.5.1:
version "7.5.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec"
integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==
dependencies:
lru-cache "^6.0.0"
send@0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"