Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd0b29c251 | ||
|
|
6471be323d | ||
|
|
3e9aeb2f2f | ||
|
|
e83a7915ad | ||
|
|
d2b2d16c9c | ||
|
|
16b2360da7 | ||
|
|
1adbc2dbd7 | ||
|
|
d6f39049bd | ||
|
|
1d06be8f4e | ||
|
|
5eba8fe28f | ||
|
|
2ba0e9a1b2 | ||
|
|
43d3754c64 | ||
|
|
aae5c44fa4 | ||
|
|
878bed9c7a | ||
|
|
6f9f432c90 | ||
|
|
3f01b93159 | ||
|
|
473c244d7b | ||
|
|
d6a32f5090 | ||
|
|
71540a90fb | ||
|
|
cefdbe6a3c | ||
|
|
3928dd5c0e | ||
|
|
1521bab4c4 | ||
|
|
b3036d520f | ||
|
|
429a5a23d3 | ||
|
|
5b02fd5d54 | ||
|
|
b8309a76e7 | ||
|
|
e8ae085c06 | ||
|
|
fdcd24847c | ||
|
|
e054db39b5 | ||
|
|
a01e48d508 |
@@ -113,15 +113,19 @@ jobs:
|
||||
preview_governance="not deployed"
|
||||
preview_trading="not deployed"
|
||||
preview_explorer="not deployed"
|
||||
if [[ $affected == *"governance"* ]]; then
|
||||
preview_tools="not deployed"
|
||||
if echo "$affected" | grep -q governance; then
|
||||
echo "Governance is affected"
|
||||
projects_e2e+='"governance-e2e" '
|
||||
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
|
||||
fi
|
||||
if [[ $affected == *"trading"* ]]; then
|
||||
if echo "$affected" | grep -q trading; then
|
||||
echo "Trading is affected"
|
||||
projects_e2e+='"trading-e2e" '
|
||||
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
|
||||
fi
|
||||
if [[ $affected == *"explorer"* ]]; then
|
||||
if echo "$affected" | grep -q explorer; then
|
||||
echo "Explorer is affected"
|
||||
projects_e2e+='"explorer-e2e" '
|
||||
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
|
||||
fi
|
||||
@@ -131,13 +135,30 @@ jobs:
|
||||
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
|
||||
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
|
||||
fi
|
||||
projects="$(echo $projects_e2e | sed 's|-e2e||g')"
|
||||
if echo "$affected" | grep -q multisig-signer; then
|
||||
echo "Tools are affected"
|
||||
# tools are only applicable to check previews or deploy from develop to mainnet
|
||||
if [[ "${{ github.event_name }}" = "pull_request" ]]; then
|
||||
echo "Deploying tools on preview"
|
||||
preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug")
|
||||
projects+=' "multisig-signer" '
|
||||
fi
|
||||
if [[ "${{ github.ref }}" =~ .*develop$ ]]; then
|
||||
echo "Deploying tools on s3"
|
||||
projects+=' "multisig-signer" '
|
||||
fi
|
||||
fi
|
||||
projects_e2e=${projects_e2e%?}
|
||||
projects_e2e=[${projects_e2e// /,}]
|
||||
projects=${projects%?}
|
||||
projects=[${projects// /,}]
|
||||
echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV
|
||||
echo PROJECTS=$(echo $projects_e2e | sed 's|-e2e||g') >> $GITHUB_ENV
|
||||
echo PROJECTS=$projects >> $GITHUB_ENV
|
||||
echo PREVIEW_GOVERNANCE=$preview_governance >> $GITHUB_ENV
|
||||
echo PREVIEW_TRADING=$preview_trading >> $GITHUB_ENV
|
||||
echo PREVIEW_EXPLORER=$preview_explorer >> $GITHUB_ENV
|
||||
echo PREVIEW_TOOLS=$preview_tools >> $GITHUB_ENV
|
||||
|
||||
outputs:
|
||||
projects: ${{ env.PROJECTS }}
|
||||
@@ -145,11 +166,12 @@ jobs:
|
||||
preview_governance: ${{ env.PREVIEW_GOVERNANCE }}
|
||||
preview_trading: ${{ env.PREVIEW_TRADING }}
|
||||
preview_explorer: ${{ env.PREVIEW_EXPLORER }}
|
||||
preview_tools: ${{ env.PREVIEW_TOOLS }}
|
||||
|
||||
cypress:
|
||||
needs: lint-test-build
|
||||
name: '(CI) cypress'
|
||||
if: ${{ needs.lint-test-build.outputs.projects != '[]' }}
|
||||
if: ${{ needs.lint-test-build.outputs.projects-e2e != '[]' }}
|
||||
uses: ./.github/workflows/cypress-run.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
@@ -203,6 +225,12 @@ jobs:
|
||||
sleep 5
|
||||
done
|
||||
fi
|
||||
if [[ "${{ needs.lint-test-build.outputs.preview_tools }}" =~ $regex ]]; then
|
||||
until curl -L --fail "${{ needs.lint-test-build.outputs.preview_tools }}"; do
|
||||
echo "waiting for tools preview"
|
||||
sleep 5
|
||||
done
|
||||
fi
|
||||
|
||||
- name: Create comment
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
@@ -214,6 +242,7 @@ jobs:
|
||||
* governance: ${{ needs.lint-test-build.outputs.preview_governance }}
|
||||
* explorer: ${{ needs.lint-test-build.outputs.preview_explorer }}
|
||||
* trading: ${{ needs.lint-test-build.outputs.preview_trading }}
|
||||
* tools: ${{ needs.lint-test-build.outputs.preview_tools }}
|
||||
|
||||
# Report single result at the end, to avoid mess with required checks in PR
|
||||
cypress-check:
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
project: ${{ fromJSON(inputs.projects) }}
|
||||
name: ${{ matrix.project }}
|
||||
runs-on: self-hosted-runner
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 100
|
||||
steps:
|
||||
# Checks if skip cache was requested
|
||||
- name: Set skip-nx-cache flag
|
||||
|
||||
@@ -70,6 +70,10 @@ jobs:
|
||||
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)"
|
||||
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
|
||||
envName="stagnet1"
|
||||
if [[ "${{ matrix.app }}" = "multisig-signer" ]]; then
|
||||
envName="mainnet"
|
||||
bucketName="tools.vega.xyz"
|
||||
fi
|
||||
elif [[ "${{ github.ref }}" =~ .*main$ ]]; then
|
||||
envName="mainnet"
|
||||
elif [[ "${{ matrix.app}}" = "trading" ]] && [[ "${{ startsWith(github.ref, 'refs/tags/v') && 'true' || 'false' }}" = "true" ]]; then
|
||||
@@ -78,10 +82,14 @@ jobs:
|
||||
|
||||
if [[ "${envName}" = "mainnet" ]]; then
|
||||
domain="vega.xyz"
|
||||
bucketName="${{ matrix.app }}.${domain}"
|
||||
if [[ -z "${bucketName}" ]]; then
|
||||
bucketName="${{ matrix.app }}.${domain}"
|
||||
fi
|
||||
elif [[ "${envName}" = "testnet" ]]; then
|
||||
domain="fairground.wtf"
|
||||
bucketName="${{ matrix.app }}.${domain}"
|
||||
if [[ -z "${bucketName}" ]]; then
|
||||
bucketName="${{ matrix.app }}.${domain}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${bucketName}" ]]; then
|
||||
|
||||
@@ -15,6 +15,7 @@ NX_VEGA_GOVERNANCE_URL=https://governance.stagnet1.vega.rocks
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_NETWORKS='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz"}'
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases/tag/
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS=1
|
||||
|
||||
@@ -103,8 +103,6 @@ describe(
|
||||
it('Newly created freeform proposal details - shows proposed and closing dates', function () {
|
||||
const proposalTitle = generateFreeFormProposalTitle();
|
||||
const proposalTimeStamp = createTenDigitUnixTimeStampForSpecifiedDays(3);
|
||||
// const currentDate = new Date(createTenDigitUnixTimeStampForSpecifiedDays(0) * 1000)
|
||||
// const proposedDate = new Date(currentDate.getTime() + 60000)
|
||||
|
||||
submitUniqueRawProposal({
|
||||
proposalTitle: proposalTitle,
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
createUpdateNetworkProposalTxBody,
|
||||
createFreeFormProposalTxBody,
|
||||
} from '../../support/proposal.functions';
|
||||
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
|
||||
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
|
||||
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
|
||||
|
||||
@@ -43,6 +44,7 @@ context(
|
||||
waitForSpinner();
|
||||
cy.connectVegaWallet();
|
||||
ethereumWalletConnect();
|
||||
ensureSpecifiedUnstakedTokensAreAssociated('1');
|
||||
navigateTo(navigation.proposals);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
closeDialog,
|
||||
dissociateFromSecondWalletKey,
|
||||
navigateTo,
|
||||
navigation,
|
||||
turnTelemetryOff,
|
||||
@@ -15,7 +16,11 @@ import {
|
||||
governanceProposalType,
|
||||
voteForProposal,
|
||||
} from '../../support/governance.functions';
|
||||
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
|
||||
import {
|
||||
ensureSpecifiedUnstakedTokensAreAssociated,
|
||||
stakingPageAssociateTokens,
|
||||
stakingPageDisassociateAllTokens,
|
||||
} from '../../support/staking.functions';
|
||||
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
|
||||
import {
|
||||
vegaWalletSetSpecifiedApprovalAmount,
|
||||
@@ -100,9 +105,9 @@ context(
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath, submit: false });
|
||||
});
|
||||
cy.get(newProposalSubmitButton).click();
|
||||
validateDialogContentMsg('PROPOSAL_ERROR_CLOSE_TIME_TOO_SOON');
|
||||
});
|
||||
cy.get(newProposalSubmitButton).click();
|
||||
validateDialogContentMsg('PROPOSAL_ERROR_CLOSE_TIME_TOO_SOON');
|
||||
});
|
||||
|
||||
// 3007-PNEC-001 3007-PNEC-003
|
||||
@@ -182,13 +187,17 @@ context(
|
||||
'have.text',
|
||||
'Proposal will fail if enactment is earlier than the voting deadline'
|
||||
);
|
||||
cy.get(proposalDownloadBtn).click();
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-network-param-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath, submit: false });
|
||||
});
|
||||
cy.get(proposalDownloadBtn)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-network-param-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath, submit: false });
|
||||
});
|
||||
});
|
||||
cy.get(newProposalSubmitButton).click();
|
||||
validateFeedBackMsg(
|
||||
'Invalid params: proposal_submission.terms.closing_timestamp (cannot be after enactment time)'
|
||||
@@ -207,13 +216,17 @@ context(
|
||||
delay: 2,
|
||||
});
|
||||
});
|
||||
cy.get(proposalDownloadBtn).should('be.visible').click();
|
||||
cy.wrap(getDownloadedProposalJsonPath('vega-new-market-proposal-')).then(
|
||||
(filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath }); // 3003-PMAN-003
|
||||
}
|
||||
);
|
||||
cy.get(proposalDownloadBtn)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-new-market-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath }); // 3003-PMAN-003
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Unable to submit new market proposal with missing/invalid fields', function () {
|
||||
@@ -233,13 +246,17 @@ context(
|
||||
delay: 2,
|
||||
});
|
||||
});
|
||||
cy.get(proposalDownloadBtn).should('be.visible').click();
|
||||
cy.wrap(getDownloadedProposalJsonPath('vega-new-market-proposal-')).then(
|
||||
(filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath, submit: false });
|
||||
}
|
||||
);
|
||||
cy.get(proposalDownloadBtn)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-new-market-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath, submit: false });
|
||||
});
|
||||
});
|
||||
cy.get(newProposalSubmitButton).should('be.visible').click();
|
||||
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
|
||||
validateFeedBackMsg(errorMsg);
|
||||
@@ -248,6 +265,9 @@ context(
|
||||
// Will fail if run after 'Able to submit update market proposal and vote for proposal'
|
||||
// 3002-PROP-022
|
||||
it('Unable to submit update market proposal without equity-like share in the market', function () {
|
||||
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
|
||||
cy.get('[data-testid="select-keypair-button"]').eq(0).click(); // switch to second wallet pub key
|
||||
stakingPageAssociateTokens('1');
|
||||
goToMakeNewProposal(governanceProposalType.UPDATE_MARKET);
|
||||
cy.get(newProposalTitle).type('Test update market proposal - rejected');
|
||||
cy.get(newProposalDescription).type('E2E test for proposals');
|
||||
@@ -259,17 +279,25 @@ context(
|
||||
delay: 2,
|
||||
});
|
||||
});
|
||||
cy.get(proposalDownloadBtn).should('be.visible').click();
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-update-market-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath, submit: false });
|
||||
});
|
||||
cy.get(proposalDownloadBtn)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-update-market-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath, submit: false });
|
||||
});
|
||||
});
|
||||
cy.get(newProposalSubmitButton).should('be.visible').click();
|
||||
cy.contains('Proposal rejected', proposalTimeout).should('be.visible');
|
||||
validateDialogContentMsg('PROPOSAL_ERROR_INSUFFICIENT_EQUITY_LIKE_SHARE');
|
||||
ensureSpecifiedUnstakedTokensAreAssociated('1');
|
||||
closeDialog();
|
||||
ethereumWalletConnect();
|
||||
stakingPageDisassociateAllTokens();
|
||||
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
|
||||
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
|
||||
});
|
||||
|
||||
// 3002-PROP-020
|
||||
@@ -291,13 +319,17 @@ context(
|
||||
delay: 2,
|
||||
});
|
||||
});
|
||||
cy.get(proposalDownloadBtn).should('be.visible').click();
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-update-market-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath, submit: false });
|
||||
});
|
||||
cy.get(proposalDownloadBtn)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-update-market-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath, submit: false });
|
||||
});
|
||||
});
|
||||
cy.get(newProposalSubmitButton).should('be.visible').click();
|
||||
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
|
||||
validateFeedBackMsg(
|
||||
@@ -335,13 +367,17 @@ context(
|
||||
delay: 2,
|
||||
});
|
||||
});
|
||||
cy.get(proposalDownloadBtn).should('be.visible').click();
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-update-market-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath });
|
||||
});
|
||||
cy.get(proposalDownloadBtn)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-update-market-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath });
|
||||
});
|
||||
});
|
||||
navigateTo(navigation.proposals);
|
||||
cy.get('@EnactedMarketId').then((marketId) => {
|
||||
cy.contains(String(marketId).slice(0, 6))
|
||||
@@ -390,14 +426,17 @@ context(
|
||||
cy.get(minVoteDeadline).click();
|
||||
cy.get(minValidationDeadline).click();
|
||||
cy.get(minEnactDeadline).click();
|
||||
|
||||
cy.get(proposalDownloadBtn).should('be.visible').click();
|
||||
cy.wrap(getDownloadedProposalJsonPath('vega-new-asset-proposal-')).then(
|
||||
(filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath, submit: false }); // 3005-PASN-003
|
||||
}
|
||||
);
|
||||
cy.get(proposalDownloadBtn)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-new-asset-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath, submit: false }); // 3005-PASN-003
|
||||
});
|
||||
});
|
||||
cy.get(newProposalSubmitButton).should('be.visible').click();
|
||||
closeDialog();
|
||||
cy.get(newProposalSubmitButton).should('be.visible').click();
|
||||
@@ -433,13 +472,17 @@ context(
|
||||
enterUpdateAssetProposalDetails();
|
||||
cy.get(minVoteDeadline).click();
|
||||
cy.get(minEnactDeadline).click();
|
||||
cy.get(proposalDownloadBtn).should('be.visible').click();
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-update-asset-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath });
|
||||
});
|
||||
cy.get(proposalDownloadBtn)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-update-asset-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath });
|
||||
});
|
||||
});
|
||||
navigateTo(navigation.proposals);
|
||||
cy.get(openProposals).within(() => {
|
||||
cy.get(proposalType)
|
||||
@@ -471,13 +514,17 @@ context(
|
||||
enterUpdateAssetProposalDetails();
|
||||
cy.get(maxVoteDeadline).click();
|
||||
cy.get(maxEnactDeadline).click();
|
||||
cy.get(proposalDownloadBtn).should('be.visible').click();
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-update-asset-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath });
|
||||
});
|
||||
cy.get(proposalDownloadBtn)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-update-asset-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Unable to submit edit asset proposal with missing/invalid fields', function () {
|
||||
@@ -525,6 +572,13 @@ context(
|
||||
});
|
||||
});
|
||||
|
||||
after('Disassociate from second wallet key if present', function () {
|
||||
cy.reload();
|
||||
waitForSpinner();
|
||||
ethereumWalletConnect();
|
||||
dissociateFromSecondWalletKey();
|
||||
});
|
||||
|
||||
function validateDialogContentMsg(expectedMsg: string) {
|
||||
cy.getByTestId('dialog-content')
|
||||
.last()
|
||||
|
||||
@@ -86,11 +86,12 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
|
||||
});
|
||||
|
||||
it('Newly created proposals list - shows title and portion of summary', function () {
|
||||
const proposalPath = '/proposals/new-market-raw.json';
|
||||
const enactmentTimestamp = createTenDigitUnixTimeStampForSpecifiedDays(3);
|
||||
const proposalPath = 'src/fixtures/proposals/new-market-raw.json';
|
||||
const proposalTimestamp = createTenDigitUnixTimeStampForSpecifiedDays(3);
|
||||
submitUniqueRawProposal({
|
||||
proposalBody: proposalPath,
|
||||
enactmentTimestamp: enactmentTimestamp,
|
||||
enactmentTimestamp: proposalTimestamp,
|
||||
closingTimestamp: proposalTimestamp,
|
||||
}); // 3001-VOTE-052
|
||||
// 3001-VOTE-008
|
||||
// 3001-VOTE-034
|
||||
|
||||
@@ -29,13 +29,17 @@ context('rewards - flow', { tags: '@slow' }, function () {
|
||||
turnTelemetryOff();
|
||||
cy.visit('/');
|
||||
waitForSpinner();
|
||||
depositAsset(vegaAssetAddress, '1000', 18);
|
||||
ethereumWalletConnect();
|
||||
cy.connectVegaWallet();
|
||||
depositAsset(vegaAssetAddress, '1000', 18);
|
||||
cy.getByTestId('currency-title', txTimeout).should(
|
||||
'contain.text',
|
||||
'Collateral'
|
||||
);
|
||||
vegaWalletTeardown();
|
||||
cy.associateTokensToVegaWallet('6000');
|
||||
cy.VegaWalletTopUpRewardsPool(30, 200);
|
||||
navigateTo(navigation.validators);
|
||||
cy.VegaWalletTopUpRewardsPool();
|
||||
cy.get(vegaWalletUnstakedBalance, txTimeout).should(
|
||||
'contain',
|
||||
'6,000.0',
|
||||
|
||||
@@ -109,6 +109,7 @@ context(
|
||||
cy.getByTestId(userStake, epochTimeout)
|
||||
.first()
|
||||
.should('have.text', '2.00');
|
||||
waitForBeginningOfEpoch();
|
||||
cy.getByTestId('total-stake').first().realHover();
|
||||
cy.getByTestId('staked-by-user-tooltip')
|
||||
.first()
|
||||
@@ -379,6 +380,7 @@ context(
|
||||
});
|
||||
|
||||
it('Disassociating some tokens - prioritizes unstaked tokens', function () {
|
||||
vegaWalletSetSpecifiedApprovalAmount('1000');
|
||||
stakingPageAssociateTokens('3');
|
||||
verifyUnstakedBalance(3.0);
|
||||
cy.get('button').contains('Select a validator to nominate').click();
|
||||
@@ -485,6 +487,7 @@ context(
|
||||
});
|
||||
|
||||
afterEach('Teardown Wallet', function () {
|
||||
navigateTo(navigation.home);
|
||||
vegaWalletTeardown();
|
||||
});
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -39,7 +38,7 @@ const associatedKey = '[data-testid="associated-key"]';
|
||||
const associatedAmount = '[data-testid="associated-amount"]';
|
||||
const associateCompleteText = '[data-testid="transaction-complete-body"]';
|
||||
const disassociationWarning = '[data-testid="disassociation-warning"]';
|
||||
const vegaWallet = '[data-testid="vega-wallet"]';
|
||||
const vegaWallet = 'aside [data-testid="vega-wallet"]';
|
||||
|
||||
context(
|
||||
'Token association flow - with eth and vega wallets connected',
|
||||
@@ -79,27 +78,15 @@ context(
|
||||
//0005-ETXN-003
|
||||
//0005-ETXN-005
|
||||
stakingPageAssociateTokens('2', { skipConfirmation: true });
|
||||
|
||||
cy.get(currencyTitle, txTimeout).should('have.length.above', 4);
|
||||
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);
|
||||
|
||||
// 0005-ETXN-002
|
||||
verifyEthWalletAssociatedBalance('2.0');
|
||||
|
||||
verifyEthWalletTotalAssociatedBalance('2.0');
|
||||
|
||||
cy.get(vegaWallet)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||
'contain',
|
||||
2.0
|
||||
);
|
||||
});
|
||||
|
||||
cy.get(vegaWallet).within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0);
|
||||
});
|
||||
cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', 2.0);
|
||||
});
|
||||
|
||||
@@ -114,12 +101,11 @@ context(
|
||||
verifyEthWalletAssociatedBalance('2.0');
|
||||
verifyEthWalletTotalAssociatedBalance('6,002.00');
|
||||
cy.get('button').contains('Select a validator to nominate').click();
|
||||
cy.getByTestId('epoch-countdown').should('be.visible');
|
||||
stakingPageDisassociateTokens('2');
|
||||
cy.get(currencyTitle, txTimeout).should('have.length.above', 4);
|
||||
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.get(
|
||||
'[data-testid="eth-wallet-associated-balances"]:visible',
|
||||
txTimeout
|
||||
@@ -132,38 +118,26 @@ context(
|
||||
stakingPageAssociateTokens('1001', { approve: true });
|
||||
verifyEthWalletAssociatedBalance('1,001.00');
|
||||
verifyEthWalletTotalAssociatedBalance('7,001.00');
|
||||
cy.get(vegaWallet)
|
||||
.last()
|
||||
.within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||
'contain',
|
||||
'1,001.00'
|
||||
);
|
||||
});
|
||||
cy.get(vegaWallet).within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||
'contain',
|
||||
'1,001.00'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('Able to disassociate a partial amount of tokens currently associated', function () {
|
||||
stakingPageAssociateTokens('2');
|
||||
cy.get(vegaWallet)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||
'contain',
|
||||
2.0
|
||||
);
|
||||
});
|
||||
|
||||
cy.get(vegaWallet).within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0);
|
||||
});
|
||||
cy.get('button').contains('Select a validator to nominate').click();
|
||||
cy.getByTestId('epoch-countdown').should('be.visible');
|
||||
stakingPageDisassociateTokens('1');
|
||||
verifyEthWalletAssociatedBalance('1.0');
|
||||
cy.get(vegaWallet)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||
'contain',
|
||||
1.0
|
||||
);
|
||||
});
|
||||
cy.get(vegaWallet).within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 1.0);
|
||||
});
|
||||
});
|
||||
|
||||
it('Able to disassociate all tokens - using max', function () {
|
||||
@@ -171,15 +145,11 @@ context(
|
||||
const warningText =
|
||||
'Warning: Any tokens that have been nominated to a node will sacrifice rewards they are due for the current epoch. If you do not wish to sacrifice these, you should remove stake from a node at the end of an epoch before disassociation.';
|
||||
stakingPageAssociateTokens('2');
|
||||
cy.get(vegaWallet)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||
'contain',
|
||||
2.0
|
||||
);
|
||||
});
|
||||
cy.get(vegaWallet).within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0);
|
||||
});
|
||||
cy.get('button').contains('Select a validator to nominate').click();
|
||||
cy.getByTestId('epoch-countdown').should('be.visible');
|
||||
cy.get(ethWalletDissociateButton).click();
|
||||
cy.get(disassociationWarning).should('contain', warningText);
|
||||
stakingPageDisassociateAllTokens();
|
||||
@@ -197,14 +167,9 @@ context(
|
||||
'not.exist'
|
||||
);
|
||||
});
|
||||
cy.get(vegaWallet)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||
'contain',
|
||||
0.0
|
||||
);
|
||||
});
|
||||
cy.get(vegaWallet).within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 0.0);
|
||||
});
|
||||
});
|
||||
|
||||
it('Able to associate and disassociate vesting contract tokens', function () {
|
||||
@@ -219,32 +184,22 @@ context(
|
||||
type: 'contract',
|
||||
skipConfirmation: true,
|
||||
});
|
||||
|
||||
cy.get(currencyTitle, txTimeout).should('have.length.above', 4);
|
||||
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);
|
||||
verifyEthWalletAssociatedBalance('2.0');
|
||||
verifyEthWalletTotalAssociatedBalance('2.0');
|
||||
cy.get(vegaWallet)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||
'contain',
|
||||
2.0
|
||||
);
|
||||
});
|
||||
cy.get(vegaWallet).within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0);
|
||||
});
|
||||
cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', 2.0);
|
||||
stakingPageDisassociateTokens('1', {
|
||||
type: 'contract',
|
||||
skipConfirmation: true,
|
||||
});
|
||||
cy.get(currencyTitle, txTimeout).should('have.length.above', 4);
|
||||
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);
|
||||
verifyEthWalletAssociatedBalance('1.0');
|
||||
verifyEthWalletTotalAssociatedBalance('1.0');
|
||||
});
|
||||
@@ -256,6 +211,7 @@ context(
|
||||
// 1004-ASSO-022
|
||||
stakingPageAssociateTokens('21', { type: 'wallet' });
|
||||
cy.get('button').contains('Select a validator to nominate').click();
|
||||
cy.getByTestId('epoch-countdown').should('be.visible');
|
||||
stakingPageAssociateTokens('37', { type: 'contract' });
|
||||
cy.get(vestingContractSection)
|
||||
.first()
|
||||
@@ -275,28 +231,18 @@ context(
|
||||
);
|
||||
cy.get(associatedAmount, txTimeout).should('contain', 21);
|
||||
});
|
||||
cy.get(vegaWallet)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||
'contain',
|
||||
58
|
||||
);
|
||||
});
|
||||
cy.get(vegaWallet).within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 58);
|
||||
});
|
||||
stakingPageDisassociateTokens('6', { type: 'contract' });
|
||||
cy.get(vestingContractSection)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(associatedAmount, txTimeout).should('contain', 31);
|
||||
});
|
||||
cy.get(vegaWallet)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||
'contain',
|
||||
52
|
||||
);
|
||||
});
|
||||
cy.get(vegaWallet).within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 52);
|
||||
});
|
||||
navigateTo(navigation.validators);
|
||||
stakingPageDisassociateTokens('9', { type: 'wallet' });
|
||||
cy.get(vegaInWalletSection)
|
||||
@@ -304,14 +250,9 @@ context(
|
||||
.within(() => {
|
||||
cy.get(associatedAmount, txTimeout).should('contain', 12);
|
||||
});
|
||||
cy.get(vegaWallet)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||
'contain',
|
||||
43
|
||||
);
|
||||
});
|
||||
cy.get(vegaWallet).within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 43);
|
||||
});
|
||||
});
|
||||
|
||||
it('Not able to associate more tokens than owned', function () {
|
||||
@@ -328,11 +269,9 @@ context(
|
||||
// 1004-ASSO-004
|
||||
it('Pending association outside of app is shown', function () {
|
||||
vegaWalletAssociate('2');
|
||||
cy.get(currencyTitle, txTimeout).should('have.length.above', 4);
|
||||
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);
|
||||
validateWalletCurrency('Associated', '2.00');
|
||||
});
|
||||
|
||||
@@ -341,11 +280,9 @@ context(
|
||||
cy.wrap(validateWalletCurrency('Associated', '2.00')).then(() => {
|
||||
vegaWalletDisassociate('2');
|
||||
});
|
||||
cy.get(currencyTitle, txTimeout).should('have.length.above', 4);
|
||||
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);
|
||||
validateWalletCurrency('Associated', '0.00');
|
||||
});
|
||||
|
||||
@@ -364,14 +301,9 @@ context(
|
||||
Cypress.env('vegaWalletPublicKey2')
|
||||
);
|
||||
stakingPageAssociateTokens('2');
|
||||
cy.get(vegaWallet)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||
'contain',
|
||||
2.0
|
||||
);
|
||||
});
|
||||
cy.get(vegaWallet).within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0);
|
||||
});
|
||||
cy.get(associateCompleteText).should(
|
||||
'have.text',
|
||||
`Vega key ${Cypress.env(
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { stakingPageDisassociateAllTokens } from './staking.functions';
|
||||
|
||||
const tokenDropDown = 'state-trigger';
|
||||
const txTimeout = Cypress.env('txTimeout');
|
||||
|
||||
export enum navigation {
|
||||
section = 'nav',
|
||||
home = '[href="/"]',
|
||||
vesting = '[href="/token/redeem"]',
|
||||
validators = '[href="/validators"]',
|
||||
rewards = '[href="/rewards"]',
|
||||
@@ -18,6 +21,7 @@ export function convertTokenValueToNumber(subject: string) {
|
||||
}
|
||||
|
||||
const topLevelRoutes = [
|
||||
navigation.home,
|
||||
navigation.proposals,
|
||||
navigation.validators,
|
||||
navigation.rewards,
|
||||
@@ -97,3 +101,25 @@ export function turnTelemetryOff() {
|
||||
win.localStorage.setItem('vega_telemetry_on', 'false')
|
||||
);
|
||||
}
|
||||
|
||||
export function dissociateFromSecondWalletKey() {
|
||||
const secondWalletKey = Cypress.env('vegaWalletPublicKey2Short');
|
||||
cy.getByTestId('vega-in-wallet')
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.getByTestId('eth-wallet-associated-balances')
|
||||
.last()
|
||||
.within(() => {
|
||||
cy.getByTestId('associated-key')
|
||||
.invoke('text')
|
||||
.as('associatedPubKey');
|
||||
});
|
||||
});
|
||||
cy.get('@associatedPubKey').then((associatedPubKey) => {
|
||||
if (associatedPubKey == secondWalletKey) {
|
||||
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
|
||||
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
|
||||
stakingPageDisassociateAllTokens();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,29 +59,29 @@ export function submitUniqueRawProposal(proposalFields: {
|
||||
submit?: boolean;
|
||||
}) {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
let proposalBodyPath = '/proposals/raw.json';
|
||||
let proposalBodyPath = 'src/fixtures/proposals/raw.json';
|
||||
if (proposalFields.proposalBody) {
|
||||
proposalBodyPath = proposalFields.proposalBody;
|
||||
}
|
||||
cy.readFile(proposalBodyPath).then((rawProposal) => {
|
||||
if (!proposalFields.proposalBody) {
|
||||
if (proposalFields.proposalTitle) {
|
||||
rawProposal.rationale.title = proposalFields.proposalTitle;
|
||||
cy.wrap(proposalFields.proposalTitle).as('proposalTitle');
|
||||
}
|
||||
if (proposalFields.proposalDescription) {
|
||||
rawProposal.rationale.description = proposalFields.proposalDescription;
|
||||
}
|
||||
if (proposalFields.closingTimestamp) {
|
||||
rawProposal.terms.closingTimestamp = proposalFields.closingTimestamp;
|
||||
} else {
|
||||
const minTimeStamp = createTenDigitUnixTimeStampForSpecifiedDays(2);
|
||||
rawProposal.terms.closingTimestamp = minTimeStamp;
|
||||
}
|
||||
if (proposalFields.enactmentTimestamp) {
|
||||
rawProposal.terms.enactmentTimestamp =
|
||||
proposalFields.enactmentTimestamp;
|
||||
}
|
||||
if (proposalFields.proposalTitle) {
|
||||
rawProposal.rationale.title = proposalFields.proposalTitle;
|
||||
cy.wrap(proposalFields.proposalTitle).as('proposalTitle');
|
||||
}
|
||||
if (proposalFields.proposalDescription) {
|
||||
rawProposal.rationale.description = proposalFields.proposalDescription;
|
||||
}
|
||||
if (proposalFields.closingTimestamp) {
|
||||
rawProposal.terms.closingTimestamp = proposalFields.closingTimestamp;
|
||||
} else if (
|
||||
!proposalFields.closingTimestamp &&
|
||||
!proposalFields.proposalBody
|
||||
) {
|
||||
const minTimeStamp = createTenDigitUnixTimeStampForSpecifiedDays(2);
|
||||
rawProposal.terms.closingTimestamp = minTimeStamp;
|
||||
}
|
||||
if (proposalFields.enactmentTimestamp) {
|
||||
rawProposal.terms.enactmentTimestamp = proposalFields.enactmentTimestamp;
|
||||
}
|
||||
|
||||
const proposalPayload = JSON.stringify(rawProposal);
|
||||
|
||||
@@ -236,8 +236,8 @@ export function validateWalletCurrency(
|
||||
currencyTitle: string,
|
||||
expectedAmount: string
|
||||
) {
|
||||
cy.get("[data-testid='currency-title']")
|
||||
.contains(currencyTitle)
|
||||
cy.get("[data-testid='currency-title']", txTimeout)
|
||||
.contains(currencyTitle, txTimeout)
|
||||
.parent()
|
||||
.parent()
|
||||
.within(() => {
|
||||
|
||||
@@ -19,7 +19,7 @@ const ethStakingBridgeContractAddress = Cypress.env(
|
||||
);
|
||||
const ethProviderUrl = Cypress.env('ethProviderUrl');
|
||||
const getAccount = (number = 0) => `m/44'/60'/0'/0/${number}`;
|
||||
const transactionTimeout = 100000;
|
||||
const transactionTimeout = { timeout: 100000, log: false };
|
||||
const Erc20BridgeAddress = '0x9708FF7510D4A7B9541e1699d15b53Ecb1AFDc54';
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider({ url: ethProviderUrl });
|
||||
@@ -43,10 +43,7 @@ export async function depositAsset(
|
||||
const faucet = new Token(assetEthAddress, signer);
|
||||
cy.wrap(
|
||||
faucet.approve(Erc20BridgeAddress, amount + '0'.repeat(decimalPlaces + 1)),
|
||||
{
|
||||
timeout: transactionTimeout,
|
||||
log: false,
|
||||
}
|
||||
transactionTimeout
|
||||
).then(() => {
|
||||
const collateralBridge = new CollateralBridge(Erc20BridgeAddress, signer);
|
||||
cy.wrap(
|
||||
@@ -55,7 +52,7 @@ export async function depositAsset(
|
||||
amount + '0'.repeat(decimalPlaces),
|
||||
'0x' + vegaWalletPubKey
|
||||
),
|
||||
{ timeout: transactionTimeout, log: false }
|
||||
transactionTimeout
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -79,13 +76,13 @@ export async function vegaWalletTeardown() {
|
||||
}
|
||||
});
|
||||
cy.get(vegaWalletContainer).within(() => {
|
||||
cy.get(associatedAmountInWallet, {
|
||||
timeout: transactionTimeout,
|
||||
})
|
||||
.should('have.length', 1, { timeout: transactionTimeout })
|
||||
.contains('0.00', {
|
||||
timeout: transactionTimeout,
|
||||
});
|
||||
cy.get(associatedAmountInWallet, transactionTimeout).should(
|
||||
'have.length',
|
||||
1
|
||||
);
|
||||
cy.get(associatedAmountInWallet)
|
||||
.first(transactionTimeout)
|
||||
.should('have.text', '0.00');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -109,7 +106,7 @@ async function vegaWalletTeardownStaking(stakingBridgeContract: StakingBridge) {
|
||||
cy.highlight('Tearing down staking tokens from vega wallet if present');
|
||||
cy.wrap(
|
||||
stakingBridgeContract.stake_balance(ethWalletPubKey, vegaWalletPubKey),
|
||||
{ timeout: transactionTimeout }
|
||||
transactionTimeout
|
||||
).then((stakeBalance) => {
|
||||
if (Number(stakeBalance) != 0) {
|
||||
cy.get(vegaWalletContainer).within(() => {
|
||||
@@ -122,31 +119,25 @@ async function vegaWalletTeardownStaking(stakingBridgeContract: StakingBridge) {
|
||||
String(stakeBalance),
|
||||
vegaWalletPubKey
|
||||
),
|
||||
{ timeout: transactionTimeout }
|
||||
transactionTimeout
|
||||
);
|
||||
cy.wrap(
|
||||
vestingContract.stake_balance(ethWalletPubKey, vegaWalletPubKey),
|
||||
{
|
||||
timeout: transactionTimeout,
|
||||
log: false,
|
||||
}
|
||||
transactionTimeout
|
||||
).then((vestingAmount) => {
|
||||
if (Number(vestingAmount) != 0) {
|
||||
cy.contains('Associated', {
|
||||
timeout: transactionTimeout,
|
||||
})
|
||||
cy.contains('Associated', transactionTimeout)
|
||||
.parent()
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.getByTestId('currency-value', {
|
||||
timeout: transactionTimeout,
|
||||
})
|
||||
.should('have.length', 1)
|
||||
cy.getByTestId('currency-value', transactionTimeout)
|
||||
.first()
|
||||
.invoke('text')
|
||||
.as('displayedAmount');
|
||||
cy.get('@displayedAmount', {
|
||||
timeout: transactionTimeout,
|
||||
}).should('not.eq', $associatedAmount);
|
||||
cy.get('@displayedAmount', transactionTimeout).should(
|
||||
'not.eq',
|
||||
$associatedAmount
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -158,14 +149,14 @@ async function vegaWalletTeardownStaking(stakingBridgeContract: StakingBridge) {
|
||||
|
||||
async function vegaWalletTeardownVesting(vestingContract: TokenVesting) {
|
||||
cy.highlight('Tearing down vesting tokens from vega wallet if present');
|
||||
cy.wrap(vestingContract.stake_balance(ethWalletPubKey, vegaWalletPubKey), {
|
||||
timeout: transactionTimeout,
|
||||
log: false,
|
||||
}).then((vestingAmount) => {
|
||||
cy.wrap(
|
||||
vestingContract.stake_balance(ethWalletPubKey, vegaWalletPubKey),
|
||||
transactionTimeout
|
||||
).then((vestingAmount) => {
|
||||
if (Number(vestingAmount) != 0) {
|
||||
cy.wrap(
|
||||
vestingContract.remove_stake(String(vestingAmount), vegaWalletPubKey),
|
||||
{ timeout: transactionTimeout }
|
||||
transactionTimeout
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ NX_VEGA_NETWORKS='{"DEVNET":"https://dev.governance.vega.xyz","TESTNET":"https:/
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
|
||||
NX_VEGA_EXPLORER_URL=#
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-devnet1-k8s.ops.vega.xyz
|
||||
|
||||
@@ -7,7 +7,7 @@ NX_VEGA_NETWORKS='{"DEVNET":"https://dev.governance.vega.xyz","TESTNET":"https:/
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.validators-testnet.vega.rocks/
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_REST_URL=https://api-validators-testnet.vega.rocks/api/v2/
|
||||
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './multisig-incorrect-notice';
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { MultisigIncorrectNotice } from './multisig-incorrect-notice';
|
||||
|
||||
jest.mock('@vegaprotocol/web3', () => ({
|
||||
useEthereumConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
useEnvironment: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('MultisigIncorrectNotice', () => {
|
||||
it('renders correctly when config is provided', () => {
|
||||
(useEthereumConfig as jest.Mock).mockReturnValue({
|
||||
config: {
|
||||
multisig_control_contract: {
|
||||
address: '0x1234',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
(useEnvironment as unknown as jest.Mock).mockReturnValue({
|
||||
ETHERSCAN_URL: 'https://etherscan.io',
|
||||
});
|
||||
|
||||
render(<MultisigIncorrectNotice />);
|
||||
|
||||
expect(screen.getByTestId('multisig-contract-link')).toHaveAttribute(
|
||||
'href',
|
||||
'https://etherscan.io/address/0x1234'
|
||||
);
|
||||
expect(screen.getByTestId('multisig-contract-link')).toHaveAttribute(
|
||||
'title',
|
||||
'0x1234'
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('multisig-validators-learn-more')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render when config is not provided', () => {
|
||||
(useEthereumConfig as jest.Mock).mockReturnValue({
|
||||
config: null,
|
||||
});
|
||||
|
||||
const { container } = render(<MultisigIncorrectNotice />);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Callout, Intent, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
|
||||
import type { EthereumConfig } from '@vegaprotocol/web3';
|
||||
|
||||
export const MultisigIncorrectNotice = () => {
|
||||
const { t } = useTranslation();
|
||||
const { config } = useEthereumConfig();
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const contract = config[
|
||||
'multisig_control_contract' as keyof EthereumConfig
|
||||
] as {
|
||||
address: string;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-10">
|
||||
<Callout intent={Intent.Warning}>
|
||||
<div>
|
||||
<Link
|
||||
title={contract.address}
|
||||
href={`${ETHERSCAN_URL}/address/${contract.address}`}
|
||||
target="_blank"
|
||||
data-testid="multisig-contract-link"
|
||||
>
|
||||
{t('multisigContractLink')}
|
||||
</Link>{' '}
|
||||
{t('multisigContractIncorrect')}
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<Link
|
||||
href={DocsLinks?.VALIDATOR_SCORES_REWARDS}
|
||||
target="_blank"
|
||||
data-testid="multisig-validators-learn-more"
|
||||
>
|
||||
{t('learnMore')}
|
||||
</Link>
|
||||
</div>
|
||||
</Callout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -116,7 +116,7 @@
|
||||
"Showing tranches with <{{trancheMinimum}} VEGA, click to hide these tranches": "Showing tranches with ≤{{trancheMinimum}} $VEGA, click to hide these tranches",
|
||||
"Not showing tranches with <{{trancheMinimum}} VEGA, click to show all tranches": "Not showing tranches with ≤{{trancheMinimum}} $VEGA, click to show all tranches",
|
||||
"the holder": "the holder",
|
||||
"We couldn't seem to load your data.": "We couldn't seem to load your data.",
|
||||
"Your data couldn't be loaded": "Your data couldn't be loaded",
|
||||
"Vesting VEGA": "Vesting VEGA",
|
||||
"All the tokens in this tranche are locked and can not be redeemed yet.": "All the tokens in this tranche are locked and can not be redeemed yet.",
|
||||
"Redeem unlocked VEGA from tranche {{id}}": "Redeem unlocked $VEGA from tranche {{id}}",
|
||||
@@ -728,7 +728,7 @@
|
||||
"ThisWillSetEnactmentDeadlineTo": "This will set the enactment date to",
|
||||
"ThisWillSetValidationDeadlineTo": "This will set the validation deadline to",
|
||||
"Hours": "hours",
|
||||
"ThisWillAdd2MinutesToAllowTimeToConfirmInWallet": "Note: we add 2 minutes of extra time when you choose the minimum value. This gives you time to confirm the proposal in your wallet.",
|
||||
"ThisWillAdd2MinutesToAllowTimeToConfirmInWallet": "Note: 2 minutes of extra time are added when you choose the minimum value. This gives you time to confirm the proposal in your wallet.",
|
||||
"ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline": "Proposal will fail if enactment is earlier than the voting deadline",
|
||||
"SelectAMarketToChange": "Select a market to change",
|
||||
"MarketName": "Market name",
|
||||
@@ -821,5 +821,8 @@
|
||||
"disclaimer2": "The Vega Governance App is free, public and open source software. Software upgrades may contain bugs or security vulnerabilities that might result in loss of functionality.",
|
||||
"disclaimer3": "The Vega Governance App uses data obtained from nodes on the Vega Blockchain. The developers of the Vega Governance App do not operate or run the Vega Blockchain or any other blockchain.",
|
||||
"disclaimer4": "The Vega Governance App is provided “as is”. The developers of the Vega Governance App make no representations or warranties of any kind, whether express or implied, statutory or otherwise regarding the Vega Governance App. They disclaim all warranties of merchantability, quality, fitness for purpose. They disclaim all warranties that the Vega Governance App is free of harmful components or errors.",
|
||||
"disclaimer5": "No developer of the Vega Governance App accepts any responsibility for, or liability to users in connection with their use of the Vega Governance App."
|
||||
"disclaimer5": "No developer of the Vega Governance App accepts any responsibility for, or liability to users in connection with their use of the Vega Governance App.",
|
||||
"multisigContractLink": "Ethereum Multisig Contract",
|
||||
"multisigContractIncorrect": "is incorrectly configured. Validator and delegator rewards will be penalised until this is resolved.",
|
||||
"learnMore": "Learn more"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
getMultisigStatusInfo,
|
||||
MultisigStatus,
|
||||
} from './get-multisig-status-info';
|
||||
import type { PreviousEpochQuery } from '../routes/staking/__generated__/PreviousEpoch';
|
||||
|
||||
const createNode = (id: string, multisigScore: string) => ({
|
||||
node: {
|
||||
id,
|
||||
stakedTotal: '1000',
|
||||
rewardScore: { multisigScore },
|
||||
},
|
||||
});
|
||||
|
||||
describe('getMultisigStatus', () => {
|
||||
it('should return MultisigStatus.noNodes when no nodes are present', () => {
|
||||
const result = getMultisigStatusInfo({
|
||||
epoch: { id: '1', validatorsConnection: { edges: [] } },
|
||||
} as PreviousEpochQuery);
|
||||
expect(result).toEqual({
|
||||
multisigStatus: MultisigStatus.noNodes,
|
||||
showMultisigStatusError: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return MultisigStatus.correct when all nodes have multisigScore of 1', () => {
|
||||
const result = getMultisigStatusInfo({
|
||||
epoch: {
|
||||
id: '1',
|
||||
validatorsConnection: {
|
||||
edges: [createNode('1', '1'), createNode('2', '1')],
|
||||
},
|
||||
},
|
||||
} as PreviousEpochQuery);
|
||||
expect(result).toEqual({
|
||||
multisigStatus: MultisigStatus.correct,
|
||||
showMultisigStatusError: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return MultisigStatus.nodeNeedsRemoving when all nodes have multisigScore of 0', () => {
|
||||
const result = getMultisigStatusInfo({
|
||||
epoch: {
|
||||
id: '1',
|
||||
validatorsConnection: {
|
||||
edges: [createNode('1', '0'), createNode('2', '0')],
|
||||
},
|
||||
},
|
||||
} as PreviousEpochQuery);
|
||||
expect(result).toEqual({
|
||||
multisigStatus: MultisigStatus.nodeNeedsRemoving,
|
||||
showMultisigStatusError: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return MultisigStatus.nodeNeedsAdding when some nodes have multisigScore of 0 and others have 1', () => {
|
||||
const result = getMultisigStatusInfo({
|
||||
epoch: {
|
||||
id: '1',
|
||||
validatorsConnection: {
|
||||
edges: [createNode('1', '0'), createNode('2', '1')],
|
||||
},
|
||||
},
|
||||
} as PreviousEpochQuery);
|
||||
expect(result).toEqual({
|
||||
multisigStatus: MultisigStatus.nodeNeedsAdding,
|
||||
showMultisigStatusError: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import type { PreviousEpochQuery } from '../routes/staking/__generated__/PreviousEpoch';
|
||||
|
||||
export enum MultisigStatus {
|
||||
'correct' = 'correct',
|
||||
'nodeNeedsAdding' = 'nodeNeedsAdding',
|
||||
'nodeNeedsRemoving' = 'nodeNeedsRemoving ',
|
||||
'noNodes' = 'noNodes',
|
||||
}
|
||||
|
||||
export const getMultisigStatusInfo = (
|
||||
previousEpochData: PreviousEpochQuery
|
||||
) => {
|
||||
let status = MultisigStatus.noNodes;
|
||||
|
||||
const allNodesInPreviousEpoch = removePaginationWrapper(
|
||||
previousEpochData?.epoch.validatorsConnection?.edges
|
||||
);
|
||||
|
||||
const hasZero = allNodesInPreviousEpoch.some(
|
||||
(node) => Number(node?.rewardScore?.multisigScore) === 0
|
||||
);
|
||||
const hasOne = allNodesInPreviousEpoch.some(
|
||||
(node) => Number(node?.rewardScore?.multisigScore) === 1
|
||||
);
|
||||
|
||||
if (hasZero && hasOne) {
|
||||
// If any individual node has 0 it means that node is missing from the multisig and needs to be added
|
||||
status = MultisigStatus.nodeNeedsAdding;
|
||||
} else if (hasZero) {
|
||||
// If all nodes have 0 it means there is an incorrect address in the multisig that needs to be removed
|
||||
status = MultisigStatus.nodeNeedsRemoving;
|
||||
} else if (allNodesInPreviousEpoch.length > 0) {
|
||||
// If all nodes have 1 it means the multisig is correct
|
||||
status = MultisigStatus.correct;
|
||||
}
|
||||
|
||||
return {
|
||||
showMultisigStatusError: status !== MultisigStatus.correct,
|
||||
multisigStatus: status,
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -146,7 +146,7 @@ describe('Proposal form vote, validation and enactment deadline', () => {
|
||||
it('should show the correct datetimes', () => {
|
||||
renderComponent();
|
||||
// Should be adding 2 mins to the vote deadline as the minimum is set by
|
||||
// default, and we add 2 mins for wallet confirmation
|
||||
// default, and 2 mins are added for wallet confirmation
|
||||
expect(screen.getByTestId('voting-date')).toHaveTextContent(
|
||||
'2022-01-01T01:02:00.000Z'
|
||||
);
|
||||
|
||||
@@ -23,6 +23,9 @@ import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { ConnectToSeeRewards } from '../connect-to-see-rewards';
|
||||
import { EpochTotalRewards } from '../epoch-total-rewards/epoch-total-rewards';
|
||||
import { usePreviousEpochQuery } from '../../staking/__generated__/PreviousEpoch';
|
||||
import { getMultisigStatusInfo } from '../../../lib/get-multisig-status-info';
|
||||
import { MultisigIncorrectNotice } from '../../../components/multisig-incorrect-notice';
|
||||
|
||||
type RewardsView = 'total' | 'individual';
|
||||
|
||||
@@ -41,12 +44,25 @@ export const RewardsPage = () => {
|
||||
|
||||
useRefreshAfterEpoch(epochData?.epoch.timestamps.expiry, refetch);
|
||||
|
||||
const { data: previousEpochData } = usePreviousEpochQuery({
|
||||
variables: {
|
||||
epochId: (Number(epochData?.epoch.id) - 1).toString(),
|
||||
},
|
||||
skip: !epochData?.epoch.id,
|
||||
});
|
||||
|
||||
const multisigStatus = previousEpochData
|
||||
? getMultisigStatusInfo(previousEpochData)
|
||||
: undefined;
|
||||
|
||||
const {
|
||||
params,
|
||||
loading: paramsLoading,
|
||||
error: paramsError,
|
||||
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
|
||||
|
||||
console.log('params', params);
|
||||
|
||||
const payoutDuration = useMemo(() => {
|
||||
if (!params) {
|
||||
return 0;
|
||||
@@ -78,14 +94,18 @@ export const RewardsPage = () => {
|
||||
)}
|
||||
</p>
|
||||
|
||||
{payoutDuration ? (
|
||||
{multisigStatus?.showMultisigStatusError ? (
|
||||
<MultisigIncorrectNotice />
|
||||
) : null}
|
||||
|
||||
{!multisigStatus?.showMultisigStatusError && payoutDuration ? (
|
||||
<div className="my-8">
|
||||
<Callout
|
||||
title={t('rewardsCallout', {
|
||||
duration: formatDistance(new Date(0), payoutDuration),
|
||||
})}
|
||||
headingLevel={3}
|
||||
intent={Intent.Warning}
|
||||
intent={Intent.Primary}
|
||||
>
|
||||
<p className="mb-0">{t('rewardsCalloutDetail')}</p>
|
||||
</Callout>
|
||||
|
||||
@@ -7,6 +7,8 @@ import { ValidatorTables } from './validator-tables';
|
||||
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { ENV } from '../../../config';
|
||||
import { getMultisigStatusInfo } from '../../../lib/get-multisig-status-info';
|
||||
import { MultisigIncorrectNotice } from '../../../components/multisig-incorrect-notice';
|
||||
|
||||
export const EpochData = () => {
|
||||
// errorPolicy due to vegaprotocol/vega issue 5898
|
||||
@@ -46,12 +48,20 @@ export const EpochData = () => {
|
||||
userStakingRefetch();
|
||||
});
|
||||
|
||||
const multisigStatus = previousEpochData
|
||||
? getMultisigStatusInfo(previousEpochData)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={nodesLoading || userStakingLoading}
|
||||
error={nodesError || userStakingError}
|
||||
data={nodesData}
|
||||
>
|
||||
{multisigStatus?.showMultisigStatusError ? (
|
||||
<MultisigIncorrectNotice />
|
||||
) : null}
|
||||
|
||||
{nodesData?.epoch &&
|
||||
nodesData.epoch.timestamps.start &&
|
||||
nodesData?.epoch.timestamps.expiry && (
|
||||
|
||||
@@ -5,4 +5,4 @@ NX_VEGA_ENV=DEVNET
|
||||
NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
|
||||
NX_VEGA_EXPLORER_URL=#
|
||||
|
||||
@@ -3,3 +3,6 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/maste
|
||||
NX_VEGA_URL=https://api.vega.community/graphql
|
||||
NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}'
|
||||
NX_VEGA_ENV=MAINNET
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
|
||||
it('market volume displayed', () => {
|
||||
cy.getByTestId(marketTitle).contains('Market volume').click();
|
||||
validateMarketDataRow(0, '24 Hour Volume', 'Unknown');
|
||||
validateMarketDataRow(1, 'Open Interest', '-');
|
||||
validateMarketDataRow(2, 'Best Bid Volume', '1');
|
||||
validateMarketDataRow(3, 'Best Offer Volume', '3');
|
||||
|
||||
@@ -40,26 +40,26 @@ describe('markets selector', { tags: '@smoke' }, () => {
|
||||
{
|
||||
code: 'SOLUSD',
|
||||
markPrice: '84.41XYZalpha',
|
||||
change: '+200.00%',
|
||||
vol: '324h vol',
|
||||
change: '',
|
||||
vol: '0.0024h vol',
|
||||
},
|
||||
{
|
||||
code: 'ETHBTC.QM21',
|
||||
markPrice: '46,126.90058tBTC',
|
||||
change: '+200.00%',
|
||||
vol: '324h vol',
|
||||
change: '',
|
||||
vol: '0.0024h vol',
|
||||
},
|
||||
{
|
||||
code: 'BTCUSD.MF21',
|
||||
markPrice: '46,126.90058tDAI',
|
||||
change: '+200.00%',
|
||||
vol: '324h vol',
|
||||
change: '',
|
||||
vol: '0.0024h vol',
|
||||
},
|
||||
{
|
||||
code: 'AAPL.MF21',
|
||||
markPrice: '46,126.90058tUSDC',
|
||||
change: '+200.00%',
|
||||
vol: '324h vol',
|
||||
change: '',
|
||||
vol: '0.0024h vol',
|
||||
},
|
||||
];
|
||||
cy.getByTestId(list)
|
||||
@@ -80,7 +80,7 @@ describe('markets selector', { tags: '@smoke' }, () => {
|
||||
market.change
|
||||
);
|
||||
// 6001-MARK-025
|
||||
expect(item.find('[data-testid="sparkline-svg"]')).to.exist;
|
||||
expect(item.find('[data-testid="sparkline-svg"]')).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -88,7 +88,8 @@ describe(
|
||||
.pop()
|
||||
?.toLowerCase()} and not accepting orders`
|
||||
);
|
||||
cy.getByTestId('place-order').should('be.disabled');
|
||||
// 7002-SORD-060
|
||||
cy.getByTestId('place-order').should('be.enabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,7 +77,8 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
it('must warn if order size input has too many digits after the decimal place', function () {
|
||||
// 7002-SORD-016
|
||||
cy.getByTestId(orderSizeField).clear().type('1.234');
|
||||
cy.getByTestId(placeOrderBtn).should('be.disabled');
|
||||
// 7002-SORD-060
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId('dealticket-error-message-size-market').should(
|
||||
'have.text',
|
||||
'Size must be whole numbers for this market'
|
||||
@@ -86,12 +87,13 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
|
||||
it('must warn if order size is set to 0', function () {
|
||||
cy.getByTestId(orderSizeField).clear().type('0');
|
||||
cy.getByTestId(placeOrderBtn).should('be.disabled');
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId('dealticket-error-message-size-market').should(
|
||||
'have.text',
|
||||
'Size cannot be lower than 1'
|
||||
);
|
||||
});
|
||||
|
||||
it('must have total margin available', () => {
|
||||
// 7001-COLL-011
|
||||
cy.getByTestId('tab-ticket')
|
||||
@@ -100,9 +102,8 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
.within(() => {
|
||||
cy.get('[data-state="closed"]').should(
|
||||
'have.text',
|
||||
'Total margin available'
|
||||
'Total margin available100,000.01 tDAI'
|
||||
);
|
||||
cy.get('.text-neutral-500').should('have.text', '100,000.01 tDAI');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,13 +23,14 @@ describe(
|
||||
});
|
||||
|
||||
it('should show an error if your balance is zero', () => {
|
||||
cy.getByTestId('place-order').should('be.disabled');
|
||||
// 7002-SORD-060
|
||||
cy.getByTestId('place-order').should('be.enabled');
|
||||
// 7002-SORD-003
|
||||
cy.getByTestId('dealticket-error-message-zero-balance').should(
|
||||
'have.text',
|
||||
'You need ' +
|
||||
'tDAI' +
|
||||
' in your wallet to trade in this market.See all your collateral.Make a deposit'
|
||||
' in your wallet to trade in this market. See all your collateral.Make a deposit'
|
||||
);
|
||||
cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist');
|
||||
});
|
||||
|
||||
@@ -34,9 +34,9 @@ describe('suspended market validation', { tags: '@regression' }, () => {
|
||||
|
||||
it('should show warning for market order', function () {
|
||||
cy.getByTestId(toggleMarket).click();
|
||||
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
|
||||
// 7002-SORD-060
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
cy.getByTestId(placeOrderBtn).should('be.disabled');
|
||||
cy.getByTestId('dealticket-error-message-type').should(
|
||||
'have.text',
|
||||
'This market is in auction until it reaches sufficient liquidity. Only limit orders are permitted when market is in auction'
|
||||
@@ -59,7 +59,7 @@ describe('suspended market validation', { tags: '@regression' }, () => {
|
||||
cy.getByTestId(orderTIFDropDown).select(
|
||||
TIFlist.filter((item) => item.code === 'FOK')[0].value
|
||||
);
|
||||
cy.getByTestId(placeOrderBtn).should('be.disabled');
|
||||
cy.getByTestId(placeOrderBtn).should('be.enabled');
|
||||
cy.getByTestId('dealticket-error-message-tif').should(
|
||||
'have.text',
|
||||
'This market is in auction until it reaches sufficient liquidity. Until the auction ends, you can only place GFA, GTT, or GTC limit orders'
|
||||
|
||||
@@ -9,7 +9,7 @@ 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_TOKEN_URL=https://governance.fairground.wtf
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_VEGA_DOCS_URL=#
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
|
||||
@@ -6,25 +6,32 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type {
|
||||
MarketCandlesQuery,
|
||||
MarketCandlesQueryVariables,
|
||||
MarketDataUpdateFieldsFragment,
|
||||
MarketDataUpdateSubscription,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { MarketCandlesDocument } from '@vegaprotocol/markets';
|
||||
import { MarketDataUpdateDocument } from '@vegaprotocol/markets';
|
||||
import {
|
||||
AuctionTrigger,
|
||||
Interval,
|
||||
MarketState,
|
||||
MarketTradingMode,
|
||||
} from '@vegaprotocol/types';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { subDays } from 'date-fns';
|
||||
|
||||
describe('MarketSelectorItem', () => {
|
||||
const yesterday = new Date();
|
||||
yesterday.setHours(yesterday.getHours() - 20);
|
||||
const market = createMarketFragment({
|
||||
id: 'market-0',
|
||||
decimalPlaces: 2,
|
||||
// @ts-ignore fragment doesn't contain candles
|
||||
candles: [
|
||||
{ close: '5', volume: '50' },
|
||||
{ close: '10', volume: '50' },
|
||||
{ close: '5', volume: '50', periodStart: yesterday.toISOString() },
|
||||
{ close: '10', volume: '50', periodStart: yesterday.toISOString() },
|
||||
],
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
@@ -36,6 +43,7 @@ describe('MarketSelectorItem', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const marketData: MarketDataUpdateFieldsFragment = {
|
||||
__typename: 'ObservableMarketData',
|
||||
marketId: market.id,
|
||||
@@ -63,26 +71,32 @@ describe('MarketSelectorItem', () => {
|
||||
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
|
||||
priceMonitoringBounds: null,
|
||||
};
|
||||
const mock: MockedResponse<MarketDataUpdateSubscription> = {
|
||||
request: {
|
||||
query: MarketDataUpdateDocument,
|
||||
variables: {
|
||||
marketId: market.id,
|
||||
},
|
||||
|
||||
const candles = [
|
||||
{
|
||||
open: '5',
|
||||
close: '5',
|
||||
high: '5',
|
||||
low: '5',
|
||||
volume: '50',
|
||||
periodStart: yesterday.toISOString(),
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsData: [marketData],
|
||||
},
|
||||
{
|
||||
open: '10',
|
||||
close: '10',
|
||||
high: '10',
|
||||
low: '10',
|
||||
volume: '50',
|
||||
periodStart: yesterday.toISOString(),
|
||||
},
|
||||
};
|
||||
];
|
||||
|
||||
const mockOnSelect = jest.fn();
|
||||
|
||||
const renderJsx = () => {
|
||||
const renderJsx = (mocks: MockedResponse[]) => {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider mocks={[mock]}>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MarketSelectorItem
|
||||
market={market}
|
||||
currentMarketId={market.id}
|
||||
@@ -94,11 +108,66 @@ describe('MarketSelectorItem', () => {
|
||||
);
|
||||
};
|
||||
|
||||
let dateSpy: jest.SpyInstance;
|
||||
const ts = 1685577600000; // 2023-06-01
|
||||
|
||||
beforeAll(() => {
|
||||
dateSpy = jest.spyOn(Date, 'now').mockImplementation(() => ts);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
dateSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('renders market information', async () => {
|
||||
const symbol =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.symbol;
|
||||
|
||||
renderJsx();
|
||||
const mock: MockedResponse<MarketDataUpdateSubscription> = {
|
||||
request: {
|
||||
query: MarketDataUpdateDocument,
|
||||
variables: {
|
||||
marketId: market.id,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsData: [marketData],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const since = subDays(Date.now(), 5).toISOString();
|
||||
const variables: MarketCandlesQueryVariables = {
|
||||
marketId: market.id,
|
||||
interval: Interval.INTERVAL_I1H,
|
||||
since,
|
||||
};
|
||||
const mockCandles: MockedResponse<MarketCandlesQuery> = {
|
||||
request: {
|
||||
query: MarketCandlesDocument,
|
||||
variables,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
candlesConnection: {
|
||||
edges: candles.map((c) => ({
|
||||
node: c,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderJsx([mock, mockCandles]);
|
||||
|
||||
const link = screen.getByRole('link');
|
||||
// link renders and is styled
|
||||
@@ -106,18 +175,17 @@ describe('MarketSelectorItem', () => {
|
||||
|
||||
expect(link).toHaveClass('ring-1');
|
||||
|
||||
expect(screen.getByTitle('24h vol')).toHaveTextContent('100');
|
||||
expect(screen.getByTitle('24h vol')).toHaveTextContent('0.00');
|
||||
expect(screen.getByTitle(symbol)).toHaveTextContent('-');
|
||||
|
||||
// candles are loaded immediately
|
||||
expect(screen.getByTestId('market-item-change')).toHaveTextContent(
|
||||
'+100.00%'
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTitle('24h vol')).toHaveTextContent('100');
|
||||
expect(screen.getByTitle(symbol)).toHaveTextContent(
|
||||
addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
|
||||
);
|
||||
expect(screen.getByTestId('market-item-change')).toHaveTextContent(
|
||||
'+100.00%'
|
||||
);
|
||||
});
|
||||
|
||||
await userEvent.click(link);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@vegaprotocol/utils';
|
||||
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
|
||||
import { calcCandleVolume } from '@vegaprotocol/markets';
|
||||
import { useCandles } from '@vegaprotocol/markets';
|
||||
import { useMarketDataUpdateSubscription } from '@vegaprotocol/markets';
|
||||
import { Sparkline } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
@@ -80,8 +81,9 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
|
||||
: '';
|
||||
|
||||
const instrument = market.tradableInstrument.instrument;
|
||||
const { oneDayCandles } = useCandles({ marketId: market.id });
|
||||
|
||||
const vol = market.candles ? calcCandleVolume(market.candles) : '0';
|
||||
const vol = oneDayCandles ? calcCandleVolume(oneDayCandles) : '0';
|
||||
const volume =
|
||||
vol && vol !== '0'
|
||||
? addDecimalsFormatNumber(vol, market.positionDecimalPlaces)
|
||||
@@ -111,20 +113,20 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
|
||||
value={price}
|
||||
label={instrument.product.settlementAsset.symbol}
|
||||
/>
|
||||
<div className="relative">
|
||||
{market.candles && (
|
||||
<PriceChange candles={market.candles.map((c) => c.close)} />
|
||||
<div className="relative text-xs p-1">
|
||||
{oneDayCandles && (
|
||||
<PriceChange candles={oneDayCandles.map((c) => c.close)} />
|
||||
)}
|
||||
|
||||
<div
|
||||
// absolute so height is not larger than price change value
|
||||
className="absolute right-0 bottom-0 w-[120px]"
|
||||
>
|
||||
{market.candles && (
|
||||
{oneDayCandles && (
|
||||
<Sparkline
|
||||
width={120}
|
||||
height={20}
|
||||
data={market.candles.filter(Boolean).map((c) => Number(c.close))}
|
||||
data={oneDayCandles.map((c) => Number(c.close))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -133,7 +135,13 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const DataRow = ({ value, label }: { value: string; label: string }) => {
|
||||
const DataRow = ({
|
||||
value,
|
||||
label,
|
||||
}: {
|
||||
value: string | ReactNode;
|
||||
label: string;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="text-ellipsis whitespace-nowrap overflow-hidden leading-tight"
|
||||
|
||||
@@ -296,7 +296,7 @@ const MainGrid = memo(
|
||||
</TradeGridChild>
|
||||
</ResizableGridPanel>
|
||||
<ResizableGridPanel
|
||||
preferredSize={sizesMiddle[2] || 430}
|
||||
preferredSize={sizesMiddle[2] || 300}
|
||||
minSize={200}
|
||||
>
|
||||
<TradeGridChild>
|
||||
|
||||
@@ -53,7 +53,7 @@ export const useMarketSelectorList = ({
|
||||
});
|
||||
|
||||
if (sort === Sort.None) {
|
||||
// Sort by market state primarilly and AtoZ secondarilly
|
||||
// Sort by market state primarily and AtoZ secondarily
|
||||
return orderBy(
|
||||
markets,
|
||||
[
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { useLocalStorage } from '@vegaprotocol/react-helpers';
|
||||
import { SentryInit, SentryClose } from '@vegaprotocol/utils';
|
||||
import { SentryInit, SentryClose } from '@vegaprotocol/logger';
|
||||
import { STORAGE_KEY, useTelemetryApproval } from './use-telemetry-approval';
|
||||
|
||||
const mockSetValue = jest.fn();
|
||||
const mockRemoveValue = jest.fn();
|
||||
jest.mock('@vegaprotocol/utils');
|
||||
jest.mock('@vegaprotocol/logger');
|
||||
jest.mock('@vegaprotocol/react-helpers', () => ({
|
||||
...jest.requireActual('@vegaprotocol/react-helpers'),
|
||||
useLocalStorage: jest
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useLocalStorage } from '@vegaprotocol/react-helpers';
|
||||
import { useCallback } from 'react';
|
||||
import { SentryInit, SentryClose } from '@vegaprotocol/utils';
|
||||
import { SentryInit, SentryClose } from '@vegaprotocol/logger';
|
||||
import { ENV } from '../config';
|
||||
export const STORAGE_KEY = 'vega_telemetry_approval';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ENV } from './lib/config/env';
|
||||
import { LocalStorage, SentryInit } from '@vegaprotocol/utils';
|
||||
import { LocalStorage, SentryInit } from '@vegaprotocol/logger';
|
||||
import { STORAGE_KEY } from './lib/hooks/use-telemetry-approval';
|
||||
|
||||
const { dsn, envName } = ENV;
|
||||
|
||||
@@ -3,6 +3,7 @@ server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
add_header 'Cache-Control' 'max-age=60';
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { createClient as createWSClient } from 'graphql-ws';
|
||||
import { onError } from '@apollo/client/link/error';
|
||||
import { RetryLink } from '@apollo/client/link/retry';
|
||||
import ApolloLinkTimeout from 'apollo-link-timeout';
|
||||
import { localLoggerFactory } from '@vegaprotocol/utils';
|
||||
import { localLoggerFactory } from '@vegaprotocol/logger';
|
||||
import { useHeaderStore } from './header-store';
|
||||
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
@@ -7,44 +7,54 @@ declare global {
|
||||
namespace Cypress {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface Chainable<Subject> {
|
||||
VegaWalletTopUpRewardsPool(
|
||||
transferStartEpoch: number,
|
||||
transferEndEpoch: number
|
||||
): void;
|
||||
VegaWalletTopUpRewardsPool(): void;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function addVegaWalletTopUpRewardsPool() {
|
||||
Cypress.Commands.add(
|
||||
'VegaWalletTopUpRewardsPool',
|
||||
(transferStartEpoch, transferEndEpoch) => {
|
||||
const vegaWalletUrl = Cypress.env('VEGA_WALLET_URL');
|
||||
const token = Cypress.env('VEGA_WALLET_API_TOKEN');
|
||||
const vegaPubKey = Cypress.env('VEGA_PUBLIC_KEY');
|
||||
const assetAddress =
|
||||
'b4f2726571fbe8e33b442dc92ed2d7f0d810e21835b7371a7915a365f07ccd9b';
|
||||
Cypress.Commands.add('VegaWalletTopUpRewardsPool', () => {
|
||||
let transferStartEpoch = 0;
|
||||
let transferEndEpoch = 0;
|
||||
const vegaWalletUrl = Cypress.env('VEGA_WALLET_URL');
|
||||
const token = Cypress.env('VEGA_WALLET_API_TOKEN');
|
||||
const vegaPubKey = Cypress.env('VEGA_PUBLIC_KEY');
|
||||
const assetAddress =
|
||||
'b4f2726571fbe8e33b442dc92ed2d7f0d810e21835b7371a7915a365f07ccd9b';
|
||||
|
||||
createWalletClient(vegaWalletUrl, token);
|
||||
cy.getByTestId('epoch-countdown')
|
||||
.within(() => {
|
||||
cy.get('h3')
|
||||
.invoke('text')
|
||||
.then((epochText) => {
|
||||
transferStartEpoch = Number(epochText.replace('Epoch', '')) + 5;
|
||||
transferEndEpoch = transferStartEpoch + 100;
|
||||
|
||||
const transactionBody: TransferBody = {
|
||||
transfer: {
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||
to: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
asset: assetAddress,
|
||||
amount: '1000000000000000000',
|
||||
recurring: {
|
||||
factor: '1',
|
||||
startEpoch: transferStartEpoch,
|
||||
endEpoch: transferEndEpoch,
|
||||
console.log(transferStartEpoch);
|
||||
console.log(transferEndEpoch);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
createWalletClient(vegaWalletUrl, token);
|
||||
|
||||
const transactionBody: TransferBody = {
|
||||
transfer: {
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||
to: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
asset: assetAddress,
|
||||
amount: '1000000000000000000',
|
||||
recurring: {
|
||||
factor: '1',
|
||||
startEpoch: transferStartEpoch,
|
||||
endEpoch: transferEndEpoch,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
cy.highlight('Topping up rewards pool');
|
||||
cy.highlight('Topping up rewards pool');
|
||||
|
||||
sendVegaTx(vegaPubKey, transactionBody);
|
||||
}
|
||||
);
|
||||
sendVegaTx(vegaPubKey, transactionBody);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { forwardRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { getDecimalSeparator, isNumeric } from '@vegaprotocol/utils';
|
||||
|
||||
interface NumericCellProps {
|
||||
value: number | bigint | null | undefined;
|
||||
valueFormatted: string;
|
||||
testId?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -12,7 +14,7 @@ interface NumericCellProps {
|
||||
* use, right aligned, monospace and decimals deemphasised
|
||||
*/
|
||||
export const NumericCell = forwardRef<HTMLSpanElement, NumericCellProps>(
|
||||
({ value, valueFormatted, testId }, ref) => {
|
||||
({ value, valueFormatted, testId, className }, ref) => {
|
||||
if (!isNumeric(value)) {
|
||||
return (
|
||||
<span ref={ref} data-testid={testId}>
|
||||
@@ -29,7 +31,10 @@ export const NumericCell = forwardRef<HTMLSpanElement, NumericCellProps>(
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
className="font-mono relative text-black dark:text-white whitespace-nowrap overflow-hidden text-ellipsis text-right rtl-dir"
|
||||
className={classNames(
|
||||
'font-mono relative text-black dark:text-white whitespace-nowrap overflow-hidden text-ellipsis text-right rtl-dir',
|
||||
className
|
||||
)}
|
||||
data-testid={testId}
|
||||
title={valueFormatted}
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@ export const OrderTypeCell = ({
|
||||
data: order,
|
||||
onClick,
|
||||
}: OrderTypeCellProps) => {
|
||||
const id = order ? order.market.id : '';
|
||||
const id = order?.market?.id ?? '';
|
||||
|
||||
const label = useMemo(() => {
|
||||
if (!order) {
|
||||
|
||||
@@ -6,11 +6,15 @@ export interface IPriceCellProps {
|
||||
valueFormatted: string;
|
||||
testId?: string;
|
||||
onClick?: (price?: string | number) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const PriceCell = memo(
|
||||
forwardRef<HTMLSpanElement, IPriceCellProps>(
|
||||
({ value, valueFormatted, testId, onClick }: IPriceCellProps, ref) => {
|
||||
(
|
||||
{ value, valueFormatted, testId, onClick, className }: IPriceCellProps,
|
||||
ref
|
||||
) => {
|
||||
if (!isNumeric(value)) {
|
||||
return (
|
||||
<span data-testid="price" ref={ref}>
|
||||
@@ -27,6 +31,7 @@ export const PriceCell = memo(
|
||||
value={value}
|
||||
valueFormatted={valueFormatted}
|
||||
testId={testId || 'price'}
|
||||
className={className}
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
|
||||
@@ -31,6 +31,7 @@ export const MarginWarning = ({ margin, balance, asset }: Props) => {
|
||||
text: t(`Deposit ${asset.symbol}`),
|
||||
action: () => openDepositDialog(asset.id),
|
||||
dataTestId: 'deal-ticket-deposit-dialog-button',
|
||||
size: 'sm',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -21,10 +21,14 @@ export const ZeroBalanceError = ({
|
||||
testId="dealticket-error-message-zero-balance"
|
||||
message={
|
||||
<>
|
||||
You need {asset.symbol} in your wallet to trade in this market.
|
||||
{t(
|
||||
'You need %s in your wallet to trade in this market. ',
|
||||
asset.symbol
|
||||
)}
|
||||
{onClickCollateral && (
|
||||
<>
|
||||
See all your <Link onClick={onClickCollateral}>collateral</Link>.
|
||||
{t('See all your')}{' '}
|
||||
<Link onClick={onClickCollateral}>collateral</Link>.
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@@ -33,7 +37,7 @@ export const ZeroBalanceError = ({
|
||||
text: t(`Make a deposit`),
|
||||
action: () => openDepositDialog(asset.id),
|
||||
dataTestId: 'deal-ticket-deposit-dialog-button',
|
||||
size: 'md',
|
||||
size: 'sm',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { ButtonVariant } from '@vegaprotocol/ui-toolkit';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
|
||||
interface Props {
|
||||
disabled: boolean;
|
||||
variant: ButtonVariant;
|
||||
}
|
||||
|
||||
export const DealTicketButton = ({ disabled, variant }: Props) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const isDisabled = !pubKey || isReadOnly || disabled;
|
||||
export const DealTicketButton = ({ variant }: Props) => {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<Button
|
||||
variant={variant}
|
||||
fill
|
||||
type="submit"
|
||||
disabled={isDisabled}
|
||||
data-testid="place-order"
|
||||
>
|
||||
<Button variant={variant} fill type="submit" data-testid="place-order">
|
||||
{t('Place order')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -33,24 +33,35 @@ export const DealTicketFeeDetails = (props: FeeDetails) => {
|
||||
const details = getFeeDetailsValues(props);
|
||||
return (
|
||||
<div>
|
||||
{details.map(({ label, value, labelDescription, symbol, indent }) => (
|
||||
<div
|
||||
key={typeof label === 'string' ? label : 'value-dropdown'}
|
||||
className={classnames(
|
||||
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
|
||||
{ 'ml-2': indent }
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<Tooltip description={labelDescription}>
|
||||
<div>{label}</div>
|
||||
{details.map(
|
||||
({
|
||||
label,
|
||||
value,
|
||||
labelDescription,
|
||||
symbol,
|
||||
indent,
|
||||
formattedValue,
|
||||
}) => (
|
||||
<div
|
||||
key={typeof label === 'string' ? label : 'value-dropdown'}
|
||||
className={classnames(
|
||||
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
|
||||
{ 'ml-2': indent }
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<Tooltip description={labelDescription}>
|
||||
<div>{label}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip description={`${value ?? '-'} ${symbol || ''}`}>
|
||||
<div className="text-neutral-500 dark:text-neutral-300">{`${
|
||||
formattedValue ?? '-'
|
||||
} ${symbol || ''}`}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="text-neutral-500 dark:text-neutral-300">{`${
|
||||
value ?? '-'
|
||||
} ${symbol || ''}`}</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -42,6 +42,9 @@ describe('DealTicket', () => {
|
||||
it('should display ticket defaults', () => {
|
||||
const { container } = render(generateJsx());
|
||||
|
||||
// place order button should always be enabled
|
||||
expect(screen.getByTestId('place-order')).toBeEnabled();
|
||||
|
||||
// Assert defaults are used
|
||||
expect(
|
||||
screen.getByTestId(`order-type-${Schema.OrderType.TYPE_MARKET}`)
|
||||
|
||||
@@ -217,6 +217,8 @@ export const DealTicket = ({
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// No error found above clear the error in case it was active on a previous render
|
||||
clearErrors('summary');
|
||||
}, [
|
||||
marketState,
|
||||
@@ -480,7 +482,6 @@ export const DealTicket = ({
|
||||
onClickCollateral={onClickCollateral}
|
||||
/>
|
||||
<DealTicketButton
|
||||
disabled={Object.keys(errors).length >= 1 || isReadOnly}
|
||||
variant={
|
||||
order.side === Schema.Side.SIDE_BUY ? 'ternary' : 'secondary'
|
||||
}
|
||||
@@ -562,7 +563,7 @@ const SummaryMessage = memo(
|
||||
text: t('Connect wallet'),
|
||||
action: openVegaWalletDialog,
|
||||
dataTestId: 'order-connect-wallet',
|
||||
size: 'md',
|
||||
size: 'sm',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { formatRange, formatValue } from './use-fee-deal-ticket-details';
|
||||
|
||||
describe('useFeeDealTicketDetails', () => {
|
||||
it.each([
|
||||
{ v: 123000, d: 5, o: '1.23' },
|
||||
{ v: 123000, d: 3, o: '123.00' },
|
||||
{ v: 123000, d: 1, o: '12,300.0' },
|
||||
{ v: 123001000, d: 2, o: '1,230,010.00' },
|
||||
{ v: 123001, d: 2, o: '1,230.01' },
|
||||
{
|
||||
v: '123456789123456789',
|
||||
d: 10,
|
||||
o: '12,345,678.91234568',
|
||||
},
|
||||
])('formats values correctly', ({ v, d, o }) => {
|
||||
expect(formatValue(v, d)).toStrictEqual(o);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ v: 123000, d: 5, o: '1.23', q: '0.1' },
|
||||
{ v: 123000, d: 3, o: '123.00', q: '0.1' },
|
||||
{ v: 123000, d: 1, o: '12,300.00', q: '0.1' },
|
||||
{ v: 123001000, d: 2, o: '1,230,010.00', q: '0.1' },
|
||||
{ v: 123001, d: 2, o: '1,230', q: '100' },
|
||||
{ v: 123001, d: 2, o: '1,230.01', q: '0.1' },
|
||||
{
|
||||
v: '123456789123456789',
|
||||
d: 10,
|
||||
o: '12,345,678.9123457',
|
||||
q: '0.00003846',
|
||||
},
|
||||
])(
|
||||
'formats with formatValue with quantum given number correctly',
|
||||
({ v, d, o, q }) => {
|
||||
expect(formatValue(v.toString(), d, q)).toStrictEqual(o);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ min: 123000, max: 12300011111, d: 5, o: '1.23 - 123,000.111', q: '0.1' },
|
||||
{
|
||||
min: 123000,
|
||||
max: 12300011111,
|
||||
d: 3,
|
||||
o: '123.00 - 12,300,011.111',
|
||||
q: '0.1',
|
||||
},
|
||||
{
|
||||
min: 123000,
|
||||
max: 12300011111,
|
||||
d: 1,
|
||||
o: '12,300.00 - 1,230,001,111.10',
|
||||
q: '0.1',
|
||||
},
|
||||
{
|
||||
min: 123001000,
|
||||
max: 12300011111,
|
||||
d: 2,
|
||||
o: '1,230,010 - 123,000,111',
|
||||
q: '100',
|
||||
},
|
||||
])(
|
||||
'formats with formatValue with quantum given number correctly',
|
||||
({ min, max, d, o, q }) => {
|
||||
expect(formatRange(min, max, d, q)).toStrictEqual(o);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
import { FeesBreakdown } from '@vegaprotocol/markets';
|
||||
import { addDecimalsFormatNumber, isNumeric } from '@vegaprotocol/utils';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
addDecimalsFormatNumberQuantum,
|
||||
isNumeric,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
@@ -52,21 +56,25 @@ export interface FeeDetails {
|
||||
}
|
||||
|
||||
const emptyValue = '-';
|
||||
const formatValue = (
|
||||
|
||||
export const formatValue = (
|
||||
value: string | number | null | undefined,
|
||||
formatDecimals: number
|
||||
formatDecimals: number,
|
||||
quantum?: string
|
||||
): string => {
|
||||
return isNumeric(value)
|
||||
? addDecimalsFormatNumber(value, formatDecimals)
|
||||
: emptyValue;
|
||||
if (!isNumeric(value)) return emptyValue;
|
||||
if (!quantum) return addDecimalsFormatNumber(value, formatDecimals);
|
||||
return addDecimalsFormatNumberQuantum(value, formatDecimals, quantum);
|
||||
};
|
||||
const formatRange = (
|
||||
|
||||
export const formatRange = (
|
||||
min: string | number | null | undefined,
|
||||
max: string | number | null | undefined,
|
||||
formatDecimals: number
|
||||
formatDecimals: number,
|
||||
quantum?: string
|
||||
) => {
|
||||
const minFormatted = formatValue(min, formatDecimals);
|
||||
const maxFormatted = formatValue(max, formatDecimals);
|
||||
const minFormatted = formatValue(min, formatDecimals, quantum);
|
||||
const maxFormatted = formatValue(max, formatDecimals, quantum);
|
||||
if (minFormatted !== maxFormatted) {
|
||||
return `${minFormatted} - ${maxFormatted}`;
|
||||
}
|
||||
@@ -93,9 +101,12 @@ export const getFeeDetailsValues = ({
|
||||
BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0');
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
const quantum =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.quantum;
|
||||
const details: {
|
||||
label: string;
|
||||
value?: string | null;
|
||||
formattedValue?: string | null;
|
||||
symbol: string;
|
||||
indent?: boolean;
|
||||
labelDescription?: React.ReactNode;
|
||||
@@ -103,6 +114,7 @@ export const getFeeDetailsValues = ({
|
||||
{
|
||||
label: t('Notional'),
|
||||
value: formatValue(notionalSize, assetDecimals),
|
||||
formattedValue: formatValue(notionalSize, assetDecimals, quantum),
|
||||
symbol: assetSymbol,
|
||||
labelDescription: NOTIONAL_SIZE_TOOLTIP_TEXT(assetSymbol),
|
||||
},
|
||||
@@ -111,6 +123,9 @@ export const getFeeDetailsValues = ({
|
||||
value:
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`,
|
||||
formattedValue:
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`,
|
||||
labelDescription: (
|
||||
<>
|
||||
<span>
|
||||
@@ -154,6 +169,12 @@ export const getFeeDetailsValues = ({
|
||||
}
|
||||
details.push({
|
||||
label: t('Margin required'),
|
||||
formattedValue: formatRange(
|
||||
marginRequiredBestCase,
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
value: formatRange(
|
||||
marginRequiredBestCase,
|
||||
marginRequiredWorstCase,
|
||||
@@ -172,12 +193,13 @@ export const getFeeDetailsValues = ({
|
||||
details.push({
|
||||
indent: true,
|
||||
label: t('Total margin available'),
|
||||
formattedValue: formatValue(totalMarginAvailable, assetDecimals, quantum),
|
||||
value: formatValue(totalMarginAvailable, assetDecimals),
|
||||
symbol: assetSymbol,
|
||||
labelDescription: TOTAL_MARGIN_AVAILABLE(
|
||||
formatValue(generalAccountBalance, assetDecimals),
|
||||
formatValue(marginAccountBalance, assetDecimals),
|
||||
formatValue(currentMaintenanceMargin, assetDecimals),
|
||||
formatValue(generalAccountBalance, assetDecimals, quantum),
|
||||
formatValue(marginAccountBalance, assetDecimals, quantum),
|
||||
formatValue(currentMaintenanceMargin, assetDecimals, quantum),
|
||||
assetSymbol
|
||||
),
|
||||
});
|
||||
@@ -203,6 +225,16 @@ export const getFeeDetailsValues = ({
|
||||
: '0',
|
||||
assetDecimals
|
||||
),
|
||||
formattedValue: formatRange(
|
||||
deductionFromCollateralBestCase > 0
|
||||
? deductionFromCollateralBestCase.toString()
|
||||
: '0',
|
||||
deductionFromCollateralWorstCase > 0
|
||||
? deductionFromCollateralWorstCase.toString()
|
||||
: '0',
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
symbol: assetSymbol,
|
||||
labelDescription: DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT(assetSymbol),
|
||||
});
|
||||
@@ -214,6 +246,12 @@ export const getFeeDetailsValues = ({
|
||||
marginEstimate?.worstCase.initialLevel,
|
||||
assetDecimals
|
||||
),
|
||||
formattedValue: formatRange(
|
||||
marginEstimate?.bestCase.initialLevel,
|
||||
marginEstimate?.worstCase.initialLevel,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
symbol: assetSymbol,
|
||||
labelDescription: EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||
});
|
||||
@@ -223,9 +261,11 @@ export const getFeeDetailsValues = ({
|
||||
value: formatValue(marginAccountBalance, assetDecimals),
|
||||
symbol: assetSymbol,
|
||||
labelDescription: MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
formattedValue: formatValue(marginAccountBalance, assetDecimals, quantum),
|
||||
});
|
||||
|
||||
let liquidationPriceEstimate = emptyValue;
|
||||
let liquidationPriceEstimateFormatted;
|
||||
|
||||
if (liquidationEstimate) {
|
||||
const liquidationEstimateBestCaseIncludingBuyOrders = BigInt(
|
||||
@@ -262,11 +302,24 @@ export const getFeeDetailsValues = ({
|
||||
).toString(),
|
||||
assetDecimals
|
||||
);
|
||||
liquidationPriceEstimateFormatted = formatRange(
|
||||
(liquidationEstimateBestCase < liquidationEstimateWorstCase
|
||||
? liquidationEstimateBestCase
|
||||
: liquidationEstimateWorstCase
|
||||
).toString(),
|
||||
(liquidationEstimateBestCase > liquidationEstimateWorstCase
|
||||
? liquidationEstimateBestCase
|
||||
: liquidationEstimateWorstCase
|
||||
).toString(),
|
||||
assetDecimals,
|
||||
quantum
|
||||
);
|
||||
}
|
||||
|
||||
details.push({
|
||||
label: t('Liquidation price estimate'),
|
||||
value: liquidationPriceEstimate,
|
||||
formattedValue: liquidationPriceEstimateFormatted,
|
||||
symbol: assetSymbol,
|
||||
labelDescription: LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ export function generateMarket(override?: PartialDeep<Market>): Market {
|
||||
symbol: 'tDAI',
|
||||
name: 'tDAI',
|
||||
decimals: 5,
|
||||
quantum: '1',
|
||||
__typename: 'Asset',
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { MarketState, MarketStateMapping } from '@vegaprotocol/types';
|
||||
|
||||
export const validateMarketState = (state: Schema.MarketState) => {
|
||||
export const validateMarketState = (state: MarketState) => {
|
||||
if (
|
||||
[
|
||||
Schema.MarketState.STATE_SETTLED,
|
||||
Schema.MarketState.STATE_REJECTED,
|
||||
Schema.MarketState.STATE_TRADING_TERMINATED,
|
||||
Schema.MarketState.STATE_CANCELLED,
|
||||
Schema.MarketState.STATE_CLOSED,
|
||||
MarketState.STATE_SETTLED,
|
||||
MarketState.STATE_REJECTED,
|
||||
MarketState.STATE_TRADING_TERMINATED,
|
||||
MarketState.STATE_CANCELLED,
|
||||
MarketState.STATE_CLOSED,
|
||||
].includes(state)
|
||||
) {
|
||||
return t(
|
||||
@@ -16,7 +16,7 @@ export const validateMarketState = (state: Schema.MarketState) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (state === Schema.MarketState.STATE_PROPOSED) {
|
||||
if (state === MarketState.STATE_PROPOSED) {
|
||||
return t(
|
||||
`This market is ${marketTranslations(
|
||||
state
|
||||
@@ -27,11 +27,11 @@ export const validateMarketState = (state: Schema.MarketState) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const marketTranslations = (marketState: Schema.MarketState) => {
|
||||
const marketTranslations = (marketState: MarketState) => {
|
||||
switch (marketState) {
|
||||
case Schema.MarketState.STATE_TRADING_TERMINATED:
|
||||
case MarketState.STATE_TRADING_TERMINATED:
|
||||
return t('terminated');
|
||||
default:
|
||||
return t(Schema.MarketStateMapping[marketState]).toLowerCase();
|
||||
return t(MarketStateMapping[marketState]).toLowerCase();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||
|
||||
export const validateMarketTradingMode = (
|
||||
marketTradingMode: Schema.MarketTradingMode
|
||||
marketTradingMode: MarketTradingMode
|
||||
) => {
|
||||
if (marketTradingMode === Schema.MarketTradingMode.TRADING_MODE_NO_TRADING) {
|
||||
if (marketTradingMode === MarketTradingMode.TRADING_MODE_NO_TRADING) {
|
||||
return t('Trading terminated');
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
useIsExemptDepositor,
|
||||
} from './use-get-deposit-maximum';
|
||||
import { useGetDepositedAmount } from './use-get-deposited-amount';
|
||||
import { isAssetTypeERC20, localLoggerFactory } from '@vegaprotocol/utils';
|
||||
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
|
||||
import { localLoggerFactory } from '@vegaprotocol/logger';
|
||||
import { useAccountBalance } from '@vegaprotocol/accounts';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useCallback } from 'react';
|
||||
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { addDecimal, localLoggerFactory } from '@vegaprotocol/utils';
|
||||
import { addDecimal } from '@vegaprotocol/utils';
|
||||
import { localLoggerFactory } from '@vegaprotocol/logger';
|
||||
|
||||
export const useGetAllowance = (
|
||||
contract: Token | null,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useCallback } from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { addDecimal, localLoggerFactory } from '@vegaprotocol/utils';
|
||||
import { addDecimal } from '@vegaprotocol/utils';
|
||||
import { localLoggerFactory } from '@vegaprotocol/logger';
|
||||
import type { CollateralBridge } from '@vegaprotocol/smart-contracts';
|
||||
|
||||
export const useGetDepositMaximum = (
|
||||
|
||||
@@ -3,7 +3,8 @@ import { ethers } from 'ethers';
|
||||
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { addDecimal, localLoggerFactory } from '@vegaprotocol/utils';
|
||||
import { addDecimal } from '@vegaprotocol/utils';
|
||||
import { localLoggerFactory } from '@vegaprotocol/logger';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
|
||||
export const useGetDepositedAmount = (asset: Asset | undefined) => {
|
||||
|
||||
@@ -72,6 +72,7 @@ export const DocsLinks = VEGA_DOCS_URL
|
||||
POSITION_RESOLUTION: `${VEGA_DOCS_URL}/concepts/trading-on-vega/market-protections#position-resolution`,
|
||||
LIQUIDITY: `${VEGA_DOCS_URL}/concepts/liquidity/provision`,
|
||||
WITHDRAWAL_LIMITS: `${VEGA_DOCS_URL}/concepts/assets/deposits-withdrawals#withdrawal-limits`,
|
||||
VALIDATOR_SCORES_REWARDS: `${VEGA_DOCS_URL}/concepts/vega-chain/validator-scores-and-rewards`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { localLoggerFactory } from '@vegaprotocol/utils';
|
||||
import { localLoggerFactory } from '@vegaprotocol/logger';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import z from 'zod';
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ export const generateFill = (override?: PartialDeep<Trade>) => {
|
||||
name: 'assset-id',
|
||||
symbol: 'SYM',
|
||||
decimals: 18,
|
||||
quantum: '1',
|
||||
},
|
||||
quoteName: '',
|
||||
dataSourceSpecForTradingTermination: {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nrwl/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# logger
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test logger` to execute the unit tests via [Jest](https://jestjs.io).
|
||||
@@ -0,0 +1,10 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'logger',
|
||||
preset: '../../jest.preset.js',
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': 'babel-jest',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/logger',
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "@vegaprotocol/logger",
|
||||
"version": "0.0.1",
|
||||
"type": "commonjs"
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/logger/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nrwl/js:tsc",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"format": ["esm", "cjs"],
|
||||
"options": {
|
||||
"outputPath": "dist/libs/logger",
|
||||
"main": "libs/logger/src/index.ts",
|
||||
"tsConfig": "libs/logger/tsconfig.lib.json",
|
||||
"assets": ["libs/logger/*.md"]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nrwl/linter:eslint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["libs/logger/**/*.{ts,tsx,js,jsx}"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nrwl/jest:jest",
|
||||
"outputs": ["coverage/libs/logger"],
|
||||
"options": {
|
||||
"jestConfig": "libs/logger/jest.config.ts",
|
||||
"passWithNoTests": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './use-logger';
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useRef } from 'react';
|
||||
import type { LocalLogger, LoggerConf } from '@vegaprotocol/utils';
|
||||
import { localLoggerFactory, SentryInit } from '@vegaprotocol/utils';
|
||||
import type { LocalLogger, LoggerConf } from '../lib/local-logger';
|
||||
import { localLoggerFactory } from '../lib/local-logger';
|
||||
import { SentryInit } from '../lib/sentry-utils';
|
||||
|
||||
export interface LoggerProps extends LoggerConf {
|
||||
dsn?: string;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './lib';
|
||||
export * from './hooks';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './local-logger';
|
||||
export * from './sentry-utils';
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.jsx",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
compactRows,
|
||||
updateLevels,
|
||||
updateCompactedRows,
|
||||
} from './orderbook-data';
|
||||
import type { OrderbookRowData } from './orderbook-data';
|
||||
import { compactRows, updateLevels, VolumeType } from './orderbook-data';
|
||||
import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth';
|
||||
|
||||
describe('compactRows', () => {
|
||||
@@ -26,51 +21,31 @@ describe('compactRows', () => {
|
||||
numberOfOrders: (numberOfRows - i).toString(),
|
||||
}));
|
||||
it('groups data by price and resolution', () => {
|
||||
expect(compactRows(sell, buy, 1).length).toEqual(200);
|
||||
expect(compactRows(sell, buy, 5).length).toEqual(41);
|
||||
expect(compactRows(sell, buy, 10).length).toEqual(21);
|
||||
expect(compactRows(sell, VolumeType.ask, 1).length).toEqual(100);
|
||||
expect(compactRows(buy, VolumeType.bid, 1).length).toEqual(100);
|
||||
expect(compactRows(sell, VolumeType.ask, 5).length).toEqual(21);
|
||||
expect(compactRows(buy, VolumeType.bid, 5).length).toEqual(21);
|
||||
expect(compactRows(sell, VolumeType.ask, 10).length).toEqual(11);
|
||||
expect(compactRows(buy, VolumeType.bid, 10).length).toEqual(11);
|
||||
});
|
||||
it('counts cumulative vol', () => {
|
||||
const orderbookRows = compactRows(sell, buy, 10);
|
||||
expect(orderbookRows[0].cumulativeVol.ask).toEqual(4950);
|
||||
expect(orderbookRows[0].cumulativeVol.bid).toEqual(0);
|
||||
expect(orderbookRows[10].cumulativeVol.ask).toEqual(390);
|
||||
expect(orderbookRows[10].cumulativeVol.bid).toEqual(579);
|
||||
expect(orderbookRows[orderbookRows.length - 1].cumulativeVol.bid).toEqual(
|
||||
4950
|
||||
);
|
||||
expect(orderbookRows[orderbookRows.length - 1].cumulativeVol.ask).toEqual(
|
||||
0
|
||||
);
|
||||
});
|
||||
it('stores volume by level', () => {
|
||||
const orderbookRows = compactRows(sell, buy, 10);
|
||||
expect(orderbookRows[0].askByLevel).toEqual({
|
||||
'1095': 5,
|
||||
'1096': 4,
|
||||
'1097': 3,
|
||||
'1098': 2,
|
||||
'1099': 1,
|
||||
});
|
||||
expect(orderbookRows[orderbookRows.length - 1].bidByLevel).toEqual({
|
||||
'902': 1,
|
||||
'903': 2,
|
||||
'904': 3,
|
||||
});
|
||||
const asks = compactRows(sell, VolumeType.ask, 10);
|
||||
const bids = compactRows(buy, VolumeType.bid, 10);
|
||||
expect(asks[0].cumulativeVol.value).toEqual(4950);
|
||||
expect(bids[0].cumulativeVol.value).toEqual(579);
|
||||
expect(asks[10].cumulativeVol.value).toEqual(390);
|
||||
expect(bids[10].cumulativeVol.value).toEqual(4950);
|
||||
expect(bids[bids.length - 1].cumulativeVol.value).toEqual(4950);
|
||||
expect(asks[asks.length - 1].cumulativeVol.value).toEqual(390);
|
||||
});
|
||||
|
||||
it('updates relative data', () => {
|
||||
const orderbookRows = compactRows(sell, buy, 10);
|
||||
expect(orderbookRows[0].cumulativeVol.relativeAsk).toEqual(100);
|
||||
expect(orderbookRows[0].cumulativeVol.relativeBid).toEqual(0);
|
||||
expect(orderbookRows[0].relativeAsk).toEqual(2);
|
||||
expect(orderbookRows[0].relativeBid).toEqual(0);
|
||||
expect(orderbookRows[10].cumulativeVol.relativeAsk).toEqual(8);
|
||||
expect(orderbookRows[10].cumulativeVol.relativeBid).toEqual(12);
|
||||
expect(orderbookRows[10].relativeAsk).toEqual(44);
|
||||
expect(orderbookRows[10].relativeBid).toEqual(64);
|
||||
expect(orderbookRows[orderbookRows.length - 1].relativeAsk).toEqual(0);
|
||||
expect(orderbookRows[orderbookRows.length - 1].relativeBid).toEqual(1);
|
||||
const asks = compactRows(sell, VolumeType.ask, 10);
|
||||
const bids = compactRows(buy, VolumeType.bid, 10);
|
||||
expect(asks[0].cumulativeVol.relativeValue).toEqual(100);
|
||||
expect(bids[0].cumulativeVol.relativeValue).toEqual(12);
|
||||
expect(asks[10].cumulativeVol.relativeValue).toEqual(8);
|
||||
expect(bids[10].cumulativeVol.relativeValue).toEqual(100);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,167 +105,3 @@ describe('updateLevels', () => {
|
||||
expect(updateLevels([], [updateLastRow])).toEqual([updateLastRow]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateCompactedRows', () => {
|
||||
const orderbookRows: OrderbookRowData[] = [
|
||||
{
|
||||
price: '120',
|
||||
cumulativeVol: {
|
||||
ask: 50,
|
||||
relativeAsk: 100,
|
||||
bid: 0,
|
||||
relativeBid: 0,
|
||||
},
|
||||
askByLevel: {
|
||||
'121': 10,
|
||||
},
|
||||
bidByLevel: {},
|
||||
ask: 10,
|
||||
bid: 0,
|
||||
relativeAsk: 25,
|
||||
relativeBid: 0,
|
||||
},
|
||||
{
|
||||
price: '100',
|
||||
cumulativeVol: {
|
||||
ask: 40,
|
||||
relativeAsk: 80,
|
||||
bid: 40,
|
||||
relativeBid: 80,
|
||||
},
|
||||
askByLevel: {
|
||||
'101': 10,
|
||||
'102': 30,
|
||||
},
|
||||
bidByLevel: {
|
||||
'99': 10,
|
||||
'98': 30,
|
||||
},
|
||||
ask: 40,
|
||||
bid: 40,
|
||||
relativeAsk: 100,
|
||||
relativeBid: 100,
|
||||
},
|
||||
{
|
||||
price: '80',
|
||||
cumulativeVol: {
|
||||
ask: 0,
|
||||
relativeAsk: 0,
|
||||
bid: 50,
|
||||
relativeBid: 100,
|
||||
},
|
||||
askByLevel: {},
|
||||
bidByLevel: {
|
||||
'79': 10,
|
||||
},
|
||||
ask: 0,
|
||||
bid: 10,
|
||||
relativeAsk: 0,
|
||||
relativeBid: 25,
|
||||
},
|
||||
];
|
||||
const resolution = 10;
|
||||
|
||||
it('update volume', () => {
|
||||
const sell: PriceLevelFieldsFragment = {
|
||||
__typename: 'PriceLevel',
|
||||
price: '120',
|
||||
volume: '10',
|
||||
numberOfOrders: '10',
|
||||
};
|
||||
const buy: PriceLevelFieldsFragment = {
|
||||
__typename: 'PriceLevel',
|
||||
price: '80',
|
||||
volume: '10',
|
||||
numberOfOrders: '10',
|
||||
};
|
||||
const updatedRows = updateCompactedRows(
|
||||
orderbookRows,
|
||||
[sell],
|
||||
[buy],
|
||||
resolution
|
||||
);
|
||||
expect(updatedRows[0].ask).toEqual(20);
|
||||
expect(updatedRows[0].askByLevel?.[120]).toEqual(10);
|
||||
expect(updatedRows[0].cumulativeVol.ask).toEqual(60);
|
||||
expect(updatedRows[2].bid).toEqual(20);
|
||||
expect(updatedRows[2].bidByLevel?.[80]).toEqual(10);
|
||||
expect(updatedRows[2].cumulativeVol.bid).toEqual(60);
|
||||
});
|
||||
|
||||
it('remove row', () => {
|
||||
const sell: PriceLevelFieldsFragment = {
|
||||
__typename: 'PriceLevel',
|
||||
price: '121',
|
||||
volume: '0',
|
||||
numberOfOrders: '0',
|
||||
};
|
||||
const buy: PriceLevelFieldsFragment = {
|
||||
__typename: 'PriceLevel',
|
||||
price: '79',
|
||||
volume: '0',
|
||||
numberOfOrders: '0',
|
||||
};
|
||||
const updatedRows = updateCompactedRows(
|
||||
orderbookRows,
|
||||
[sell],
|
||||
[buy],
|
||||
resolution
|
||||
);
|
||||
expect(updatedRows.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('add new row at the end', () => {
|
||||
const sell: PriceLevelFieldsFragment = {
|
||||
__typename: 'PriceLevel',
|
||||
price: '131',
|
||||
volume: '5',
|
||||
numberOfOrders: '5',
|
||||
};
|
||||
const buy: PriceLevelFieldsFragment = {
|
||||
__typename: 'PriceLevel',
|
||||
price: '59',
|
||||
volume: '5',
|
||||
numberOfOrders: '5',
|
||||
};
|
||||
const updatedRows = updateCompactedRows(
|
||||
orderbookRows,
|
||||
[sell],
|
||||
[buy],
|
||||
resolution
|
||||
);
|
||||
expect(updatedRows.length).toEqual(5);
|
||||
expect(updatedRows[0].price).toEqual('130');
|
||||
expect(updatedRows[0].cumulativeVol.ask).toEqual(55);
|
||||
expect(updatedRows[4].price).toEqual('60');
|
||||
expect(updatedRows[4].cumulativeVol.bid).toEqual(55);
|
||||
});
|
||||
|
||||
it('add new row in the middle', () => {
|
||||
const sell: PriceLevelFieldsFragment = {
|
||||
__typename: 'PriceLevel',
|
||||
price: '111',
|
||||
volume: '5',
|
||||
numberOfOrders: '5',
|
||||
};
|
||||
const buy: PriceLevelFieldsFragment = {
|
||||
__typename: 'PriceLevel',
|
||||
price: '91',
|
||||
volume: '5',
|
||||
numberOfOrders: '5',
|
||||
};
|
||||
const updatedRows = updateCompactedRows(
|
||||
orderbookRows,
|
||||
[sell],
|
||||
[buy],
|
||||
resolution
|
||||
);
|
||||
expect(updatedRows.length).toEqual(5);
|
||||
expect(updatedRows[1].price).toEqual('110');
|
||||
expect(updatedRows[1].cumulativeVol.ask).toEqual(45);
|
||||
expect(updatedRows[0].cumulativeVol.ask).toEqual(55);
|
||||
expect(updatedRows[3].price).toEqual('90');
|
||||
expect(updatedRows[3].cumulativeVol.bid).toEqual(45);
|
||||
expect(updatedRows[4].cumulativeVol.bid).toEqual(55);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import reverse from 'lodash/reverse';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { MarketData } from '@vegaprotocol/markets';
|
||||
import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth';
|
||||
|
||||
export enum VolumeType {
|
||||
@@ -11,39 +6,16 @@ export enum VolumeType {
|
||||
ask,
|
||||
}
|
||||
export interface CumulativeVol {
|
||||
bid: number;
|
||||
relativeBid?: number;
|
||||
ask: number;
|
||||
relativeAsk?: number;
|
||||
value: number;
|
||||
relativeValue?: number;
|
||||
}
|
||||
|
||||
export interface OrderbookRowData {
|
||||
price: string;
|
||||
bid: number;
|
||||
bidByLevel: Record<string, number>;
|
||||
relativeBid?: number;
|
||||
ask: number;
|
||||
askByLevel: Record<string, number>;
|
||||
relativeAsk?: number;
|
||||
value: number;
|
||||
cumulativeVol: CumulativeVol;
|
||||
}
|
||||
|
||||
type PartialOrderbookRowData = Pick<OrderbookRowData, 'price' | 'ask' | 'bid'>;
|
||||
|
||||
type OrderbookMarketData = Pick<
|
||||
MarketData,
|
||||
| 'bestStaticBidPrice'
|
||||
| 'bestStaticOfferPrice'
|
||||
| 'indicativePrice'
|
||||
| 'indicativeVolume'
|
||||
| 'marketTradingMode'
|
||||
>;
|
||||
|
||||
export type OrderbookData = Partial<OrderbookMarketData> & {
|
||||
rows: OrderbookRowData[] | null;
|
||||
midPrice?: string;
|
||||
};
|
||||
|
||||
export const getPriceLevel = (price: string | bigint, resolution: number) => {
|
||||
const p = BigInt(price);
|
||||
const r = BigInt(resolution);
|
||||
@@ -54,135 +26,66 @@ export const getPriceLevel = (price: string | bigint, resolution: number) => {
|
||||
return priceLevel.toString();
|
||||
};
|
||||
|
||||
export const getMidPrice = (
|
||||
sell: PriceLevelFieldsFragment[] | null | undefined,
|
||||
buy: PriceLevelFieldsFragment[] | null | undefined,
|
||||
resolution: number
|
||||
) =>
|
||||
buy?.length && sell?.length
|
||||
? getPriceLevel(
|
||||
(BigInt(buy[0].price) + BigInt(sell[0].price)) / BigInt(2),
|
||||
resolution
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const getMaxVolumes = (orderbookData: OrderbookRowData[]) => ({
|
||||
bid: Math.max(...orderbookData.map((data) => data.bid)),
|
||||
ask: Math.max(...orderbookData.map((data) => data.ask)),
|
||||
cumulativeVol: Math.max(
|
||||
orderbookData[0]?.cumulativeVol.ask,
|
||||
orderbookData[orderbookData.length - 1]?.cumulativeVol.bid
|
||||
orderbookData[0]?.cumulativeVol.value,
|
||||
orderbookData[orderbookData.length - 1]?.cumulativeVol.value
|
||||
),
|
||||
});
|
||||
|
||||
// round instead of ceil so we will not show 0 if value if different than 0
|
||||
const toPercentValue = (value?: number) => Math.ceil((value ?? 0) * 100);
|
||||
|
||||
/**
|
||||
* @summary Updates relativeAsk, relativeBid, cumulativeVol.relativeAsk, cumulativeVol.relativeBid
|
||||
*/
|
||||
const updateRelativeData = (data: OrderbookRowData[]) => {
|
||||
const { bid, ask, cumulativeVol } = getMaxVolumes(data);
|
||||
const maxBidAsk = Math.max(bid, ask);
|
||||
const { cumulativeVol } = getMaxVolumes(data);
|
||||
data.forEach((data, i) => {
|
||||
data.relativeAsk = toPercentValue(data.ask / maxBidAsk);
|
||||
data.relativeBid = toPercentValue(data.bid / maxBidAsk);
|
||||
data.cumulativeVol.relativeAsk = toPercentValue(
|
||||
data.cumulativeVol.ask / cumulativeVol
|
||||
);
|
||||
data.cumulativeVol.relativeBid = toPercentValue(
|
||||
data.cumulativeVol.bid / cumulativeVol
|
||||
data.cumulativeVol.relativeValue = toPercentValue(
|
||||
data.cumulativeVol.value / cumulativeVol
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const updateCumulativeVolume = (data: OrderbookRowData[]) => {
|
||||
if (data.length > 1) {
|
||||
const updateCumulativeVolumeByType = (
|
||||
data: OrderbookRowData[],
|
||||
dataType: VolumeType
|
||||
) => {
|
||||
if (data.length) {
|
||||
const maxIndex = data.length - 1;
|
||||
for (let i = 0; i <= maxIndex; i++) {
|
||||
data[i].cumulativeVol.bid =
|
||||
data[i].bid + (i !== 0 ? data[i - 1].cumulativeVol.bid : 0);
|
||||
}
|
||||
for (let i = maxIndex; i >= 0; i--) {
|
||||
data[i].cumulativeVol.ask =
|
||||
data[i].ask + (i !== maxIndex ? data[i + 1].cumulativeVol.ask : 0);
|
||||
if (dataType === VolumeType.bid) {
|
||||
for (let i = 0; i <= maxIndex; i++) {
|
||||
data[i].cumulativeVol.value =
|
||||
data[i].value + (i !== 0 ? data[i - 1].cumulativeVol.value : 0);
|
||||
}
|
||||
} else {
|
||||
for (let i = maxIndex; i >= 0; i--) {
|
||||
data[i].cumulativeVol.value =
|
||||
data[i].value +
|
||||
(i !== maxIndex ? data[i + 1].cumulativeVol.value : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const createPartialRow = (
|
||||
price: string,
|
||||
volume = 0,
|
||||
dataType?: VolumeType
|
||||
): PartialOrderbookRowData => ({
|
||||
price,
|
||||
ask: dataType === VolumeType.ask ? volume : 0,
|
||||
bid: dataType === VolumeType.bid ? volume : 0,
|
||||
});
|
||||
|
||||
export const extendRow = (row: PartialOrderbookRowData): OrderbookRowData =>
|
||||
Object.assign(row, {
|
||||
cumulativeVol: {
|
||||
ask: 0,
|
||||
bid: 0,
|
||||
},
|
||||
askByLevel: row.ask ? { [row.price]: row.ask } : {},
|
||||
bidByLevel: row.bid ? { [row.price]: row.bid } : {},
|
||||
});
|
||||
|
||||
export const createRow = (
|
||||
price: string,
|
||||
volume = 0,
|
||||
dataType?: VolumeType
|
||||
): OrderbookRowData => extendRow(createPartialRow(price, volume, dataType));
|
||||
|
||||
const mapRawData =
|
||||
(dataType: VolumeType.ask | VolumeType.bid) =>
|
||||
(data: PriceLevelFieldsFragment): PartialOrderbookRowData =>
|
||||
createPartialRow(data.price, Number(data.volume), dataType);
|
||||
|
||||
/**
|
||||
* @summary merges sell amd buy data, orders by price desc, group by price level, counts cumulative and relative values
|
||||
*/
|
||||
export const compactRows = (
|
||||
sell: PriceLevelFieldsFragment[] | null | undefined,
|
||||
buy: PriceLevelFieldsFragment[] | null | undefined,
|
||||
data: PriceLevelFieldsFragment[] | null | undefined,
|
||||
dataType: VolumeType,
|
||||
resolution: number
|
||||
) => {
|
||||
// map raw sell data to OrderbookData
|
||||
const askOrderbookData = [...(sell ?? [])].map<PartialOrderbookRowData>(
|
||||
mapRawData(VolumeType.ask)
|
||||
);
|
||||
// map raw buy data to OrderbookData
|
||||
const bidOrderbookData = [...(buy ?? [])].map<PartialOrderbookRowData>(
|
||||
mapRawData(VolumeType.bid)
|
||||
);
|
||||
// group by price level
|
||||
const groupedByLevel = groupBy<PartialOrderbookRowData>(
|
||||
[...askOrderbookData, ...bidOrderbookData],
|
||||
(row) => getPriceLevel(row.price, resolution)
|
||||
const groupedByLevel = groupBy(data, (row) =>
|
||||
getPriceLevel(row.price, resolution)
|
||||
);
|
||||
const orderbookData: OrderbookRowData[] = [];
|
||||
Object.keys(groupedByLevel).forEach((price) => {
|
||||
const row = extendRow(
|
||||
groupedByLevel[price].pop() as PartialOrderbookRowData
|
||||
);
|
||||
row.price = price;
|
||||
let subRow: PartialOrderbookRowData | undefined =
|
||||
groupedByLevel[price].pop();
|
||||
const { volume } = groupedByLevel[price].pop() as PriceLevelFieldsFragment;
|
||||
let value = Number(volume);
|
||||
let subRow: { volume: string } | undefined = groupedByLevel[price].pop();
|
||||
while (subRow) {
|
||||
row.ask += subRow.ask;
|
||||
row.bid += subRow.bid;
|
||||
if (subRow.ask) {
|
||||
row.askByLevel[subRow.price] = subRow.ask;
|
||||
}
|
||||
if (subRow.bid) {
|
||||
row.bidByLevel[subRow.price] = subRow.bid;
|
||||
}
|
||||
value += Number(subRow.volume);
|
||||
subRow = groupedByLevel[price].pop();
|
||||
}
|
||||
orderbookData.push(row);
|
||||
orderbookData.push({ price, value, cumulativeVol: { value: 0 } });
|
||||
});
|
||||
|
||||
orderbookData.sort((a, b) => {
|
||||
if (a === b) {
|
||||
return 0;
|
||||
@@ -192,100 +95,11 @@ export const compactRows = (
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
// count cumulative volumes
|
||||
if (orderbookData.length > 1) {
|
||||
const maxIndex = orderbookData.length - 1;
|
||||
for (let i = 0; i <= maxIndex; i++) {
|
||||
orderbookData[i].cumulativeVol.bid =
|
||||
orderbookData[i].bid +
|
||||
(i !== 0 ? orderbookData[i - 1].cumulativeVol.bid : 0);
|
||||
}
|
||||
for (let i = maxIndex; i >= 0; i--) {
|
||||
orderbookData[i].cumulativeVol.ask =
|
||||
orderbookData[i].ask +
|
||||
(i !== maxIndex ? orderbookData[i + 1].cumulativeVol.ask : 0);
|
||||
}
|
||||
}
|
||||
updateCumulativeVolume(orderbookData);
|
||||
// count relative volumes
|
||||
updateCumulativeVolumeByType(orderbookData, dataType);
|
||||
updateRelativeData(orderbookData);
|
||||
return orderbookData;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param type
|
||||
* @param draft
|
||||
* @param delta
|
||||
* @param resolution
|
||||
* @param modifiedIndex
|
||||
* @returns max (sell) or min (buy) modified index in draft data, mutates draft
|
||||
*/
|
||||
const partiallyUpdateCompactedRows = (
|
||||
dataType: VolumeType,
|
||||
data: OrderbookRowData[],
|
||||
delta: PriceLevelFieldsFragment,
|
||||
resolution: number
|
||||
) => {
|
||||
const { price } = delta;
|
||||
const volume = Number(delta.volume);
|
||||
const priceLevel = getPriceLevel(price, resolution);
|
||||
const isAskDataType = dataType === VolumeType.ask;
|
||||
const volKey = isAskDataType ? 'ask' : 'bid';
|
||||
const volByLevelKey = isAskDataType ? 'askByLevel' : 'bidByLevel';
|
||||
let index = data.findIndex((row) => row.price === priceLevel);
|
||||
if (index !== -1) {
|
||||
data[index][volKey] =
|
||||
data[index][volKey] - (data[index][volByLevelKey][price] || 0) + volume;
|
||||
data[index][volByLevelKey][price] = volume;
|
||||
} else {
|
||||
const newData: OrderbookRowData = createRow(priceLevel, volume, dataType);
|
||||
index = data.findIndex((row) => BigInt(row.price) < BigInt(priceLevel));
|
||||
if (index !== -1) {
|
||||
data.splice(index, 0, newData);
|
||||
} else {
|
||||
data.push(newData);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates OrderbookData[] with new data received from subscription - mutates input
|
||||
*
|
||||
* @param rows
|
||||
* @param sell
|
||||
* @param buy
|
||||
* @param resolution
|
||||
* @returns void
|
||||
*/
|
||||
export const updateCompactedRows = (
|
||||
rows: Readonly<OrderbookRowData[]>,
|
||||
sell: Readonly<PriceLevelFieldsFragment[]> | null,
|
||||
buy: Readonly<PriceLevelFieldsFragment[]> | null,
|
||||
resolution: number
|
||||
) => {
|
||||
const data = cloneDeep(rows as OrderbookRowData[]);
|
||||
uniqBy(reverse(sell || []), 'price')?.forEach((delta) => {
|
||||
partiallyUpdateCompactedRows(VolumeType.ask, data, delta, resolution);
|
||||
});
|
||||
uniqBy(reverse(buy || []), 'price')?.forEach((delta) => {
|
||||
partiallyUpdateCompactedRows(VolumeType.bid, data, delta, resolution);
|
||||
});
|
||||
updateCumulativeVolume(data);
|
||||
let index = 0;
|
||||
// remove levels that do not have any volume
|
||||
while (index < data.length) {
|
||||
if (!data[index].ask && !data[index].bid) {
|
||||
data.splice(index, 1);
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
// count relative volumes
|
||||
updateRelativeData(data);
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates raw data with new data received from subscription - mutates input
|
||||
* @param levels
|
||||
@@ -326,12 +140,9 @@ export interface MockDataGeneratorParams {
|
||||
numberOfSellRows: number;
|
||||
numberOfBuyRows: number;
|
||||
overlap: number;
|
||||
midPrice: number;
|
||||
midPrice?: string;
|
||||
bestStaticBidPrice: number;
|
||||
bestStaticOfferPrice: number;
|
||||
indicativePrice?: number;
|
||||
indicativeVolume?: number;
|
||||
resolution: number;
|
||||
}
|
||||
|
||||
export const generateMockData = ({
|
||||
@@ -341,12 +152,10 @@ export const generateMockData = ({
|
||||
overlap,
|
||||
bestStaticBidPrice,
|
||||
bestStaticOfferPrice,
|
||||
indicativePrice,
|
||||
indicativeVolume,
|
||||
resolution,
|
||||
}: MockDataGeneratorParams) => {
|
||||
let matrix = new Array(numberOfSellRows).fill(undefined);
|
||||
let price = midPrice + (numberOfSellRows - Math.ceil(overlap / 2) + 1);
|
||||
let price =
|
||||
Number(midPrice) + (numberOfSellRows - Math.ceil(overlap / 2) + 1);
|
||||
const sell: PriceLevelFieldsFragment[] = matrix.map((row, i) => ({
|
||||
price: (price -= 1).toString(),
|
||||
volume: (numberOfSellRows - i + 1).toString(),
|
||||
@@ -359,21 +168,11 @@ export const generateMockData = ({
|
||||
volume: (i + 2).toString(),
|
||||
numberOfOrders: '',
|
||||
}));
|
||||
const rows = compactRows(sell, buy, resolution);
|
||||
const marketTradingMode =
|
||||
overlap > 0
|
||||
? Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION
|
||||
: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS;
|
||||
return {
|
||||
rows,
|
||||
resolution,
|
||||
indicativeVolume: indicativeVolume?.toString(),
|
||||
marketTradingMode,
|
||||
midPrice: ((bestStaticBidPrice + bestStaticOfferPrice) / 2).toString(),
|
||||
asks: sell,
|
||||
bids: buy,
|
||||
midPrice,
|
||||
bestStaticBidPrice: bestStaticBidPrice.toString(),
|
||||
bestStaticOfferPrice: bestStaticOfferPrice.toString(),
|
||||
indicativePrice: indicativePrice
|
||||
? getPriceLevel(indicativePrice.toString(), resolution)
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,119 +1,34 @@
|
||||
import throttle from 'lodash/throttle';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { Orderbook } from './orderbook';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
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 type {
|
||||
MarketDepthUpdateSubscription,
|
||||
MarketDepthQuery,
|
||||
MarketDepthQueryVariables,
|
||||
MarketDepthUpdateSubscription,
|
||||
PriceLevelFieldsFragment,
|
||||
} from './__generated__/MarketDepth';
|
||||
import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth';
|
||||
import {
|
||||
compactRows,
|
||||
updateCompactedRows,
|
||||
getMidPrice,
|
||||
getPriceLevel,
|
||||
} from './orderbook-data';
|
||||
import type { OrderbookData } from './orderbook-data';
|
||||
import { useOrderStore } from '@vegaprotocol/orders';
|
||||
|
||||
export type OrderbookData = {
|
||||
asks: PriceLevelFieldsFragment[];
|
||||
bids: PriceLevelFieldsFragment[];
|
||||
};
|
||||
|
||||
interface OrderbookManagerProps {
|
||||
marketId: string;
|
||||
}
|
||||
|
||||
export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
const [resolution, setResolution] = useState(1);
|
||||
const variables = { marketId };
|
||||
const resolutionRef = useRef(resolution);
|
||||
const [orderbookData, setOrderbookData] = useState<OrderbookData>({
|
||||
rows: null,
|
||||
});
|
||||
const dataRef = useRef<OrderbookData>({ rows: null });
|
||||
const marketDataRef = useRef<MarketData | null>(null);
|
||||
const rawDataRef = useRef<MarketDepthQuery['market'] | null>(null);
|
||||
const deltaRef = useRef<{
|
||||
sell: PriceLevelFieldsFragment[];
|
||||
buy: PriceLevelFieldsFragment[];
|
||||
}>({
|
||||
sell: [],
|
||||
buy: [],
|
||||
});
|
||||
const updateOrderbookData = useRef(
|
||||
throttle(() => {
|
||||
dataRef.current = {
|
||||
...marketDataRef.current,
|
||||
indicativePrice:
|
||||
marketDataRef.current?.indicativePrice &&
|
||||
getPriceLevel(
|
||||
marketDataRef.current.indicativePrice,
|
||||
resolutionRef.current
|
||||
),
|
||||
midPrice: getMidPrice(
|
||||
rawDataRef.current?.depth.sell,
|
||||
rawDataRef.current?.depth.buy,
|
||||
resolution
|
||||
),
|
||||
rows:
|
||||
deltaRef.current.buy.length || deltaRef.current.sell.length
|
||||
? updateCompactedRows(
|
||||
dataRef.current.rows ?? [],
|
||||
deltaRef.current.sell,
|
||||
deltaRef.current.buy,
|
||||
resolutionRef.current
|
||||
)
|
||||
: dataRef.current.rows,
|
||||
};
|
||||
deltaRef.current.buy = [];
|
||||
deltaRef.current.sell = [];
|
||||
setOrderbookData(dataRef.current);
|
||||
}, 250)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
deltaRef.current.buy = [];
|
||||
deltaRef.current.sell = [];
|
||||
}, [marketId]);
|
||||
|
||||
const update = useCallback(
|
||||
({
|
||||
delta: deltas,
|
||||
data: rawData,
|
||||
}: {
|
||||
delta?: MarketDepthUpdateSubscription['marketsDepthUpdate'] | null;
|
||||
data: NonNullable<MarketDepthQuery['market']> | null | undefined;
|
||||
}) => {
|
||||
if (!dataRef.current.rows) {
|
||||
return false;
|
||||
}
|
||||
for (const delta of deltas || []) {
|
||||
if (delta.marketId !== marketId) {
|
||||
continue;
|
||||
}
|
||||
if (delta.sell) {
|
||||
deltaRef.current.sell.push(...delta.sell);
|
||||
}
|
||||
if (delta.buy) {
|
||||
deltaRef.current.buy.push(...delta.buy);
|
||||
}
|
||||
rawDataRef.current = rawData;
|
||||
updateOrderbookData.current();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[marketId, updateOrderbookData]
|
||||
);
|
||||
|
||||
const { data, error, loading, flush, reload } = useDataProvider<
|
||||
const { data, error, loading, reload } = useDataProvider<
|
||||
MarketDepthQuery['market'] | undefined,
|
||||
MarketDepthUpdateSubscription['marketsDepthUpdate'] | null,
|
||||
MarketDepthQueryVariables
|
||||
>({
|
||||
dataProvider: marketDepthProvider,
|
||||
update,
|
||||
variables,
|
||||
});
|
||||
|
||||
@@ -127,57 +42,15 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
variables,
|
||||
});
|
||||
|
||||
const marketDataUpdate = useCallback(
|
||||
({ data }: { data: MarketData | null }) => {
|
||||
marketDataRef.current = data;
|
||||
updateOrderbookData.current();
|
||||
return true;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const {
|
||||
data: marketData,
|
||||
error: marketDataError,
|
||||
loading: marketDataLoading,
|
||||
} = useDataProvider({
|
||||
dataProvider: marketDataProvider,
|
||||
update: marketDataUpdate,
|
||||
variables,
|
||||
});
|
||||
|
||||
if (!marketDataRef.current && marketData) {
|
||||
marketDataRef.current = marketData;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const throttleRunner = updateOrderbookData.current;
|
||||
if (!data) {
|
||||
dataRef.current = { rows: null };
|
||||
setOrderbookData(dataRef.current);
|
||||
return;
|
||||
}
|
||||
dataRef.current = {
|
||||
...marketDataRef.current,
|
||||
indicativePrice:
|
||||
marketDataRef.current?.indicativePrice &&
|
||||
getPriceLevel(marketDataRef.current.indicativePrice, resolution),
|
||||
midPrice: getMidPrice(data.depth.sell, data.depth.buy, resolution),
|
||||
rows: compactRows(data.depth.sell, data.depth.buy, resolution),
|
||||
};
|
||||
rawDataRef.current = data;
|
||||
setOrderbookData(dataRef.current);
|
||||
|
||||
return () => {
|
||||
throttleRunner.cancel();
|
||||
};
|
||||
}, [data, resolution]);
|
||||
|
||||
useEffect(() => {
|
||||
resolutionRef.current = resolution;
|
||||
flush();
|
||||
}, [resolution, flush]);
|
||||
|
||||
const updateOrder = useOrderStore((store) => store.update);
|
||||
|
||||
return (
|
||||
@@ -188,16 +61,17 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
reload={reload}
|
||||
>
|
||||
<Orderbook
|
||||
{...orderbookData}
|
||||
bids={data?.depth.buy ?? []}
|
||||
asks={data?.depth.sell ?? []}
|
||||
decimalPlaces={market?.decimalPlaces ?? 0}
|
||||
positionDecimalPlaces={market?.positionDecimalPlaces ?? 0}
|
||||
resolution={resolution}
|
||||
onResolutionChange={(resolution: number) => setResolution(resolution)}
|
||||
assetSymbol={market?.tradableInstrument.instrument.product.quoteName}
|
||||
onClick={(price: string) => {
|
||||
if (price) {
|
||||
updateOrder(marketId, { price });
|
||||
}
|
||||
}}
|
||||
midPrice={marketData?.midPrice}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
|
||||
@@ -1,130 +1,118 @@
|
||||
import React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { addDecimal, addDecimalsFixedFormatNumber } from '@vegaprotocol/utils';
|
||||
import { PriceCell, VolCell, CumulativeVol } from '@vegaprotocol/datagrid';
|
||||
import { NumericCell, PriceCell } from '@vegaprotocol/datagrid';
|
||||
import { VolumeType } from './orderbook-data';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface OrderbookRowProps {
|
||||
ask: number;
|
||||
bid: number;
|
||||
cumulativeAsk?: number;
|
||||
cumulativeBid?: number;
|
||||
cumulativeRelativeAsk?: number;
|
||||
cumulativeRelativeBid?: number;
|
||||
value: number;
|
||||
cumulativeValue?: number;
|
||||
cumulativeRelativeValue?: number;
|
||||
decimalPlaces: number;
|
||||
positionDecimalPlaces: number;
|
||||
indicativeVolume?: string;
|
||||
price: string;
|
||||
relativeAsk?: number;
|
||||
relativeBid?: number;
|
||||
onClick?: (price: string) => void;
|
||||
type: VolumeType;
|
||||
}
|
||||
|
||||
const CumulationBar = ({
|
||||
cumulativeValue = 0,
|
||||
type,
|
||||
}: {
|
||||
cumulativeValue?: number;
|
||||
type: VolumeType;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
data-testid={`${VolumeType.bid === type ? 'bid' : 'ask'}-bar`}
|
||||
className={classNames(
|
||||
'absolute top-0 left-0 h-full transition-all',
|
||||
type === VolumeType.bid
|
||||
? 'bg-vega-green/20 dark:bg-vega-green/50'
|
||||
: 'bg-vega-pink/20 dark:bg-vega-pink/30'
|
||||
)}
|
||||
style={{
|
||||
width: `${cumulativeValue}%`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CumulativeVol = memo(
|
||||
({
|
||||
testId,
|
||||
positionDecimalPlaces,
|
||||
cumulativeValue,
|
||||
}: {
|
||||
ask?: number;
|
||||
bid?: number;
|
||||
cumulativeValue?: number;
|
||||
testId?: string;
|
||||
className?: string;
|
||||
positionDecimalPlaces: number;
|
||||
}) => {
|
||||
const volume = cumulativeValue ? (
|
||||
<NumericCell
|
||||
value={cumulativeValue}
|
||||
valueFormatted={addDecimalsFixedFormatNumber(
|
||||
cumulativeValue,
|
||||
positionDecimalPlaces ?? 0
|
||||
)}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="pr-1" data-testid={testId}>
|
||||
{volume}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
CumulativeVol.displayName = 'OrderBookCumulativeVol';
|
||||
|
||||
export const OrderbookRow = React.memo(
|
||||
({
|
||||
ask,
|
||||
bid,
|
||||
cumulativeAsk,
|
||||
cumulativeBid,
|
||||
cumulativeRelativeAsk,
|
||||
cumulativeRelativeBid,
|
||||
value,
|
||||
cumulativeValue,
|
||||
cumulativeRelativeValue,
|
||||
decimalPlaces,
|
||||
positionDecimalPlaces,
|
||||
indicativeVolume,
|
||||
price,
|
||||
relativeAsk,
|
||||
relativeBid,
|
||||
onClick,
|
||||
type,
|
||||
}: OrderbookRowProps) => {
|
||||
const txtId = type === VolumeType.bid ? 'bid' : 'ask';
|
||||
return (
|
||||
<>
|
||||
<VolCell
|
||||
testId={`bid-vol-${price}`}
|
||||
value={bid}
|
||||
valueFormatted={addDecimalsFixedFormatNumber(
|
||||
bid,
|
||||
positionDecimalPlaces
|
||||
)}
|
||||
relativeValue={relativeBid}
|
||||
type="bid"
|
||||
/>
|
||||
<VolCell
|
||||
testId={`ask-vol-${price}`}
|
||||
value={ask}
|
||||
valueFormatted={addDecimalsFixedFormatNumber(
|
||||
ask,
|
||||
positionDecimalPlaces
|
||||
)}
|
||||
relativeValue={relativeAsk}
|
||||
type="ask"
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
<div className="relative">
|
||||
<CumulationBar cumulativeValue={cumulativeRelativeValue} type={type} />
|
||||
<div className="grid gap-1 text-right grid-cols-3">
|
||||
<PriceCell
|
||||
testId={`price-${price}`}
|
||||
value={BigInt(price)}
|
||||
onClick={() => onClick && onClick(addDecimal(price, decimalPlaces))}
|
||||
valueFormatted={addDecimalsFixedFormatNumber(price, decimalPlaces)}
|
||||
className={
|
||||
type === VolumeType.ask
|
||||
? '!text-vega-pink dark:text-vega-pink'
|
||||
: 'text-vega-green-550 dark:text-vega-green'
|
||||
}
|
||||
/>
|
||||
<NumericCell
|
||||
testId={`${txtId}-vol-${price}`}
|
||||
value={value}
|
||||
valueFormatted={addDecimalsFixedFormatNumber(
|
||||
value,
|
||||
positionDecimalPlaces
|
||||
)}
|
||||
/>
|
||||
<CumulativeVol
|
||||
testId={`cumulative-vol-${price}`}
|
||||
positionDecimalPlaces={positionDecimalPlaces}
|
||||
cumulativeValue={cumulativeValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
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';
|
||||
|
||||
@@ -1,260 +1,91 @@
|
||||
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
|
||||
import { generateMockData } from './orderbook-data';
|
||||
import { Orderbook, rowHeight } from './orderbook';
|
||||
import { generateMockData, VolumeType } from './orderbook-data';
|
||||
import { Orderbook } from './orderbook';
|
||||
import * as orderbookData from './orderbook-data';
|
||||
|
||||
function mockOffsetSize(width: number, height: number) {
|
||||
Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', {
|
||||
configurable: true,
|
||||
value: () => ({ height, width }),
|
||||
});
|
||||
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
|
||||
configurable: true,
|
||||
value: height,
|
||||
});
|
||||
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
||||
configurable: true,
|
||||
value: width,
|
||||
});
|
||||
}
|
||||
|
||||
describe('Orderbook', () => {
|
||||
const params = {
|
||||
numberOfSellRows: 100,
|
||||
numberOfBuyRows: 100,
|
||||
step: 1,
|
||||
midPrice: 122900,
|
||||
midPrice: '122900',
|
||||
bestStaticBidPrice: 122905,
|
||||
bestStaticOfferPrice: 122895,
|
||||
decimalPlaces: 3,
|
||||
overlap: 10,
|
||||
indicativePrice: 122900,
|
||||
indicativeVolume: 11,
|
||||
overlap: 0,
|
||||
resolution: 1,
|
||||
};
|
||||
const onResolutionChange = jest.fn();
|
||||
const decimalPlaces = 3;
|
||||
it('should scroll to mid price on init', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
|
||||
beforeEach(() => {
|
||||
mockOffsetSize(800, 768);
|
||||
});
|
||||
it('markPrice should be in the middle', async () => {
|
||||
render(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={0}
|
||||
fillGaps
|
||||
{...generateMockData(params)}
|
||||
onResolutionChange={onResolutionChange}
|
||||
assetSymbol="USD"
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
|
||||
});
|
||||
|
||||
it('should keep mid price row in the middle', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
const result = render(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={0}
|
||||
fillGaps
|
||||
{...generateMockData(params)}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
await waitFor(() =>
|
||||
screen.getByTestId(`middle-mark-price-${params.midPrice}`)
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
|
||||
result.rerender(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={0}
|
||||
fillGaps
|
||||
{...generateMockData({
|
||||
...params,
|
||||
numberOfSellRows: params.numberOfSellRows - 1,
|
||||
})}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(90 * rowHeight);
|
||||
});
|
||||
|
||||
it('should scroll to mid price when it will change', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
const result = render(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={0}
|
||||
fillGaps
|
||||
{...generateMockData(params)}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
|
||||
result.rerender(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={0}
|
||||
fillGaps
|
||||
{...generateMockData({
|
||||
...params,
|
||||
bestStaticBidPrice: params.bestStaticBidPrice + 1,
|
||||
bestStaticOfferPrice: params.bestStaticOfferPrice + 1,
|
||||
})}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(90 * rowHeight);
|
||||
});
|
||||
|
||||
it('should keep price it the middle', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
const result = render(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={0}
|
||||
fillGaps
|
||||
{...generateMockData(params)}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
const scrollElement = result.getByTestId('scroll');
|
||||
expect(scrollElement.scrollTop).toBe(91 * rowHeight);
|
||||
scrollElement.scrollTop = 92 * rowHeight + 0.01;
|
||||
fireEvent.scroll(scrollElement);
|
||||
result.rerender(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={0}
|
||||
fillGaps
|
||||
{...generateMockData({
|
||||
...params,
|
||||
numberOfSellRows: params.numberOfSellRows - 1,
|
||||
})}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 0.01);
|
||||
});
|
||||
|
||||
it('should get back to mid price on click', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
const result = render(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={0}
|
||||
fillGaps
|
||||
{...generateMockData(params)}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
const scrollElement = result.getByTestId('scroll');
|
||||
expect(scrollElement.scrollTop).toBe(91 * rowHeight);
|
||||
scrollElement.scrollTop = 1;
|
||||
fireEvent.scroll(scrollElement);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(1);
|
||||
const scrollToMidPriceButton = result.getByTestId('scroll-to-midprice');
|
||||
fireEvent.click(scrollToMidPriceButton);
|
||||
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 1);
|
||||
});
|
||||
|
||||
it('should get back to mid price on resolution change', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
const result = render(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={0}
|
||||
fillGaps
|
||||
{...generateMockData(params)}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
const scrollElement = screen.getByTestId('scroll');
|
||||
expect(scrollElement.scrollTop).toBe(91 * rowHeight);
|
||||
scrollElement.scrollTop = 1;
|
||||
fireEvent.scroll(scrollElement);
|
||||
expect(screen.getByTestId('scroll').scrollTop).toBe(1);
|
||||
const resolutionSelect = screen.getByTestId(
|
||||
'resolution'
|
||||
) as HTMLSelectElement;
|
||||
fireEvent.change(resolutionSelect, { target: { value: '10' } });
|
||||
expect(onResolutionChange.mock.calls.length).toBe(1);
|
||||
expect(onResolutionChange.mock.calls[0][0]).toBe(10);
|
||||
result.rerender(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={0}
|
||||
fillGaps
|
||||
{...generateMockData({
|
||||
...params,
|
||||
resolution: 10,
|
||||
})}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('scroll').scrollTop).toBe(6 * rowHeight);
|
||||
expect(
|
||||
screen.getByTestId(`middle-mark-price-${params.midPrice}`)
|
||||
).toHaveTextContent('122.90');
|
||||
});
|
||||
|
||||
it('should format correctly the numbers on resolution change', async () => {
|
||||
const onClickSpy = jest.fn();
|
||||
const result = render(
|
||||
jest.spyOn(orderbookData, 'compactRows');
|
||||
const mockedData = generateMockData(params);
|
||||
render(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={0}
|
||||
onClick={onClickSpy}
|
||||
fillGaps
|
||||
{...generateMockData(params)}
|
||||
onResolutionChange={onResolutionChange}
|
||||
{...mockedData}
|
||||
assetSymbol="USD"
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
await screen.findByTestId(`bid-vol-${params.midPrice}`)
|
||||
await screen.findByTestId(`middle-mark-price-${params.midPrice}`)
|
||||
).toBeInTheDocument();
|
||||
// Before resolution change the price is 122.934
|
||||
await fireEvent.click(await screen.getByTestId('price-122934'));
|
||||
expect(onClickSpy).toBeCalledWith('122.934');
|
||||
await fireEvent.click(await screen.getByTestId('price-122901'));
|
||||
expect(onClickSpy).toBeCalledWith('122.901');
|
||||
const resolutionSelect = screen.getByTestId(
|
||||
'resolution'
|
||||
) as HTMLSelectElement;
|
||||
await fireEvent.change(resolutionSelect, { target: { value: '10' } });
|
||||
await result.rerender(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={0}
|
||||
onClick={onClickSpy}
|
||||
fillGaps
|
||||
{...generateMockData({
|
||||
...params,
|
||||
resolution: 10,
|
||||
})}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
expect(orderbookData.compactRows).toHaveBeenCalledWith(
|
||||
mockedData.bids,
|
||||
VolumeType.bid,
|
||||
10
|
||||
);
|
||||
await fireEvent.click(await screen.getByTestId('price-12299'));
|
||||
// After resolution change the price is 122.99
|
||||
expect(onResolutionChange.mock.calls[0][0]).toBe(10);
|
||||
expect(onClickSpy).toBeCalledWith('122.99');
|
||||
});
|
||||
|
||||
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}
|
||||
/>
|
||||
expect(orderbookData.compactRows).toHaveBeenCalledWith(
|
||||
mockedData.asks,
|
||||
VolumeType.ask,
|
||||
10
|
||||
);
|
||||
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();
|
||||
await fireEvent.click(await screen.getByTestId('price-12294'));
|
||||
expect(onClickSpy).toBeCalledWith('122.94');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,14 +2,12 @@ import type { Story, Meta } from '@storybook/react';
|
||||
import { generateMockData } from './orderbook-data';
|
||||
import type { MockDataGeneratorParams } from './orderbook-data';
|
||||
import { Orderbook } from './orderbook';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Props = Omit<MockDataGeneratorParams, 'resolution'> & {
|
||||
decimalPlaces: number;
|
||||
};
|
||||
|
||||
const OrderbookMockDataProvider = ({ decimalPlaces, ...props }: Props) => {
|
||||
const [resolution, setResolution] = useState(1);
|
||||
return (
|
||||
<div className="absolute inset-0 dark:bg-black dark:text-neutral-200 bg-white text-neutral-800">
|
||||
<div
|
||||
@@ -18,9 +16,9 @@ const OrderbookMockDataProvider = ({ decimalPlaces, ...props }: Props) => {
|
||||
>
|
||||
<Orderbook
|
||||
positionDecimalPlaces={0}
|
||||
onResolutionChange={setResolution}
|
||||
decimalPlaces={decimalPlaces}
|
||||
{...generateMockData({ ...props, resolution })}
|
||||
{...generateMockData({ ...props })}
|
||||
assetSymbol="USD"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,8 +52,6 @@ Auction.args = {
|
||||
bestStaticOfferPrice: 122895,
|
||||
decimalPlaces: 3,
|
||||
overlap: 10,
|
||||
indicativePrice: 122900,
|
||||
indicativeVolume: 11,
|
||||
};
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
@@ -66,6 +62,4 @@ Empty.args = {
|
||||
bestStaticOfferPrice: 0,
|
||||
decimalPlaces: 3,
|
||||
overlap: 0,
|
||||
indicativePrice: 0,
|
||||
indicativeVolume: 0,
|
||||
};
|
||||
|
||||
@@ -1,680 +1,190 @@
|
||||
import colors from 'tailwindcss/colors';
|
||||
import { useMemo } from 'react';
|
||||
import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer';
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
Fragment,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
addDecimalsFixedFormatNumber,
|
||||
addDecimalsFormatNumber,
|
||||
formatNumberFixed,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
useResizeObserver,
|
||||
useThemeSwitcher,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { OrderbookRow, OrderbookContinuousRow } from './orderbook-row';
|
||||
import { createRow } from './orderbook-data';
|
||||
import { Checkbox, Icon, Splash, TinyScroll } from '@vegaprotocol/ui-toolkit';
|
||||
import type { OrderbookData, OrderbookRowData } from './orderbook-data';
|
||||
import { OrderbookRow } from './orderbook-row';
|
||||
import type { OrderbookRowData } from './orderbook-data';
|
||||
import { compactRows, VolumeType } from './orderbook-data';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth';
|
||||
|
||||
interface OrderbookProps extends OrderbookData {
|
||||
decimalPlaces: number;
|
||||
positionDecimalPlaces: number;
|
||||
resolution: number;
|
||||
onResolutionChange: (resolution: number) => void;
|
||||
onClick?: (price: string) => void;
|
||||
fillGaps?: boolean;
|
||||
}
|
||||
// Sets row height, will be used to calculate number of rows that can be
|
||||
// displayed each side of the book without overflow
|
||||
export const rowHeight = 17;
|
||||
const rowGap = 1;
|
||||
const midHeight = 30;
|
||||
|
||||
const HorizontalLine = ({ top, testId }: { top: string; testId: string }) => (
|
||||
<div
|
||||
className="absolute border-b border-default inset-x-0 hidden"
|
||||
style={{ top }}
|
||||
data-testid={testId}
|
||||
/>
|
||||
);
|
||||
|
||||
const getNumberOfRows = (
|
||||
rows: OrderbookRowData[] | null,
|
||||
resolution: number
|
||||
) => {
|
||||
if (!rows || !rows.length) {
|
||||
return 0;
|
||||
}
|
||||
if (rows.length === 1) {
|
||||
return 1;
|
||||
}
|
||||
return (
|
||||
Number(BigInt(rows[0].price) - BigInt(rows[rows.length - 1].price)) /
|
||||
resolution +
|
||||
1
|
||||
);
|
||||
};
|
||||
|
||||
const getRowsToRender = (
|
||||
rows: OrderbookRowData[] | null,
|
||||
resolution: number,
|
||||
offset: number,
|
||||
limit: number
|
||||
): OrderbookRowData[] | null => {
|
||||
if (!rows || !rows.length) {
|
||||
return rows;
|
||||
}
|
||||
if (rows.length === 1) {
|
||||
return rows;
|
||||
}
|
||||
const selectedRows: OrderbookRowData[] = [];
|
||||
let price = BigInt(rows[0].price) - BigInt(offset * resolution);
|
||||
let index = Math.max(
|
||||
rows.findIndex((row) => BigInt(row.price) <= price) - 1,
|
||||
-1
|
||||
);
|
||||
while (selectedRows.length < limit && index + 1 < rows.length) {
|
||||
if (rows[index + 1].price === price.toString()) {
|
||||
selectedRows.push(rows[index + 1]);
|
||||
index += 1;
|
||||
} else {
|
||||
const row = createRow(price.toString());
|
||||
row.cumulativeVol = {
|
||||
bid: rows[index].cumulativeVol.bid,
|
||||
relativeBid: rows[index].cumulativeVol.relativeBid,
|
||||
ask: rows[index + 1].cumulativeVol.ask,
|
||||
relativeAsk: rows[index + 1].cumulativeVol.relativeAsk,
|
||||
};
|
||||
selectedRows.push(row);
|
||||
}
|
||||
price -= BigInt(resolution);
|
||||
}
|
||||
return selectedRows;
|
||||
};
|
||||
|
||||
// 17px of row height plus 4px gap
|
||||
export const gridGap = 4;
|
||||
export const rowHeight = 21;
|
||||
// top padding to make space for header
|
||||
const headerPadding = 30;
|
||||
// bottom padding to make space for footer
|
||||
const footerPadding = 25;
|
||||
// buffer size in rows
|
||||
const bufferSize = 30;
|
||||
// margin size in px, when reached scrollOffset will be updated
|
||||
const marginSize = bufferSize * 0.9 * rowHeight;
|
||||
|
||||
const getBestStaticBidPriceLinePosition = (
|
||||
bestStaticBidPrice: string | undefined,
|
||||
fillGaps: boolean,
|
||||
maxPriceLevel: string,
|
||||
minPriceLevel: string,
|
||||
resolution: number,
|
||||
rows: OrderbookRowData[] | null
|
||||
) => {
|
||||
let bestStaticBidPriceLinePosition = '';
|
||||
if (
|
||||
rows?.length &&
|
||||
bestStaticBidPrice &&
|
||||
BigInt(bestStaticBidPrice) < BigInt(maxPriceLevel) &&
|
||||
BigInt(bestStaticBidPrice) > BigInt(minPriceLevel)
|
||||
) {
|
||||
if (fillGaps) {
|
||||
bestStaticBidPriceLinePosition = (
|
||||
((BigInt(maxPriceLevel) - BigInt(bestStaticBidPrice)) /
|
||||
BigInt(resolution)) *
|
||||
BigInt(rowHeight) +
|
||||
BigInt(headerPadding) -
|
||||
BigInt(3)
|
||||
).toString();
|
||||
} else {
|
||||
const index = rows?.findIndex(
|
||||
(row) => BigInt(row.price) <= BigInt(bestStaticBidPrice)
|
||||
);
|
||||
if (index !== undefined && index !== -1) {
|
||||
bestStaticBidPriceLinePosition = (
|
||||
index * rowHeight +
|
||||
headerPadding -
|
||||
3
|
||||
).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestStaticBidPriceLinePosition;
|
||||
};
|
||||
const getBestStaticOfferPriceLinePosition = (
|
||||
bestStaticOfferPrice: string | undefined,
|
||||
fillGaps: boolean,
|
||||
maxPriceLevel: string,
|
||||
minPriceLevel: string,
|
||||
resolution: number,
|
||||
rows: OrderbookRowData[] | null
|
||||
) => {
|
||||
let bestStaticOfferPriceLinePosition = '';
|
||||
if (
|
||||
rows?.length &&
|
||||
bestStaticOfferPrice &&
|
||||
BigInt(bestStaticOfferPrice) <= BigInt(maxPriceLevel) &&
|
||||
BigInt(bestStaticOfferPrice) > BigInt(minPriceLevel)
|
||||
) {
|
||||
if (fillGaps) {
|
||||
bestStaticOfferPriceLinePosition = (
|
||||
((BigInt(maxPriceLevel) - BigInt(bestStaticOfferPrice)) /
|
||||
BigInt(resolution) +
|
||||
BigInt(1)) *
|
||||
BigInt(rowHeight) +
|
||||
BigInt(headerPadding) -
|
||||
BigInt(3)
|
||||
).toString();
|
||||
} else {
|
||||
const index = rows?.findIndex(
|
||||
(row) => BigInt(row.price) <= BigInt(bestStaticOfferPrice)
|
||||
);
|
||||
if (index !== undefined && index !== -1) {
|
||||
bestStaticOfferPriceLinePosition = (
|
||||
(index + 1) * rowHeight +
|
||||
headerPadding -
|
||||
3
|
||||
).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestStaticOfferPriceLinePosition;
|
||||
};
|
||||
const OrderbookDebugInfo = ({
|
||||
decimalPlaces,
|
||||
numberOfRows,
|
||||
viewportHeight,
|
||||
lockOnMidPrice,
|
||||
priceInCenter,
|
||||
bestStaticBidPrice,
|
||||
bestStaticOfferPrice,
|
||||
maxPriceLevel,
|
||||
minPriceLevel,
|
||||
midPrice,
|
||||
}: {
|
||||
decimalPlaces: number;
|
||||
numberOfRows: number;
|
||||
viewportHeight: number;
|
||||
lockOnMidPrice: boolean;
|
||||
priceInCenter?: string;
|
||||
bestStaticBidPrice?: string;
|
||||
bestStaticOfferPrice?: string;
|
||||
maxPriceLevel: string;
|
||||
minPriceLevel: string;
|
||||
midPrice?: string;
|
||||
}) => (
|
||||
<Fragment>
|
||||
<div className="absolute top-1/2 left-0 border-t border-t-black w-full" />
|
||||
<div className="text-xs p-2 bg-black/80 text-white absolute left-0 bottom-6 font-mono">
|
||||
<pre>
|
||||
{JSON.stringify(
|
||||
{
|
||||
numberOfRows,
|
||||
viewportHeight,
|
||||
lockOnMidPrice,
|
||||
priceInCenter: priceInCenter
|
||||
? addDecimalsFixedFormatNumber(priceInCenter, decimalPlaces)
|
||||
: '-',
|
||||
maxPriceLevel: addDecimalsFixedFormatNumber(
|
||||
maxPriceLevel ?? '0',
|
||||
decimalPlaces
|
||||
),
|
||||
bestStaticBidPrice: addDecimalsFixedFormatNumber(
|
||||
bestStaticBidPrice ?? '0',
|
||||
decimalPlaces
|
||||
),
|
||||
bestStaticOfferPrice: addDecimalsFixedFormatNumber(
|
||||
bestStaticOfferPrice ?? '0',
|
||||
decimalPlaces
|
||||
),
|
||||
minPriceLevel: addDecimalsFixedFormatNumber(
|
||||
minPriceLevel ?? '0',
|
||||
decimalPlaces
|
||||
),
|
||||
midPrice: addDecimalsFixedFormatNumber(
|
||||
midPrice ?? '0',
|
||||
decimalPlaces
|
||||
),
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export const Orderbook = ({
|
||||
const OrderbookTable = ({
|
||||
rows,
|
||||
midPrice,
|
||||
bestStaticBidPrice,
|
||||
bestStaticOfferPrice,
|
||||
marketTradingMode,
|
||||
indicativeVolume,
|
||||
indicativePrice,
|
||||
resolution,
|
||||
type,
|
||||
decimalPlaces,
|
||||
positionDecimalPlaces,
|
||||
resolution,
|
||||
fillGaps: initialFillGaps,
|
||||
onResolutionChange,
|
||||
onClick,
|
||||
}: OrderbookProps) => {
|
||||
const { theme } = useThemeSwitcher();
|
||||
const scrollElement = useRef<HTMLDivElement>(null);
|
||||
const rootElement = useRef<HTMLDivElement>(null);
|
||||
const gridElement = useRef<HTMLDivElement>(null);
|
||||
const headerElement = useRef<HTMLDivElement>(null);
|
||||
const footerElement = useRef<HTMLDivElement>(null);
|
||||
// scroll offset for which rendered rows are selected, will change after user will scroll to margin of rendered data
|
||||
const [scrollOffset, setScrollOffset] = useState(0);
|
||||
// actual scrollTop of scrollElement current element
|
||||
const scrollTopRef = useRef(0);
|
||||
// price level which is rendered in center of viewport, need to preserve price level when rows will be added or removed
|
||||
// if undefined then we render mid price in center
|
||||
const priceInCenter = useRef<string>();
|
||||
// by default mid price is rendered in center - view locked on mid price
|
||||
const [lockOnMidPrice, setLockOnMidPrice] = useState(true);
|
||||
const resolutionRef = useRef(resolution);
|
||||
const [viewportHeight, setViewportHeight] = useState(window.innerHeight);
|
||||
// show price levels with no orders, can lead to enormous number of rows
|
||||
const [fillGaps, setFillGaps] = useState(!!initialFillGaps);
|
||||
const [debug, setDebug] = useState(false);
|
||||
|
||||
const numberOfRows = fillGaps
|
||||
? getNumberOfRows(rows, resolution)
|
||||
: rows?.length ?? 0;
|
||||
const maxPriceLevel = rows?.[0]?.price ?? '0';
|
||||
const minPriceLevel = rows?.[rows.length - 1]?.price ?? '0';
|
||||
|
||||
let offset = Math.max(0, Math.round(scrollOffset / rowHeight));
|
||||
const prependingBufferSize = Math.min(bufferSize, offset);
|
||||
offset -= prependingBufferSize;
|
||||
const viewportSize = Math.round(viewportHeight / rowHeight);
|
||||
const limit = Math.min(
|
||||
prependingBufferSize + viewportSize + bufferSize,
|
||||
numberOfRows - offset
|
||||
);
|
||||
const data = fillGaps
|
||||
? getRowsToRender(rows, resolution, offset, limit)
|
||||
: rows?.slice(offset, offset + limit) ?? [];
|
||||
|
||||
const paddingTop = offset * rowHeight + headerPadding;
|
||||
const paddingBottom =
|
||||
(numberOfRows - offset - limit) * rowHeight + footerPadding;
|
||||
|
||||
const updateScrollOffset = useCallback(
|
||||
(scrollTop: number) => {
|
||||
if (Math.abs(scrollOffset - scrollTop) > marginSize) {
|
||||
setScrollOffset(scrollTop);
|
||||
}
|
||||
},
|
||||
[scrollOffset]
|
||||
);
|
||||
|
||||
const onScroll = useCallback(
|
||||
(event: React.UIEvent<HTMLDivElement>) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
|
||||
updateScrollOffset(scrollTop);
|
||||
if (scrollTop === scrollTopRef.current) {
|
||||
return;
|
||||
} else if ((scrollTop - scrollTopRef.current) % rowHeight === 0) {
|
||||
if (scrollElement.current) {
|
||||
scrollElement.current.scrollTop = scrollTopRef.current;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (scrollTop === 0 || scrollHeight === clientHeight + scrollTop) {
|
||||
priceInCenter.current = undefined;
|
||||
} else {
|
||||
// top offset in rows to row in the middle
|
||||
const offsetTop = Math.floor(
|
||||
(scrollTop +
|
||||
Math.floor((viewportHeight - footerPadding - headerPadding) / 2)) /
|
||||
rowHeight
|
||||
);
|
||||
priceInCenter.current = fillGaps
|
||||
? (
|
||||
BigInt(maxPriceLevel) -
|
||||
BigInt(offsetTop) * BigInt(resolution)
|
||||
).toString()
|
||||
: rows?.[Math.min(offsetTop, rows.length - 1)].price.toString();
|
||||
}
|
||||
if (lockOnMidPrice) {
|
||||
setLockOnMidPrice(false);
|
||||
}
|
||||
scrollTopRef.current = scrollTop;
|
||||
},
|
||||
[
|
||||
resolution,
|
||||
lockOnMidPrice,
|
||||
maxPriceLevel,
|
||||
viewportHeight,
|
||||
updateScrollOffset,
|
||||
fillGaps,
|
||||
rows,
|
||||
]
|
||||
);
|
||||
|
||||
const scrollToPrice = useCallback(
|
||||
(price: string) => {
|
||||
if (scrollElement.current && maxPriceLevel !== '0') {
|
||||
let scrollTop = 0;
|
||||
if (fillGaps) {
|
||||
scrollTop =
|
||||
// distance in rows between given price and first row price * row Height
|
||||
(Number(
|
||||
(BigInt(maxPriceLevel) - BigInt(price)) / BigInt(resolution)
|
||||
) +
|
||||
1) *
|
||||
rowHeight;
|
||||
} else if (rows) {
|
||||
const index = rows.findIndex(
|
||||
(row) => BigInt(row.price) <= BigInt(price)
|
||||
);
|
||||
if (index !== -1) {
|
||||
scrollTop = rowHeight * (index + 1);
|
||||
if (index !== 0) {
|
||||
const diffToCurrentRow =
|
||||
BigInt(price) - BigInt(rows[index].price);
|
||||
const diffToPreviousRow =
|
||||
BigInt(rows[index - 1].price) - BigInt(price);
|
||||
if (diffToPreviousRow < diffToCurrentRow) {
|
||||
scrollTop -= rowHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// minus half height of viewport plus half of row
|
||||
scrollTop -= Math.ceil((viewportHeight - rowHeight) / 2);
|
||||
// adjust to current rows position
|
||||
scrollTop +=
|
||||
(scrollTopRef.current % rowHeight) - (scrollTop % rowHeight);
|
||||
const priceCenterScrollOffset = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
scrollTop,
|
||||
numberOfRows * rowHeight +
|
||||
headerPadding +
|
||||
footerPadding +
|
||||
-viewportHeight -
|
||||
gridGap
|
||||
)
|
||||
);
|
||||
if (scrollTopRef.current !== priceCenterScrollOffset) {
|
||||
updateScrollOffset(priceCenterScrollOffset);
|
||||
scrollTopRef.current = priceCenterScrollOffset;
|
||||
scrollElement.current.scrollTop = priceCenterScrollOffset;
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
maxPriceLevel,
|
||||
resolution,
|
||||
viewportHeight,
|
||||
numberOfRows,
|
||||
updateScrollOffset,
|
||||
fillGaps,
|
||||
rows,
|
||||
]
|
||||
);
|
||||
|
||||
const scrollToMidPrice = useCallback(() => {
|
||||
if (!midPrice) {
|
||||
return;
|
||||
}
|
||||
priceInCenter.current = undefined;
|
||||
scrollToPrice(midPrice);
|
||||
setLockOnMidPrice(true);
|
||||
}, [midPrice, scrollToPrice]);
|
||||
|
||||
// adjust scroll position to keep selected price in center
|
||||
useEffect(() => {
|
||||
if (priceInCenter.current) {
|
||||
scrollToPrice(priceInCenter.current);
|
||||
} else if (lockOnMidPrice && midPrice) {
|
||||
scrollToPrice(midPrice);
|
||||
}
|
||||
}, [midPrice, scrollToPrice, lockOnMidPrice]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resolutionRef.current !== resolution) {
|
||||
priceInCenter.current = undefined;
|
||||
resolutionRef.current = resolution;
|
||||
setLockOnMidPrice(true);
|
||||
}
|
||||
}, [resolution]);
|
||||
|
||||
// handles resizing of the Allotment.Pane (x-axis)
|
||||
// adjusts the header and footer width
|
||||
const gridResizeHandler: ResizeObserverCallback = useCallback(
|
||||
(entries) => {
|
||||
if (
|
||||
!headerElement.current ||
|
||||
!footerElement.current ||
|
||||
entries.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
contentRect: { width },
|
||||
} = entries[0];
|
||||
headerElement.current.style.width = `${width}px`;
|
||||
footerElement.current.style.width = `${width}px`;
|
||||
},
|
||||
[headerElement, footerElement]
|
||||
);
|
||||
// handles resizing of the Allotment.Pane (y-axis)
|
||||
// adjusts the scroll height
|
||||
const rootElementResizeHandler: ResizeObserverCallback = useCallback(
|
||||
(entries) => {
|
||||
if (!rootElement.current || entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
setViewportHeight(entries[0].contentRect.height);
|
||||
},
|
||||
[setViewportHeight, rootElement]
|
||||
);
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}, [isContinuousMode]);
|
||||
|
||||
const OrderBookRowComponent = isContinuousMode
|
||||
? OrderbookContinuousRow
|
||||
: OrderbookRow;
|
||||
|
||||
const tableBody = data?.length ? (
|
||||
}: {
|
||||
rows: OrderbookRowData[];
|
||||
resolution: number;
|
||||
decimalPlaces: number;
|
||||
positionDecimalPlaces: number;
|
||||
type: VolumeType;
|
||||
onClick?: (price: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'grid grid-cols-4 gap-1 text-right auto-rows-[17px]',
|
||||
isContinuousMode ? 'grid-cols-3' : 'grid-cols-4'
|
||||
)}
|
||||
className={
|
||||
// position the ask side to the bottow of the top section and the bid side to the top of the bottom section
|
||||
classNames(
|
||||
'flex flex-col',
|
||||
type === VolumeType.ask ? 'justify-end' : 'justify-start'
|
||||
)
|
||||
}
|
||||
>
|
||||
{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
|
||||
className="grid"
|
||||
style={{ gridAutoRows: rowHeight, gap: rowGap }} // use style as tailwind won't compile the dynamically set height
|
||||
>
|
||||
{rows.map((data) => (
|
||||
<OrderbookRow
|
||||
key={data.price}
|
||||
price={(BigInt(data.price) / BigInt(resolution)).toString()}
|
||||
onClick={onClick}
|
||||
decimalPlaces={decimalPlaces - Math.log10(resolution)}
|
||||
positionDecimalPlaces={positionDecimalPlaces}
|
||||
value={data.value}
|
||||
cumulativeValue={data.cumulativeVol.value}
|
||||
cumulativeRelativeValue={data.cumulativeVol.relativeValue}
|
||||
type={type}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : 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`;
|
||||
interface OrderbookProps {
|
||||
decimalPlaces: number;
|
||||
positionDecimalPlaces: number;
|
||||
onClick?: (price: string) => void;
|
||||
midPrice?: string;
|
||||
bids: PriceLevelFieldsFragment[];
|
||||
asks: PriceLevelFieldsFragment[];
|
||||
assetSymbol: string | undefined;
|
||||
}
|
||||
|
||||
const resolutions = new Array(decimalPlaces + 1)
|
||||
export const Orderbook = ({
|
||||
decimalPlaces,
|
||||
positionDecimalPlaces,
|
||||
onClick,
|
||||
midPrice,
|
||||
asks,
|
||||
bids,
|
||||
assetSymbol,
|
||||
}: OrderbookProps) => {
|
||||
const [resolution, setResolution] = useState(1);
|
||||
const resolutions = new Array(
|
||||
Math.max(midPrice?.toString().length ?? 0, decimalPlaces + 1)
|
||||
)
|
||||
.fill(null)
|
||||
.map((v, i) => Math.pow(10, i));
|
||||
|
||||
const bestStaticBidPriceLinePosition = getBestStaticBidPriceLinePosition(
|
||||
bestStaticBidPrice,
|
||||
fillGaps,
|
||||
maxPriceLevel,
|
||||
minPriceLevel,
|
||||
resolution,
|
||||
rows
|
||||
);
|
||||
const groupedAsks = useMemo(() => {
|
||||
return compactRows(asks, VolumeType.ask, resolution);
|
||||
}, [asks, resolution]);
|
||||
|
||||
const bestStaticOfferPriceLinePosition = getBestStaticOfferPriceLinePosition(
|
||||
bestStaticOfferPrice,
|
||||
fillGaps,
|
||||
maxPriceLevel,
|
||||
minPriceLevel,
|
||||
resolution,
|
||||
rows
|
||||
);
|
||||
const groupedBids = useMemo(() => {
|
||||
return compactRows(bids, VolumeType.bid, resolution);
|
||||
}, [bids, resolution]);
|
||||
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
return (
|
||||
<div
|
||||
className="h-full relative pl-1 text-xs"
|
||||
ref={rootElement}
|
||||
onDoubleClick={() => setDebug(!debug)}
|
||||
>
|
||||
{tableHeader}
|
||||
<TinyScroll
|
||||
className="h-full overflow-auto relative"
|
||||
onScroll={onScroll}
|
||||
ref={scrollElement}
|
||||
data-testid="scroll"
|
||||
>
|
||||
<div
|
||||
className="relative text-right min-h-full overflow-hidden"
|
||||
style={{
|
||||
paddingTop,
|
||||
paddingBottom,
|
||||
background: tableBody ? gradientStyles : 'none',
|
||||
<div className="h-full pl-1 text-xs grid grid-rows-[1fr_min-content]">
|
||||
<div>
|
||||
<ReactVirtualizedAutoSizer disableWidth>
|
||||
{({ height }) => {
|
||||
const limit = Math.max(
|
||||
1,
|
||||
Math.floor((height - midHeight) / 2 / (rowHeight + rowGap))
|
||||
);
|
||||
const askRows = groupedAsks?.slice(limit * -1) ?? [];
|
||||
const bidRows = groupedBids?.slice(0, limit) ?? [];
|
||||
return (
|
||||
<div
|
||||
className="overflow-hidden grid"
|
||||
data-testid="orderbook-grid-element"
|
||||
style={{
|
||||
height: height + 'px',
|
||||
gridTemplateRows: `1fr ${midHeight}px 1fr`, // cannot use tailwind here as tailwind will not parse a class string with interpolation
|
||||
}}
|
||||
>
|
||||
{askRows.length || bidRows.length ? (
|
||||
<>
|
||||
<OrderbookTable
|
||||
rows={askRows}
|
||||
type={VolumeType.ask}
|
||||
resolution={resolution}
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={positionDecimalPlaces}
|
||||
onClick={onClick}
|
||||
/>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{midPrice && (
|
||||
<>
|
||||
<span
|
||||
className="font-mono text-lg"
|
||||
data-testid={`middle-mark-price-${midPrice}`}
|
||||
>
|
||||
{addDecimalsFormatNumber(midPrice, decimalPlaces)}
|
||||
</span>
|
||||
<span className="text-base">{assetSymbol}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<OrderbookTable
|
||||
rows={bidRows}
|
||||
type={VolumeType.bid}
|
||||
resolution={resolution}
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={positionDecimalPlaces}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="inset-0 absolute">
|
||||
<Splash>{t('No data')}</Splash>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
ref={gridElement}
|
||||
</ReactVirtualizedAutoSizer>
|
||||
</div>
|
||||
<div className="border-t border-default">
|
||||
<select
|
||||
onChange={(e) => {
|
||||
setResolution(Number(e.currentTarget.value));
|
||||
}}
|
||||
value={resolution}
|
||||
className="block bg-neutral-100 dark:bg-neutral-700 font-mono text-right"
|
||||
data-testid="resolution"
|
||||
>
|
||||
{tableBody || (
|
||||
<div className="inset-0 absolute">
|
||||
<Splash>{t('No data')}</Splash>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{bestStaticBidPriceLinePosition && (
|
||||
<HorizontalLine
|
||||
top={`${bestStaticBidPriceLinePosition}px`}
|
||||
testId="best-static-bid-price"
|
||||
/>
|
||||
)}
|
||||
{bestStaticOfferPriceLinePosition && (
|
||||
<HorizontalLine
|
||||
top={`${bestStaticOfferPriceLinePosition}px`}
|
||||
testId={'best-static-offer-price'}
|
||||
/>
|
||||
)}
|
||||
</TinyScroll>
|
||||
<div
|
||||
className="absolute bottom-0 grid grid-cols-4 gap-2 border-t border-default mt-2 z-10 bg-white dark:bg-black w-full"
|
||||
ref={footerElement}
|
||||
>
|
||||
<div className="col-span-2">
|
||||
<Checkbox
|
||||
name="empty-prices"
|
||||
checked={fillGaps}
|
||||
onCheckedChange={() => setFillGaps((curr) => !curr)}
|
||||
label={
|
||||
<span className="text-xs">{t('Show prices with no orders')}</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-start-3">
|
||||
<select
|
||||
onChange={(e) => onResolutionChange(Number(e.currentTarget.value))}
|
||||
value={resolution}
|
||||
className="block bg-neutral-100 dark:bg-neutral-700 font-mono text-right w-full h-full"
|
||||
data-testid="resolution"
|
||||
>
|
||||
{resolutions.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{formatNumberFixed(0, decimalPlaces - Math.log10(r))}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-start-4 whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<button
|
||||
type="button"
|
||||
onClick={scrollToMidPrice}
|
||||
className={classNames('w-full h-full', {
|
||||
hidden: lockOnMidPrice,
|
||||
block: !lockOnMidPrice,
|
||||
})}
|
||||
data-testid="scroll-to-midprice"
|
||||
>
|
||||
{t('Go to mid')}
|
||||
<span className="ml-4">
|
||||
<Icon name="th-derived" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{resolutions.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{formatNumberFixed(
|
||||
Math.log10(r) - decimalPlaces > 0
|
||||
? Math.pow(10, Math.log10(r) - decimalPlaces)
|
||||
: 0,
|
||||
decimalPlaces - Math.log10(r)
|
||||
)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{debug && (
|
||||
<OrderbookDebugInfo
|
||||
decimalPlaces={decimalPlaces}
|
||||
midPrice={midPrice}
|
||||
numberOfRows={numberOfRows}
|
||||
viewportHeight={viewportHeight}
|
||||
lockOnMidPrice={lockOnMidPrice}
|
||||
priceInCenter={priceInCenter.current}
|
||||
maxPriceLevel={maxPriceLevel}
|
||||
bestStaticBidPrice={bestStaticBidPrice}
|
||||
bestStaticOfferPrice={bestStaticOfferPrice}
|
||||
minPriceLevel={minPriceLevel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
/* eslint-enable jsx-a11y/no-static-element-interactions */
|
||||
};
|
||||
|
||||
export default Orderbook;
|
||||
|
||||
+3
-2
@@ -7,12 +7,12 @@ export type DataSourceFilterFragment = { __typename?: 'Filter', key: { __typenam
|
||||
|
||||
export type DataSourceSpecFragment = { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } };
|
||||
|
||||
export type MarketFieldsFragment = { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } };
|
||||
export type MarketFieldsFragment = { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } };
|
||||
|
||||
export type MarketsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type MarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } } }> } | null };
|
||||
export type MarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } } }> } | null };
|
||||
|
||||
export const DataSourceFilterFragmentDoc = gql`
|
||||
fragment DataSourceFilter on Filter {
|
||||
@@ -77,6 +77,7 @@ export const MarketFieldsFragmentDoc = gql`
|
||||
symbol
|
||||
name
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
quoteName
|
||||
dataSourceSpecForTradingTermination {
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { isNumeric } from '@vegaprotocol/utils';
|
||||
import { useFiveDaysAgo, 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';
|
||||
import { useCandles } from '../../hooks/use-candles';
|
||||
|
||||
interface Props {
|
||||
marketId?: string;
|
||||
@@ -17,38 +10,16 @@ interface Props {
|
||||
initialValue?: string[];
|
||||
isHeader?: boolean;
|
||||
noUpdate?: boolean;
|
||||
inViewRoot?: RefObject<Element>;
|
||||
}
|
||||
|
||||
export const Last24hPriceChange = ({
|
||||
marketId,
|
||||
decimalPlaces,
|
||||
initialValue,
|
||||
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(),
|
||||
},
|
||||
skip: !marketId || !inView,
|
||||
const { oneDayCandles, error, fiveDaysCandles } = useCandles({
|
||||
marketId,
|
||||
});
|
||||
|
||||
const fiveDaysCandles = data?.filter((candle) => Boolean(candle));
|
||||
|
||||
const candles = fiveDaysCandles?.filter((candle) =>
|
||||
isCandleLessThan24hOld(candle, yesterday)
|
||||
);
|
||||
const oneDayCandles =
|
||||
candles
|
||||
?.map((candle) => candle?.close)
|
||||
.filter((c): c is CandleClose => c !== null) || initialValue;
|
||||
|
||||
if (
|
||||
fiveDaysCandles &&
|
||||
fiveDaysCandles.length > 0 &&
|
||||
@@ -68,30 +39,18 @@ export const Last24hPriceChange = ({
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<span ref={ref}>{t('Unknown')} </span>
|
||||
<span>-</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !isNumeric(decimalPlaces)) {
|
||||
return <span ref={ref}>-</span>;
|
||||
return <span>-</span>;
|
||||
}
|
||||
return (
|
||||
<PriceChangeCell
|
||||
candles={oneDayCandles || []}
|
||||
candles={oneDayCandles?.map((c) => c.close) || initialValue || []}
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import type { RefObject } from 'react';
|
||||
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 { 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';
|
||||
import { useCandles } from '../../hooks';
|
||||
|
||||
interface Props {
|
||||
marketId?: string;
|
||||
positionDecimalPlaces?: number;
|
||||
formatDecimals?: number;
|
||||
inViewRoot?: RefObject<Element>;
|
||||
initialValue?: string;
|
||||
}
|
||||
|
||||
@@ -22,29 +15,12 @@ export const Last24hVolume = ({
|
||||
marketId,
|
||||
positionDecimalPlaces,
|
||||
formatDecimals,
|
||||
inViewRoot,
|
||||
initialValue,
|
||||
}: Props) => {
|
||||
const yesterday = useYesterday();
|
||||
const fiveDaysAgo = useFiveDaysAgo();
|
||||
const [ref, inView] = useInView({ root: inViewRoot?.current });
|
||||
|
||||
const { data } = useThrottledDataProvider({
|
||||
dataProvider: marketCandlesProvider,
|
||||
variables: {
|
||||
marketId: marketId || '',
|
||||
interval: Schema.Interval.INTERVAL_I1H,
|
||||
since: new Date(fiveDaysAgo).toISOString(),
|
||||
},
|
||||
skip: !(inView && marketId),
|
||||
const { oneDayCandles, fiveDaysCandles } = useCandles({
|
||||
marketId,
|
||||
});
|
||||
|
||||
const fiveDaysCandles = data?.filter((candle) => Boolean(candle));
|
||||
|
||||
const oneDayCandles = fiveDaysCandles?.filter((candle) =>
|
||||
isCandleLessThan24hOld(candle, yesterday)
|
||||
);
|
||||
|
||||
if (
|
||||
fiveDaysCandles &&
|
||||
fiveDaysCandles.length > 0 &&
|
||||
@@ -72,20 +48,21 @@ export const Last24hVolume = ({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span ref={ref}>{t('Unknown')} </span>
|
||||
<span>-</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}>
|
||||
<span>
|
||||
{candleVolume && isNumeric(positionDecimalPlaces)
|
||||
? addDecimalsFormatNumber(
|
||||
candleVolume,
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './use-market-oracle';
|
||||
export * from './use-oracle-markets';
|
||||
export * from './use-oracle-proofs';
|
||||
export * from './use-oracle-spec-binding-data';
|
||||
export * from './use-candles';
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useCandles } from './use-candles';
|
||||
|
||||
const today = new Date();
|
||||
const fiveDaysAgo = new Date();
|
||||
fiveDaysAgo.setDate(today.getDate() - 5);
|
||||
|
||||
const mockData = [
|
||||
{
|
||||
high: '6293819',
|
||||
low: '6263737',
|
||||
open: '6266893',
|
||||
close: '6293819',
|
||||
volume: '72447',
|
||||
periodStart: today.toISOString(),
|
||||
__typename: 'Candle',
|
||||
},
|
||||
null,
|
||||
{
|
||||
high: '6309988',
|
||||
low: '6296335',
|
||||
open: '6307451',
|
||||
close: '6296335',
|
||||
volume: '73657',
|
||||
periodStart: today.toISOString(),
|
||||
__typename: 'Candle',
|
||||
},
|
||||
{
|
||||
high: '6315153',
|
||||
low: '6294001',
|
||||
open: '6296335',
|
||||
close: '6315152',
|
||||
volume: '89395',
|
||||
periodStart: today.toISOString(),
|
||||
__typename: 'Candle',
|
||||
},
|
||||
{
|
||||
high: '6309988',
|
||||
low: '6296335',
|
||||
open: '6307451',
|
||||
close: '6296335',
|
||||
volume: '73657',
|
||||
periodStart: fiveDaysAgo.toISOString(),
|
||||
__typename: 'Candle',
|
||||
},
|
||||
{
|
||||
high: '6315153',
|
||||
low: '6294001',
|
||||
open: '6296335',
|
||||
close: '6315152',
|
||||
volume: '89395',
|
||||
periodStart: fiveDaysAgo.toISOString(),
|
||||
__typename: 'Candle',
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('@vegaprotocol/data-provider', () => {
|
||||
return {
|
||||
...jest.requireActual('@vegaprotocol/data-provider'),
|
||||
useThrottledDataProvider: jest.fn(() => ({
|
||||
data: mockData,
|
||||
error: false,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useCandles', () => {
|
||||
it('should return one day candles and five day candles', () => {
|
||||
const { result } = renderHook(() => useCandles({ marketId: '3456789' }));
|
||||
const expectedOneDayCandles = [
|
||||
{
|
||||
high: '6293819',
|
||||
low: '6263737',
|
||||
open: '6266893',
|
||||
close: '6293819',
|
||||
volume: '72447',
|
||||
periodStart: today.toISOString(),
|
||||
__typename: 'Candle',
|
||||
},
|
||||
{
|
||||
high: '6309988',
|
||||
low: '6296335',
|
||||
open: '6307451',
|
||||
close: '6296335',
|
||||
volume: '73657',
|
||||
periodStart: today.toISOString(),
|
||||
__typename: 'Candle',
|
||||
},
|
||||
{
|
||||
high: '6315153',
|
||||
low: '6294001',
|
||||
open: '6296335',
|
||||
close: '6315152',
|
||||
volume: '89395',
|
||||
periodStart: today.toISOString(),
|
||||
__typename: 'Candle',
|
||||
},
|
||||
];
|
||||
expect(result.current).toStrictEqual({
|
||||
oneDayCandles: expectedOneDayCandles,
|
||||
fiveDaysCandles: mockData.filter(Boolean),
|
||||
error: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { useFiveDaysAgo, useYesterday } from '@vegaprotocol/react-helpers';
|
||||
import type { MarketCandlesFieldsFragment } from '../__generated__';
|
||||
import { marketCandlesProvider } from '../market-candles-provider';
|
||||
import { Interval } from '@vegaprotocol/types';
|
||||
|
||||
export const useCandles = ({ marketId }: { marketId?: string }) => {
|
||||
const fiveDaysAgo = useFiveDaysAgo();
|
||||
const yesterday = useYesterday();
|
||||
const { data, error } = useThrottledDataProvider({
|
||||
dataProvider: marketCandlesProvider,
|
||||
variables: {
|
||||
marketId: marketId || '',
|
||||
interval: Interval.INTERVAL_I1H,
|
||||
since: new Date(fiveDaysAgo).toISOString(),
|
||||
},
|
||||
skip: !marketId,
|
||||
});
|
||||
|
||||
const fiveDaysCandles = data?.filter(Boolean);
|
||||
|
||||
const oneDayCandles = fiveDaysCandles?.filter((candle) =>
|
||||
isCandleLessThan24hOld(candle, yesterday)
|
||||
);
|
||||
return { oneDayCandles, error, fiveDaysCandles };
|
||||
};
|
||||
|
||||
export const isCandleLessThan24hOld = (
|
||||
candle: MarketCandlesFieldsFragment | undefined,
|
||||
yesterday: number
|
||||
) => {
|
||||
if (!candle?.periodStart) {
|
||||
return false;
|
||||
}
|
||||
const candleDate = new Date(candle.periodStart);
|
||||
return candleDate > new Date(yesterday);
|
||||
};
|
||||
@@ -58,6 +58,7 @@ fragment MarketFields on Market {
|
||||
symbol
|
||||
name
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
quoteName
|
||||
dataSourceSpecForTradingTermination {
|
||||
|
||||
@@ -60,6 +60,7 @@ export const createMarketFragment = (
|
||||
symbol: 'tDAI',
|
||||
name: 'tDAI',
|
||||
decimals: 5,
|
||||
quantum: '1',
|
||||
__typename: 'Asset',
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
|
||||
@@ -99,6 +99,8 @@ export const NetworkParams = {
|
||||
governance_proposal_freeform_minProposerBalance:
|
||||
'governance_proposal_freeform_minProposerBalance',
|
||||
validators_delegation_minAmount: 'validators_delegation_minAmount',
|
||||
spam_protection_minimumWithdrawalQuantumMultiple:
|
||||
'spam_protection_minimumWithdrawalQuantumMultiple',
|
||||
spam_protection_voting_min_tokens: 'spam_protection_voting_min_tokens',
|
||||
spam_protection_proposal_min_tokens: 'spam_protection_proposal_min_tokens',
|
||||
market_liquidity_stakeToCcyVolume: 'market_liquidity_stakeToCcyVolume',
|
||||
|
||||
@@ -47,6 +47,7 @@ export const generateOrder = (partialOrder?: PartialDeep<Order>) => {
|
||||
decimals: 1,
|
||||
symbol: 'XYZ',
|
||||
name: 'XYZ',
|
||||
quantum: '1',
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
__typename: 'DataSourceSpec',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user