Compare commits

..
Author SHA1 Message Date
Matthew Russell cd0b29c251 fix: use t function for text content in zero balance message 2023-06-08 09:15:10 -07:00
Matthew Russell 6471be323d test: update e2e tests with ac for button state 2023-06-07 22:25:16 -07:00
Matthew Russell 3e9aeb2f2f test: check that button is enabled before form is filled in 2023-06-07 22:14:36 -07:00
Matthew Russell e83a7915ad chore: make assertion check for enabled 2023-06-07 22:09:03 -07:00
Matthew Russell d2b2d16c9c test: update to check that button is always enabled 2023-06-07 22:03:23 -07:00
Matthew Russell 16b2360da7 chore: make deal ticket notification buttons smaller 2023-06-07 17:31:13 -07:00
Matthew Russell 1adbc2dbd7 chore: remove disabled submit button 2023-06-07 17:20:59 -07:00
Ciaran McGhie d6f39049bd chore(ui-toolkit,react-helpers,utils): publish new versions of ui-toolkit, react-helpers and utils (#4049) 2023-06-07 15:58:20 -07:00
Maciek 1d06be8f4e fix: wrong css class (#4053) 2023-06-07 20:44:28 +00:00
Edd 5eba8fe28f fix(explorer): fix broken protocol upgrade tx link (#4029) 2023-06-07 14:47:55 +01:00
m.ray 2ba0e9a1b2 chore(trading): add quantum formatting to deal ticket (#4030) 2023-06-07 11:49:50 +01:00
43d3754c64 feat(trading): 3945 orderbook enhancements (#4016)
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-06-07 10:58:34 +02:00
m.ray aae5c44fa4 fix(trading): market selector volume update (#3989) 2023-06-06 18:45:52 -07:00
Art 878bed9c7a fix(withdraws): minimal withdrawal amount validation (#3993) 2023-06-06 22:21:48 +01:00
Sam Keen 6f9f432c90 feat(governance): multisig warning (#3994) 2023-06-06 22:21:31 +01:00
Sam Keen 3f01b93159 feat(governance,proposals,web3): remove use of word "we" in copy (#4034) 2023-06-06 22:21:17 +01:00
Edd 473c244d7b chore(governance,liquidity-provision-dashboard,trading): remove broken urls (#4042) 2023-06-06 22:19:42 +01:00
Joe Tsang d6a32f5090 chore(governance): fix flaky test failures (#4043) 2023-06-06 22:19:23 +01:00
Mikołaj Młodzikowski 71540a90fb fix(ci): resolving bucket name 2023-06-06 17:02:24 +02:00
Mikołaj Młodzikowski cefdbe6a3c fix(ci): missing space on resolving projects 2023-06-06 16:46:58 +02:00
Mikołaj Młodzikowski 3928dd5c0e fix(ci): way of resolving affected 2023-06-06 15:37:06 +02:00
Ciaran McGhie 1521bab4c4 chore(react-helpers,utils,logger): create logger lib and move sentry/logger utils there (#3990) 2023-06-06 14:10:03 +01:00
Mikołaj Młodzikowski b3036d520f fix(ci): way of resolving affected 2023-06-06 15:03:18 +02:00
Mikołaj Młodzikowski 429a5a23d3 fix(ci): way of resolving affected 2023-06-06 14:48:49 +02:00
Mikołaj Młodzikowski 5b02fd5d54 fix(ci): way of resolving affected 2023-06-06 14:39:28 +02:00
Mikołaj Młodzikowski b8309a76e7 fix(ci): way of resolving affected 2023-06-06 14:29:46 +02:00
Mikołaj Młodzikowski e8ae085c06 fix(ci): typo in app name 2023-06-06 14:18:47 +02:00
Mikołaj Młodzikowski fdcd24847c fix(ci): syntax for publishing dist 2023-06-06 14:07:56 +02:00
Mikołaj Młodzikowski e054db39b5 feat(ci): add deploys for multisig-signer (#4039) 2023-06-06 13:47:42 +02:00
Joe Tsang a01e48d508 chore(governance): nightly e2e test fixes (#4038) 2023-06-06 12:12:59 +01:00
124 changed files with 1775 additions and 2122 deletions
+34 -5
View File
@@ -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:
+1 -1
View File
@@ -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
+10 -2
View File
@@ -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
+1
View File
@@ -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
);
}
});
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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';
@@ -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();
});
});
@@ -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,
};
};
@@ -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
View File
@@ -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'
+1 -1
View File
@@ -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 -1
View File
@@ -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;
+1
View File
@@ -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;
+1 -1
View File
@@ -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);
});
});
}
+7 -2
View File
@@ -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 -1
View File
@@ -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,
});
+1
View File
@@ -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';
+2 -1
View File
@@ -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) => {
+1
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
import { localLoggerFactory } from '@vegaprotocol/utils';
import { localLoggerFactory } from '@vegaprotocol/logger';
import { useCallback, useEffect, useState } from 'react';
import z from 'zod';
+1
View File
@@ -75,6 +75,7 @@ export const generateFill = (override?: PartialDeep<Trade>) => {
name: 'assset-id',
symbol: 'SYM',
decimals: 18,
quantum: '1',
},
quoteName: '',
dataSourceSpecForTradingTermination: {
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nrwl/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+18
View File
@@ -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": {}
}
]
}
+7
View File
@@ -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).
+10
View File
@@ -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',
};
+5
View File
@@ -0,0 +1,5 @@
{
"name": "@vegaprotocol/logger",
"version": "0.0.1",
"type": "commonjs"
}
+34
View File
@@ -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
}
}
}
}
+1
View File
@@ -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;
+2
View File
@@ -0,0 +1,2 @@
export * from './lib';
export * from './hooks';
+2
View File
@@ -0,0 +1,2 @@
export * from './local-logger';
export * from './sentry-utils';
+23
View File
@@ -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"
}
]
}
+10
View File
@@ -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"]
}
+20
View File
@@ -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"
]
}
+21 -210
View File
@@ -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);
});
});
+41 -242
View File
@@ -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,
};
};
+12 -138
View File
@@ -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>
);
+97 -109
View File
@@ -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';
+49 -218
View File
@@ -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,
};
+161 -651
View File
@@ -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
View File
@@ -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,
+1
View File
@@ -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,
});
});
});
+37
View File
@@ -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);
};
+1
View File
@@ -58,6 +58,7 @@ fragment MarketFields on Market {
symbol
name
decimals
quantum
}
quoteName
dataSourceSpecForTradingTermination {
+1
View File
@@ -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