Compare commits

...
Author SHA1 Message Date
Matthew Russell bc758e90c8 chore: add specs 2023-08-07 08:40:02 +01:00
Matthew Russell a70a7bcc8e fix(trading): dont show connect wallet message in deal ticket (#4482) 2023-08-04 11:09:43 +01:00
Matthew Russell 4461a6a497 fix(trading): remove hover state from disabled view as button (#4487) 2023-08-04 11:08:48 +01:00
Matthew Russell 1041864ad8 chore(trading): remove unnecessary forwarding of grid refs (#4486) 2023-08-04 11:08:28 +01:00
Matthew Russell 015e0188ec chore: enable iceberg orders 2023-08-03 12:38:51 +01:00
Matthew Russell 228236c152 chore(deal-ticket): type dropdown icon (#4468) 2023-08-03 12:15:21 +01:00
Art cd6e906dc6 feat(trading): view as button in sidebar (#4470) 2023-08-03 12:14:35 +01:00
Maciek 8a47a92dbd fix(trading): add info splashes on deposit and withdrawals list containers (#4479) 2023-08-03 12:01:42 +01:00
Matthew Russell af14c27df9 chore(trading): responsive welcome dialog content and content update (#4475) 2023-08-03 12:01:09 +01:00
Matthew Russell 38edcd88c0 chore(trading): fix alignment of expire (#4477) 2023-08-03 12:00:12 +01:00
m.ray a74cf02030 feat(trading): order details dialog (#4281) 2023-08-03 10:05:12 +00:00
Joe Tsang 6686755763 chore(governance): fix failing tests from nightly run (#4453) 2023-08-03 10:01:00 +01:00
Joe Tsang 79cf8163be chore(governance): add e2e test for successor market (#4471) 2023-08-03 09:54:51 +01:00
Matthew Russell 1688e26f4b feat(trading): add fairground texture background to logo (#4474) 2023-08-03 09:28:01 +01:00
Art 5e101a8eaf feat(trading): deposit page (#4472) 2023-08-02 19:06:49 +02:00
Matthew Russell 65071f57d0 fix(trading,governance): eager connection for browser wallet (#4444) 2023-08-02 17:16:09 +01:00
daro-maj b87c981a90 test(trading): fix for key-to-key test (#4469) 2023-08-02 18:11:31 +02:00
Joe TsangandMatthew Russell b509e5b347 test(explorer): e2e proposal test (#4379)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-08-02 16:37:55 +01:00
Maciek bb9fce4831 feat(trading): 4322 get started steps (#4440) 2023-08-02 15:34:04 +00:00
Matthew Russell 918825aca8 chore: adjust spacing and text size of long/short type selectors 2023-08-02 15:36:39 +01:00
Matthew Russell 74bf183fad feat(trading): 3114 pane context (#4442) 2023-08-02 15:29:41 +01:00
m.ray 674baa1ee3 fix(trading): iceberg order fills (#4460) 2023-08-02 16:55:49 +03:00
Matthew Russell d18a0c7e15 fix(trading): lp table forward ref breaking resize grid (#4447) 2023-08-02 13:59:05 +01:00
m.ray 76a6005b77 fix(trading): replace all markets with open markets (#4452) 2023-08-02 13:58:26 +01:00
JonRay15andMatthew Russell 79414614c2 chore(trading,governance,explorer): fix issue template (#4459)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-08-02 12:52:51 +01:00
m.ray fde49e5446 fix(trading): order stopped misleading message (#4455) 2023-08-02 11:52:07 +01:00
JonRay15andMatthew Russell 45283161aa chore: create feature-epic issue template (#4456)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-08-02 11:48:23 +01:00
Bartłomiej GłowniaandDariusz Majcherczyk 375d447fa8 feat(trading): add stop orders table and form (#4265)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
2023-08-02 11:28:33 +01:00
Mikołaj Młodzikowski 1dd97a2bce feat(ci): add develop docker image (#4458) 2023-08-02 10:58:17 +02:00
219 changed files with 8908 additions and 2707 deletions
+28
View File
@@ -0,0 +1,28 @@
---
name: Feature Epic
about: A template to capture and scope user requirements, high level process, and basic mockups for an upcoming feature as part of the initial core spec review process.
title: 'Epic: '
labels: feature-epic
---
## Core Feature
<Name>
## Tasks
- [ ] Define high level requirements
- [ ] Create basic mockups
- [ ] Update "API Requirements" in core spec
- [ ] Update "User-Interface Spec" in relevant front end repo
- [ ] Create detailed user stories using normal template
## High Level Requirements
## Basic Mockups
## Link to API Requirements in Core spec
## Link to User Interface Specs
## Linked User Stories
+9 -3
View File
@@ -29,6 +29,12 @@ jobs:
echo IS_TESTNET_RELEASE=false >> $GITHUB_ENV echo IS_TESTNET_RELEASE=false >> $GITHUB_ENV
echo IS_IPFS_RELEASE=false >> $GITHUB_ENV echo IS_IPFS_RELEASE=false >> $GITHUB_ENV
echo IS_S3_RELEASE=false >> $GITHUB_ENV echo IS_S3_RELEASE=false >> $GITHUB_ENV
echo IS_DEV_IMAGE=false >> $GITHUB_ENV
- name: Is dev image
if: ${{ contains(github.ref, 'develop') && github.event_name == 'push' && matrix.app == 'trading' }}
run: |
echo IS_DEV_IMAGE=true >> $GITHUB_ENV
- name: Is PR - name: Is PR
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
@@ -75,7 +81,7 @@ jobs:
- name: Log in to the Container registry (docker hub) - name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2 uses: docker/login-action@v2
if: ${{ env.IS_IPFS_RELEASE == 'true' }} if: ${{ env.IS_IPFS_RELEASE == 'true' || env.IS_DEV_IMAGE == 'true' }}
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -173,7 +179,7 @@ jobs:
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
continue-on-error: true continue-on-error: true
id: dockerhub-push id: dockerhub-push
if: ${{ env.IS_IPFS_RELEASE == 'true' }} if: ${{ env.IS_IPFS_RELEASE == 'true' || env.IS_DEV_IMAGE == 'true' }}
with: with:
context: . context: .
file: docker/node-outside-docker.Dockerfile file: docker/node-outside-docker.Dockerfile
@@ -183,7 +189,7 @@ jobs:
ENV_NAME=${{ env.ENV_NAME }} ENV_NAME=${{ env.ENV_NAME }}
tags: | tags: |
vegaprotocol/${{ matrix.app }}:${{ github.sha }} vegaprotocol/${{ matrix.app }}:${{ github.sha }}
vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || '' }} vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || env.IS_DEV_IMAGE == 'true' && 'develop' || '' }}
- name: Publish dist as docker image (ghcr - retry) - name: Publish dist as docker image (ghcr - retry)
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
@@ -0,0 +1,46 @@
context('Proposal page', { tags: '@smoke' }, function () {
describe('Verify elements on page', function () {
const proposalHeading = 'proposals-heading';
const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
const proposalTitle = 'Add Lorem Ipsum market';
before('Create market proposal', function () {
cy.visit('/');
cy.createMarket();
});
it('Able to view proposal', function () {
cy.navigate_to('governanceProposals');
cy.getByTestId(proposalHeading).should('be.visible');
// get first proposal in list
cy.get('[row-index="0"]').within(() => {
cy.get_element_by_col_id('title').should('have.text', proposalTitle);
cy.get_element_by_col_id('type').should('have.text', 'NewMarket');
cy.get_element_by_col_id('state').should('have.text', 'Enacted');
cy.getByTestId('vote-progress').should('be.visible');
cy.get('[col-id="cDate"]')
.invoke('text')
.should('match', dateTimeRegex);
cy.get('[col-id="eDate"]')
.invoke('text')
.should('match', dateTimeRegex);
cy.getByTestId('external-link')
.should('have.attr', 'href')
.and('contains', 'https://governance.fairground.wtf/proposals/');
cy.contains('View terms').should('exist').click();
});
cy.getByTestId('dialog-title').should('have.text', proposalTitle);
cy.get('.language-json').should('exist');
});
it('Proposal page displayed on mobile', function () {
cy.common_switch_to_mobile_and_click_toggle();
cy.navigate_to('governanceProposals', true);
cy.getByTestId(proposalHeading).should('be.visible');
cy.get('[row-index="0"]').within(() => {
cy.get_element_by_col_id('title').should('have.text', proposalTitle);
});
});
});
});
@@ -0,0 +1,107 @@
{
"changes": {
"decimalPlaces": "5",
"positionDecimalPlaces": "5",
"linearSlippageFactor": "0.001",
"quadraticSlippageFactor": "0",
"lpPriceRange": "10",
"instrument": {
"name": "Token test market",
"code": "Token.24h",
"future": {
"settlementAsset": "816af99af60d684502a40824758f6b5377e6af48e50a9ee8ef478ecb879ea8bc",
"quoteName": "fUSDC",
"dataSourceSpecForSettlementData": {
"external": {
"oracle": {
"signers": [
{
"pubKey": {
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
}
}
],
"filters": [
{
"key": {
"name": "prices.BTC.value",
"type": "TYPE_INTEGER",
"numberDecimalPlaces": "0"
},
"conditions": [
{
"operator": "OPERATOR_GREATER_THAN",
"value": "0"
}
]
}
]
}
}
},
"dataSourceSpecForTradingTermination": {
"external": {
"oracle": {
"signers": [
{
"pubKey": {
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
}
}
],
"filters": [
{
"key": {
"name": "trading.terminated.ETH5",
"type": "TYPE_BOOLEAN"
},
"conditions": [
{
"operator": "OPERATOR_EQUALS",
"value": "true"
}
]
}
]
}
}
},
"dataSourceSpecBinding": {
"settlementDataProperty": "prices.BTC.value",
"tradingTerminationProperty": "trading.terminated.ETH5"
}
}
},
"metadata": ["sector:food", "sector:materials", "source:docs.vega.xyz"],
"priceMonitoringParameters": {
"triggers": [
{
"horizon": "43200",
"probability": "0.9999999",
"auctionExtension": "600"
}
]
},
"liquidityMonitoringParameters": {
"targetStakeParameters": {
"timeWindow": "3600",
"scalingFactor": 10
},
"triggeringRatio": "0.7",
"auctionExtension": "1"
},
"logNormal": {
"tau": 0.0001140771161,
"riskAversionParameter": 0.001,
"params": {
"mu": 0,
"r": 0.016,
"sigma": 0.8
}
},
"successor": {
"parentMarketId": "",
"insurancePoolFraction": "0.75"
}
}
}
@@ -220,7 +220,7 @@ context(
function () { function () {
const proposalTitle = 'Test new market proposal'; const proposalTitle = 'Test new market proposal';
goToMakeNewProposal(governanceProposalType.NEW_MARKET); goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.getByTestId(newProposalTitle).type('Test new market proposal'); cy.getByTestId(newProposalTitle).type(proposalTitle);
cy.getByTestId(newProposalDescription).type('E2E test for proposals'); cy.getByTestId(newProposalDescription).type('E2E test for proposals');
cy.fixture('/proposals/new-market').then((newMarketProposal) => { cy.fixture('/proposals/new-market').then((newMarketProposal) => {
const newMarketPayload = JSON.stringify(newMarketProposal); const newMarketPayload = JSON.stringify(newMarketProposal);
@@ -606,6 +606,94 @@ context(
}); });
}); });
it('able to submit successor market proposal', function () {
const proposalTitle = 'Test successor market proposal';
cy.createMarket();
cy.reload();
waitForSpinner();
cy.getByTestId('closed-proposals').within(() => {
cy.contains('Add Lorem Ipsum market')
.parentsUntil(proposalListItem)
.last()
.within(() => {
cy.getByTestId(viewProposalBtn).click();
});
});
getProposalInformationFromTable('ID').invoke('text').as('parentMarketId');
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.getByTestId(newProposalTitle).type(proposalTitle);
cy.getByTestId(newProposalDescription).type(
'E2E test for successor market'
);
cy.fixture('/proposals/successor-market').then((newMarketProposal) => {
newMarketProposal.changes.successor.parentMarketId =
this.parentMarketId;
const newMarketPayload = JSON.stringify(newMarketProposal);
cy.getByTestId(newProposalTerms).type(newMarketPayload, {
parseSpecialCharSequences: false,
delay: 2,
});
});
cy.getByTestId(proposalDownloadBtn)
.should('be.visible')
.click()
.then(() => {
cy.wrap(
getDownloadedProposalJsonPath('vega-new-market-proposal-')
).then((filePath) => {
goToMakeNewProposal(governanceProposalType.RAW);
submitUniqueRawProposal({ proposalBody: filePath });
});
});
navigateTo(navigation.proposals);
getProposalFromTitle(proposalTitle).within(() => {
// 3003-PMAN-008
cy.getByTestId('proposal-successor-info')
.should('have.text', 'Successor market to: TEST.24h')
.find('a')
.should('have.attr', 'href')
.and('contain', this.parentMarketId);
cy.getByTestId('view-proposal-btn').click();
});
// #3003-PMAN-010
cy.getByTestId(proposalJsonToggle).click();
cy.get('.language-json').within(() => {
cy.get('.hljs-attr').should('contain.text', 'parentMarketId');
cy.get('.hljs-string').should('contain.text', this.parentMarketId);
cy.get('.hljs-attr').should('contain.text', 'insurancePoolFraction');
cy.get('.hljs-string').should('contain.text', '0.75');
});
cy.getByTestId('proposal-market-data').within(() => {
cy.getByTestId('proposal-market-data-toggle').click();
cy.contains('Key details').click();
// 3003-PMAN-009
getMarketProposalDetailsFromTable('Parent Market ID').should(
'have.text',
this.parentMarketId
);
getMarketProposalDetailsFromTable('Insurance Pool Fraction').should(
'have.text',
'0.75'
);
getMarketProposalDetailsFromTable('Trading Mode').should(
'have.text',
'No trading'
);
});
// 3003-PMAN-011
cy.contains('Parent Market ID').realHover();
cy.getByTestId('tooltip-content').should(
'contain.text',
'The ID of the market this market succeeds.'
);
cy.contains('Insurance Pool Fraction').realHover();
cy.getByTestId('tooltip-content').should(
'contain.text',
'The fraction of the insurance pool balance that is carried over from the parent market to the successor.'
);
});
after('Disassociate from second wallet key if present', function () { after('Disassociate from second wallet key if present', function () {
cy.reload(); cy.reload();
waitForSpinner(); waitForSpinner();
@@ -96,12 +96,14 @@ context('rewards - flow', { tags: '@slow' }, function () {
.within(() => { .within(() => {
cy.get('h2').first().should('contain.text', 'EPOCH'); cy.get('h2').first().should('contain.text', 'EPOCH');
cy.getByTestId('individual-rewards-asset').should('have.text', 'Vega'); cy.getByTestId('individual-rewards-asset').should('have.text', 'Vega');
cy.getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD', rewardsTimeOut) cy.getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD', rewardsTimeOut).should(
.should('contain.text', '0.4415') 'contain.text',
.and('contain.text', '(44.15%)'); '0.4415'
cy.getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE') );
.should('contain.text', '0.0004') cy.getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE').should(
.and('contain.text', '(44.15%)'); 'contain.text',
'0.0004'
);
cy.getByTestId('total').should('have.text', '0.4419'); cy.getByTestId('total').should('have.text', '0.4419');
}); });
}); });
@@ -339,7 +339,7 @@ context(
cy.getByTestId(vegaWalletCurrencyTitle) cy.getByTestId(vegaWalletCurrencyTitle)
.contains(name) .contains(name)
.parent() .parent()
.siblings() .siblings(txTimeout)
.should((elementAmount) => { .should((elementAmount) => {
const displayedAmount = parseFloat(elementAmount.text()); const displayedAmount = parseFloat(elementAmount.text());
expect(displayedAmount).be.gte(expectedAmount); expect(displayedAmount).be.gte(expectedAmount);
@@ -107,7 +107,7 @@ export function dissociateFromSecondWalletKey() {
cy.getByTestId('vega-in-wallet') cy.getByTestId('vega-in-wallet')
.first() .first()
.within(() => { .within(() => {
cy.getByTestId('eth-wallet-associated-balances') cy.getByTestId('eth-wallet-associated-balances', txTimeout)
.last() .last()
.within(() => { .within(() => {
cy.getByTestId('associated-key') cy.getByTestId('associated-key')
@@ -78,13 +78,21 @@ export function stakingPageAssociateTokens(
} }
cy.get(tokenAmountInputBox, epochTimeout).type(amount); cy.get(tokenAmountInputBox, epochTimeout).type(amount);
if (approve) { if (approve) {
cy.get(tokenInputApprove, txTimeout).should('be.enabled').click(); cy.getByTestId('wallet-associate').then((walletAssociateField) => {
cy.contains('Approve $VEGA Tokens for staking on Vega').should( if (
'be.visible' walletAssociateField.find('[data-testid="token-input-approve-button"]')
); .length
cy.contains('Approve $VEGA Tokens for staking on Vega', txTimeout).should( ) {
'not.exist' cy.get(tokenInputApprove, txTimeout).should('be.enabled').click();
); cy.contains('Approve $VEGA Tokens for staking on Vega').should(
'be.visible'
);
cy.contains(
'Approve $VEGA Tokens for staking on Vega',
txTimeout
).should('not.exist');
}
});
} }
cy.get(tokenSubmitButton, txTimeout).should('be.enabled').click(); cy.get(tokenSubmitButton, txTimeout).should('be.enabled').click();
@@ -200,7 +200,7 @@ export const ProposalMarketData = ({
title={marketData.tradableInstrument.instrument.code} title={marketData.tradableInstrument.instrument.code}
open={isOpen} open={isOpen}
onChange={(isOpen) => (isOpen ? open() : close())} onChange={(isOpen) => (isOpen ? open() : close())}
size="medium" size="large"
dataTestId="market-json-dialog" dataTestId="market-json-dialog"
> >
<CopyWithTooltip text={JSON.stringify(marketData)}> <CopyWithTooltip text={JSON.stringify(marketData)}>
@@ -57,7 +57,7 @@ const ROWS = [
export const HealthDialog = ({ onChange, isOpen }: HealthDialogProps) => { export const HealthDialog = ({ onChange, isOpen }: HealthDialogProps) => {
return ( return (
<Dialog size="medium" open={isOpen} onChange={onChange}> <Dialog size="large" open={isOpen} onChange={onChange}>
<h1 className="text-2xl mb-5 pr-2 font-medium font-alpha uppercase"> <h1 className="text-2xl mb-5 pr-2 font-medium font-alpha uppercase">
{t('Health')} {t('Health')}
</h1> </h1>
@@ -2,6 +2,7 @@ describe('charts', { tags: '@smoke' }, () => {
before(() => { before(() => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
cy.wait('@Markets'); cy.wait('@Markets');
cy.getByTestId('Depth').click(); cy.getByTestId('Depth').click();
@@ -202,6 +202,7 @@ describe('Closed markets', { tags: '@smoke' }, () => {
const specDataConnection = createDataConnection(); const specDataConnection = createDataConnection();
before(() => { before(() => {
cy.setOnBoardingViewed();
cy.mockGQL((req) => { cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery()); aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery()); aliasGQLQuery(req, 'Statistics', statisticsQuery());
@@ -455,6 +456,7 @@ describe('no closed markets', { tags: '@smoke', testIsolation: true }, () => {
before(() => { before(() => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all'); cy.visit('/#/markets/all');
cy.get('[data-testid="Closed markets"]').click(); cy.get('[data-testid="Closed markets"]').click();
}); });
@@ -4,6 +4,7 @@ const nodeHealthTrigger = 'node-health-trigger';
describe('home', { tags: '@regression' }, () => { describe('home', { tags: '@regression' }, () => {
before(() => { before(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.visit('/'); cy.visit('/');
+38 -37
View File
@@ -2,7 +2,7 @@ import { aliasGQLQuery } from '@vegaprotocol/cypress';
import type { ProposalListFieldsFragment } from '@vegaprotocol/proposals'; import type { ProposalListFieldsFragment } from '@vegaprotocol/proposals';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
const dialogContent = 'dialog-content'; const dialogContent = 'welcome-dialog';
const generateProposal = (code: string): ProposalListFieldsFragment => ({ const generateProposal = (code: string): ProposalListFieldsFragment => ({
__typename: 'Proposal', __typename: 'Proposal',
@@ -140,43 +140,43 @@ describe('home', { tags: '@regression' }, () => {
cy.wait('@MarketsData'); cy.wait('@MarketsData');
}); });
it('redirects to market/all and displays welcome notice', () => { it('close welcome dialog should redirect to market/all', () => {
cy.url().should('eq', Cypress.config().baseUrl + `/#/markets/all`); cy.url().should('eq', Cypress.config().baseUrl + `/#/markets/all`);
cy.getByTestId('welcome-notice-title').should( cy.getByTestId('welcome-dialog').should('be.visible');
'contain.text', cy.getByTestId('welcome-title').should('contain.text', 'Console CUSTOM');
'Welcome to Console' cy.getByTestId('browse-markets-button').should('not.be.disabled');
); cy.getByTestId('get-started-banner').should('be.visible');
}); cy.getByTestId('get-started-button').should('not.be.disabled');
}); cy.getByTestId('dialog-close').click();
cy.url().should('eq', Cypress.config().baseUrl + `/#/markets/all`);
describe('no proposal nor markets found', () => { cy.window().then((window) => {
it('there are welcome text and a link to propose market', () => { expect(window.localStorage.getItem('vega_onboarding_viewed')).to.equal(
cy.mockGQL((req) => { 'true'
const data = { );
marketsConnection: {
__typename: 'MarketConnection',
edges: [],
},
};
aliasGQLQuery(req, 'Markets', data);
aliasGQLQuery(req, 'MarketsData', data);
aliasGQLQuery(req, 'ProposalsList', {
proposalsConnection: {
__typename: 'ProposalsConnection',
edges: null,
},
});
}); });
cy.visit('/'); });
cy.wait('@Markets');
cy.wait('@MarketsData'); it('click browse markets button should redirect to market/all', () => {
cy.getByTestId('welcome-notice-title').should( cy.getByTestId('welcome-dialog').should('be.visible');
'contain.text', cy.getByTestId('browse-markets-button').click();
'Welcome to Console' cy.url().should('eq', Cypress.config().baseUrl + `/#/markets/all`);
); cy.window().then((window) => {
cy.getByTestId('external-link') expect(window.localStorage.getItem('vega_onboarding_viewed')).to.equal(
.contains('Propose a market') 'true'
.should('exist'); );
});
});
it('click get started button should open connect dialog', () => {
cy.getByTestId('welcome-dialog').should('be.visible');
cy.getByTestId('get-started-button').click();
cy.url().should('eq', Cypress.config().baseUrl + `/#/markets/all`);
cy.window().then((window) => {
expect(window.localStorage.getItem('vega_onboarding_viewed')).to.equal(
'true'
);
});
cy.getByTestId('wallet-dialog-title').should('contain.text', 'Connect');
}); });
}); });
@@ -185,7 +185,7 @@ describe('home', { tags: '@regression' }, () => {
cy.window().then((window) => { cy.window().then((window) => {
window.localStorage.setItem('marketId', 'market-1'); window.localStorage.setItem('marketId', 'market-1');
cy.visit('/'); cy.visit('/');
cy.wait('@Markets'); cy.getByTestId('dialog-close').click();
cy.location('hash').should('equal', '#/markets/market-1'); cy.location('hash').should('equal', '#/markets/market-1');
cy.getByTestId(dialogContent).should('not.exist'); cy.getByTestId(dialogContent).should('not.exist');
}); });
@@ -199,6 +199,7 @@ describe('home', { tags: '@regression' }, () => {
}); });
cy.visit('/'); cy.visit('/');
cy.wait('@Markets'); cy.wait('@Markets');
cy.getByTestId('dialog-close').click();
cy.location('hash').should('equal', '#/markets/market-not-existing'); cy.location('hash').should('equal', '#/markets/market-not-existing');
cy.getByTestId(dialogContent).should('not.exist'); cy.getByTestId(dialogContent).should('not.exist');
}); });
@@ -3,12 +3,13 @@ import type { MarketsQuery } from '@vegaprotocol/markets';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
const rowSelector = const rowSelector =
'[data-testid="tab-all-markets"] .ag-center-cols-container .ag-row'; '[data-testid="tab-open-markets"] .ag-center-cols-container .ag-row';
const colInstrumentCode = '[col-id="tradableInstrument.instrument.code"]'; const colInstrumentCode = '[col-id="tradableInstrument.instrument.code"]';
describe('markets all table', { tags: '@smoke' }, () => { describe('markets all table', { tags: '@smoke' }, () => {
beforeEach(() => { beforeEach(() => {
cy.clearLocalStorage().then(() => { cy.clearLocalStorage().then(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage( cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE, Schema.MarketState.STATE_ACTIVE,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION, Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
@@ -34,7 +35,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
'Settlement asset', 'Settlement asset',
'', '',
]; ];
cy.getByTestId('tab-all-markets').within(($headers) => { cy.getByTestId('tab-open-markets').within(($headers) => {
cy.wrap($headers) cy.wrap($headers)
.get('.ag-header-cell-text') .get('.ag-header-cell-text')
.each(($header, i) => { .each(($header, i) => {
@@ -44,7 +45,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
}); });
it('markets tab should be rendered properly', () => { it('markets tab should be rendered properly', () => {
cy.get('[data-testid="All markets"]').should( cy.get('[data-testid="Open markets"]').should(
'have.attr', 'have.attr',
'data-state', 'data-state',
'active' 'active'
@@ -166,7 +167,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
'ETHBTC.QM21', 'ETHBTC.QM21',
'SOLUSD', 'SOLUSD',
]; ];
cy.get('[data-testid="All markets"]').click({ force: true }); cy.get('[data-testid="Open markets"]').click({ force: true });
cy.url().should('eq', Cypress.config('baseUrl') + '/#/markets/all'); cy.url().should('eq', Cypress.config('baseUrl') + '/#/markets/all');
cy.contains('AAPL.MF21').should('be.visible'); cy.contains('AAPL.MF21').should('be.visible');
cy.get('.ag-header-cell-label').contains('Market').click(); // sort by market name cy.get('.ag-header-cell-label').contains('Market').click(); // sort by market name
@@ -191,7 +192,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
}); });
}); });
describe('no all markets', { tags: '@smoke', testIsolation: true }, () => { describe('no open markets', { tags: '@smoke', testIsolation: true }, () => {
before(() => { before(() => {
cy.mockTradingPage(); cy.mockTradingPage();
const markets: MarketsQuery = {}; const markets: MarketsQuery = {};
@@ -199,11 +200,12 @@ describe('no all markets', { tags: '@smoke', testIsolation: true }, () => {
aliasGQLQuery(req, 'Markets', markets); aliasGQLQuery(req, 'Markets', markets);
}); });
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all'); cy.visit('/#/markets/all');
}); });
it.skip('can see no markets message', () => { it.skip('can see no markets message', () => {
// 6001-MARK-048 // 6001-MARK-048
cy.getByTestId('tab-all-markets').should('contain.text', 'No markets'); cy.getByTestId('tab-open-markets').should('contain.text', 'No markets');
}); });
}); });
@@ -21,6 +21,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
}); });
before(() => { before(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage(MarketState.STATE_ACTIVE); cy.mockTradingPage(MarketState.STATE_ACTIVE);
cy.mockSubscription(); cy.mockSubscription();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
@@ -36,6 +36,7 @@ const headers = [
describe('liquidity table - trading', { tags: '@smoke' }, () => { describe('liquidity table - trading', { tags: '@smoke' }, () => {
before(() => { before(() => {
cy.setOnBoardingViewed();
cy.mockSubscription(); cy.mockSubscription();
cy.mockTradingPage( cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE, Schema.MarketState.STATE_ACTIVE,
@@ -119,6 +120,7 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
describe('liquidity table view', { tags: '@smoke' }, () => { describe('liquidity table view', { tags: '@smoke' }, () => {
before(() => { before(() => {
cy.setOnBoardingViewed();
cy.mockSubscription(); cy.mockSubscription();
cy.mockTradingPage( cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE, Schema.MarketState.STATE_ACTIVE,
@@ -12,6 +12,7 @@ describe('markets selector', { tags: '@smoke' }, () => {
cy.window().then((window) => { cy.window().then((window) => {
window.localStorage.setItem('marketId', 'market-1'); window.localStorage.setItem('marketId', 'market-1');
}); });
cy.setOnBoardingViewed();
cy.mockTradingPage( cy.mockTradingPage(
MarketState.STATE_ACTIVE, MarketState.STATE_ACTIVE,
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION, MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
@@ -30,6 +30,7 @@ describe('Market trading page', () => {
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
); );
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
cy.wait('@MarketData'); cy.wait('@MarketData');
cy.getByTestId(marketSummaryBlock).should('be.visible'); cy.getByTestId(marketSummaryBlock).should('be.visible');
@@ -9,6 +9,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
before(() => { before(() => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all'); cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click(); cy.get('[data-testid="Proposed markets"]').click();
}); });
@@ -100,7 +101,6 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
'VEGA_TOKEN_URL' 'VEGA_TOKEN_URL'
)}/proposals/e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829` )}/proposals/e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829`
); );
cy.getByTestId('proposal-actions-content').click();
}); });
// 6001-MARK-060 // 6001-MARK-060
@@ -214,11 +214,13 @@ describe('no markets proposed', { tags: '@smoke', testIsolation: true }, () => {
aliasGQLQuery(req, 'ProposalsList', proposal); aliasGQLQuery(req, 'ProposalsList', proposal);
}); });
cy.mockSubscription(); cy.mockSubscription();
cy.visit('/#/markets/all'); cy.setOnBoardingViewed();
cy.get('[data-testid="Proposed markets"]').click();
}); });
it('can see no markets message', () => { it('can see no markets message', () => {
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
// 6001-MARK-061 // 6001-MARK-061
cy.getByTestId('tab-proposed-markets').should('contain.text', 'No markets'); cy.getByTestId('tab-proposed-markets').should('contain.text', 'No markets');
}); });
@@ -12,6 +12,7 @@ describe('markets table', { tags: '@smoke' }, () => {
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
); );
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all'); cy.visit('/#/markets/all');
}); });
}); });
@@ -6,6 +6,7 @@ const oracleFullProfile = 'oracle-full-profile';
describe('oracle information', { tags: '@smoke' }, () => { describe('oracle information', { tags: '@smoke' }, () => {
before(() => { before(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage( cy.mockTradingPage(
MarketState.STATE_ACTIVE, MarketState.STATE_ACTIVE,
undefined, undefined,
@@ -14,6 +14,7 @@ const resPrice = 'price-990';
describe('order book', { tags: '@smoke' }, () => { describe('order book', { tags: '@smoke' }, () => {
before(() => { before(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
@@ -4,6 +4,7 @@ describe('Settings page', { tags: '@smoke' }, () => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/'); cy.visit('/');
// Only click if not already active otherwise sidebar will close // Only click if not already active otherwise sidebar will close
@@ -77,6 +77,7 @@ function getButtonSelectorByText(text: string): string {
beforeEach(() => { beforeEach(() => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
cy.wait('@Markets'); cy.wait('@Markets');
}); });
@@ -14,6 +14,7 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.clearAllLocalStorage(); cy.clearAllLocalStorage();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
cy.wait('@Markets'); cy.wait('@Markets');
}); });
@@ -21,37 +22,36 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
it('must show place order button and connect wallet if wallet is not connected', () => { it('must show place order button and connect wallet if wallet is not connected', () => {
// 0003-WTXN-001 // 0003-WTXN-001
cy.getByTestId('connect-vega-wallet'); // Not connected cy.getByTestId('connect-vega-wallet'); // Not connected
cy.getByTestId('order-connect-wallet').should('exist');
cy.getByTestId(placeOrderBtn).should('exist'); cy.getByTestId(placeOrderBtn).should('exist');
cy.getByTestId('deal-ticket-connect-wallet').should('exist'); cy.getByTestId('get-started-button').should('exist');
}); });
it('must be able to select order direction - long/short', function () { it('must be able to select order direction - long/short', function () {
// 7002-SORD-004 // 7002-SORD-004
cy.getByTestId(toggleShort).click().children('input').should('be.checked'); cy.getByTestId(toggleShort).click().next('input').should('be.checked');
cy.getByTestId(toggleLong).click().children('input').should('be.checked'); cy.getByTestId(toggleLong).click().next('input').should('be.checked');
}); });
it('must be able to select order type - limit/market', function () { it('must be able to select order type - limit/market', function () {
// 7002-SORD-005 // 7002-SORD-005
// 7002-SORD-006 // 7002-SORD-006
// 7002-SORD-007 // 7002-SORD-007
cy.getByTestId(toggleLimit).click().children('input').should('be.checked'); cy.getByTestId(toggleLimit).click().next('input').should('be.checked');
cy.getByTestId(toggleMarket).click().children('input').should('be.checked'); cy.getByTestId(toggleMarket).click().next('input').should('be.checked');
}); });
it('order connect vega wallet button should connect', () => { it('order connect vega wallet button should connect', () => {
mockConnectWallet(); mockConnectWallet();
cy.getByTestId(toggleLimit).click(); cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('101'); cy.getByTestId(orderPriceField).clear().type('101');
cy.getByTestId('order-connect-wallet').click(); cy.getByTestId('get-started-button').click();
cy.getByTestId('dialog-content').should('be.visible'); cy.getByTestId('dialog-content').should('be.visible');
cy.getByTestId('connectors-list') cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]') .find('[data-testid="connector-jsonRpc"]')
.click(); .click();
cy.wait('@walletReq'); cy.wait('@walletReq');
cy.getByTestId(placeOrderBtn).should('be.visible'); cy.getByTestId(placeOrderBtn).should('be.visible');
cy.getByTestId(toggleLimit).children('input').should('be.checked'); cy.getByTestId(toggleLimit).next('input').should('be.checked');
cy.getByTestId(orderPriceField).should('have.value', '101'); cy.getByTestId(orderPriceField).should('have.value', '101');
}); });
}); });
@@ -15,6 +15,7 @@ describe('time in force validation', { tags: '@smoke' }, () => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.clearAllLocalStorage(); cy.clearAllLocalStorage();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
cy.wait('@Markets'); cy.wait('@Markets');
}); });
@@ -9,6 +9,7 @@ describe('trades', { tags: '@smoke' }, () => {
before(() => { before(() => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.intercept('POST', '/graphql', (req) => { cy.intercept('POST', '/graphql', (req) => {
if (req.body.operationName === 'Trades') { if (req.body.operationName === 'Trades') {
req.alias = '@Trades'; req.alias = '@Trades';
@@ -17,6 +17,7 @@ describe(
cy.visit('/#/portfolio'); cy.visit('/#/portfolio');
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.get('[data-testid="pathname-/portfolio"]').should('exist'); cy.get('[data-testid="pathname-/portfolio"]').should('exist');
}); });
@@ -122,6 +123,7 @@ describe('connect vega wallet', { tags: '@smoke', testIsolation: true }, () => {
cy.visit('/#/portfolio'); cy.visit('/#/portfolio');
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.get('[data-testid="pathname-/portfolio"]').should('exist'); cy.get('[data-testid="pathname-/portfolio"]').should('exist');
}); });
@@ -0,0 +1,191 @@
import { selectAsset } from '../support/helpers';
const amountField = 'input[name="amount"]';
const transferText = 'transfer-intro-text';
const errorText = 'input-error-text';
const formFieldError = 'input-error-text';
const includeTransferFeeRadioBtn = 'include-transfer-fee';
const keyID = `[data-testid="${transferText}"] > .rounded-md`;
const manageVegaWallet = 'manage-vega-wallet';
const submitTransferBtn = '[type="submit"]';
const toAddressField = '[name="toAddress"]';
const totalTransferfee = 'total-transfer-fee';
const transferAmount = 'transfer-amount';
const transferForm = 'transfer-form';
const transferFee = 'transfer-fee';
const walletTransfer = 'wallet-transfer';
const ASSET_EURO = 1;
const ASSET_SEPOLIA_TBTC = 2;
describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Assets');
cy.wait('@Accounts');
cy.mockVegaWalletTransaction();
});
it('transfer fees tooltips', () => {
// 1003-TRAN-015
// 1003-TRAN-016
// 1003-TRAN-017
// 1003-TRAN-018
// 1003-TRAN-019
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
/// Check Include Transfer Fee tooltip
cy.get('label[for="include-transfer-fee"] div').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Transfer Fee tooltip
cy.contains('div', 'Transfer fee').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Amount to be transferred tooltip
cy.contains('div', 'Amount to be transferred').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Total amount (with fee) tooltip
cy.contains('div', 'Total amount (with fee)').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
});
it('transfer fees', () => {
// 1003-TRAN-020
// 1003-TRAN-021
// 1003-TRAN-022
// 1003-TRAN-023
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(includeTransferFeeRadioBtn).should('be.disabled');
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '1.00');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.01');
cy.getByTestId(includeTransferFeeRadioBtn).click();
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '0.99');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.00');
});
});
describe(
'transfer form validation',
{ tags: '@regression', testIsolation: true },
() => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Accounts');
cy.wait('@Assets');
});
it('transfer Text', () => {
// 1003-TRAN-003
cy.getByTestId(transferText)
.should('exist')
.get(keyID)
.invoke('text')
.should('match', /[\w.]{6}…[\w.]{6}/);
});
it('invalid vega key validation', () => {
//1003-TRAN-013
//1003-TRAN-004
cy.getByTestId(transferForm).should('be.visible');
cy.contains('Enter manually').click();
cy.getByTestId(transferForm).find(toAddressField).type('asd');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Invalid Vega key');
cy.contains('label', 'Vega key').should('be.visible');
cy.contains('label', 'Asset').should('be.visible');
cy.contains('label', 'Amount').should('be.visible');
});
it('empty fields', () => {
// 1003-TRAN-012
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
cy.getByTestId(formFieldError).should('have.length', 3);
});
it('min amount', () => {
// 1002-WITH-010
// 1003-TRAN-014
selectAsset(ASSET_SEPOLIA_TBTC);
cy.get(amountField).clear().type('0');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'Value is below minimum'
);
});
it('max amount', () => {
// 1003-TRAN-002
// 1003-TRAN-011
// 1003-TRAN-002
selectAsset(ASSET_EURO); // Will be above maximum because the vega wallet doesn't have any collateral
cy.get(amountField).clear().type('1001', { delay: 100 });
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'You cannot transfer more than your available collateral'
);
});
}
);
@@ -5,205 +5,15 @@ const amountShortName = 'input[name="amount"] + div + span.text-xs';
const assetSelection = 'select-asset'; const assetSelection = 'select-asset';
const assetBalance = 'asset-balance'; const assetBalance = 'asset-balance';
const assetOption = 'rich-select-option'; const assetOption = 'rich-select-option';
const transferText = 'transfer-intro-text';
const errorText = 'input-error-text';
const formFieldError = 'input-error-text';
const includeTransferFeeRadioBtn = 'include-transfer-fee';
const keyID = `[data-testid="${transferText}"] > .rounded-md`;
const manageVegaWallet = 'manage-vega-wallet';
const openTransferButton = 'open-transfer'; const openTransferButton = 'open-transfer';
const submitTransferBtn = '[type="submit"]'; const submitTransferBtn = '[type="submit"]';
const toAddressField = '[name="toAddress"]'; const toAddressField = '[name="toAddress"]';
const totalTransferfee = 'total-transfer-fee';
const transferAmount = 'transfer-amount';
const transferForm = 'transfer-form'; const transferForm = 'transfer-form';
const transferFee = 'transfer-fee';
const walletTransfer = 'wallet-transfer';
const ASSET_EURO = 1;
const ASSET_SEPOLIA_TBTC = 2; const ASSET_SEPOLIA_TBTC = 2;
const collateralTab = 'Collateral'; const collateralTab = 'Collateral';
const toastCloseBtn = 'toast-close'; const toastCloseBtn = 'toast-close';
const toastContent = 'toast-content'; const toastContent = 'toast-content';
describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/');
cy.wait('@Assets');
cy.wait('@Accounts');
cy.mockVegaWalletTransaction();
// Only click if not already active otherwise sidebar will close
cy.get('[data-testid="sidebar-content"]').then(($sidebarContent) => {
if ($sidebarContent.find('h2').text() !== 'Transfer') {
cy.get('[data-testid="sidebar"] [data-testid="Transfer"]').click();
}
});
});
it('transfer fees tooltips', () => {
// 1003-TRAN-015
// 1003-TRAN-016
// 1003-TRAN-017
// 1003-TRAN-018
// 1003-TRAN-019
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
/// Check Include Transfer Fee tooltip
cy.get('label[for="include-transfer-fee"] div').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Transfer Fee tooltip
cy.contains('div', 'Transfer fee').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Amount to be transferred tooltip
cy.contains('div', 'Amount to be transferred').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Total amount (with fee) tooltip
cy.contains('div', 'Total amount (with fee)').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
});
it('transfer fees', () => {
// 1003-TRAN-020
// 1003-TRAN-021
// 1003-TRAN-022
// 1003-TRAN-023
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(includeTransferFeeRadioBtn).should('be.disabled');
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '1.00');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.01');
cy.getByTestId(includeTransferFeeRadioBtn).click();
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '0.99');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.00');
});
});
describe(
'transfer form validation',
{ tags: '@regression', testIsolation: true },
() => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Accounts');
cy.wait('@Assets');
});
it('transfer Text', () => {
// 1003-TRAN-003
cy.getByTestId(transferText)
.should('exist')
.get(keyID)
.invoke('text')
.should('match', /[\w.]{6}…[\w.]{6}/);
});
it('invalid vega key validation', () => {
//1003-TRAN-013
//1003-TRAN-004
cy.getByTestId(transferForm).should('be.visible');
cy.contains('Enter manually').click();
cy.getByTestId(transferForm).find(toAddressField).type('asd');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Invalid Vega key');
cy.contains('label', 'Vega key').should('be.visible');
cy.contains('label', 'Asset').should('be.visible');
cy.contains('label', 'Amount').should('be.visible');
});
it('empty fields', () => {
// 1003-TRAN-012
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
cy.getByTestId(formFieldError).should('have.length', 3);
});
it('min amount', () => {
// 1002-WITH-010
// 1003-TRAN-014
selectAsset(ASSET_SEPOLIA_TBTC);
cy.get(amountField).clear().type('0');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'Value is below minimum'
);
});
it('max amount', () => {
// 1003-TRAN-002
// 1003-TRAN-011
// 1003-TRAN-002
selectAsset(ASSET_EURO); // Will be above maximum because the vega wallet doesn't have any collateral
cy.get(amountField).clear().type('1001', { delay: 100 });
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'You cannot transfer more than your available collateral'
);
});
}
);
describe('withdraw actions', { tags: '@smoke', testIsolation: true }, () => { describe('withdraw actions', { tags: '@smoke', testIsolation: true }, () => {
beforeEach(() => { beforeEach(() => {
cy.mockWeb3Provider(); cy.mockWeb3Provider();
+4 -1
View File
@@ -1,3 +1,4 @@
import { OrderType } from '@vegaprotocol/types';
import type { OrderSubmission } from '@vegaprotocol/wallet'; import type { OrderSubmission } from '@vegaprotocol/wallet';
const orderSizeField = 'order-size'; const orderSizeField = 'order-size';
@@ -9,7 +10,9 @@ export const createOrder = (order: OrderSubmission): void => {
cy.log('Placing order', order); cy.log('Placing order', order);
const { type, side, size, price, timeInForce, expiresAt } = order; const { type, side, size, price, timeInForce, expiresAt } = order;
cy.getByTestId(`order-type-${type}`).click(); cy.getByTestId(
`order-type-${type === OrderType.TYPE_LIMIT ? 'Limit' : 'Market'}`
).click();
cy.getByTestId(`order-side-${side}`).click(); cy.getByTestId(`order-side-${side}`).click();
cy.getByTestId(orderSizeField).clear().type(size); cy.getByTestId(orderSizeField).clear().type(size);
if (price) { if (price) {
+2 -2
View File
@@ -6,8 +6,8 @@ export const orderTIFDropDown = 'order-tif';
export const placeOrderBtn = 'place-order'; export const placeOrderBtn = 'place-order';
export const toggleShort = 'order-side-SIDE_SELL'; export const toggleShort = 'order-side-SIDE_SELL';
export const toggleLong = 'order-side-SIDE_BUY'; export const toggleLong = 'order-side-SIDE_BUY';
export const toggleLimit = 'order-type-TYPE_LIMIT'; export const toggleLimit = 'order-type-Limit';
export const toggleMarket = 'order-type-TYPE_MARKET'; export const toggleMarket = 'order-type-Market';
export const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => { export const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
return { return {
+1 -1
View File
@@ -16,6 +16,6 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
# Cosmic elevator flags # Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true NX_SUCCESSOR_MARKETS=true
# NX_STOP_ORDERS NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS # NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS # NX_PRODUCT_PERPETUALS
+1 -1
View File
@@ -18,6 +18,6 @@ NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supp
# Cosmic elevator flags # Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false NX_SUCCESSOR_MARKETS=false
# NX_STOP_ORDERS NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS # NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS # NX_PRODUCT_PERPETUALS
+1 -1
View File
@@ -16,6 +16,6 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
# Cosmic elevator flags # Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true NX_SUCCESSOR_MARKETS=true
# NX_STOP_ORDERS NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS # NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS # NX_PRODUCT_PERPETUALS
+2 -2
View File
@@ -18,6 +18,6 @@ NX_APP_VERSION=v0.20.21-core-0.71.6
# Cosmic elevator flags # Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false NX_SUCCESSOR_MARKETS=false
# NX_STOP_ORDERS NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS # NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS # NX_PRODUCT_PERPETUALS
+1 -1
View File
@@ -18,6 +18,6 @@ NX_APP_VERSION=v0.20.19-core-0.71.6
# Cosmic elevator flags # Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false NX_SUCCESSOR_MARKETS=false
# NX_STOP_ORDERS NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS # NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS # NX_PRODUCT_PERPETUALS
+1 -1
View File
@@ -16,6 +16,6 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
# Cosmic elevator flags # Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true NX_SUCCESSOR_MARKETS=true
# NX_STOP_ORDERS NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS # NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS # NX_PRODUCT_PERPETUALS
+2 -2
View File
@@ -17,6 +17,6 @@ NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
# Cosmic elevator flags # Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true NX_SUCCESSOR_MARKETS=true
# NX_STOP_ORDERS NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS # NX_PRODUCT_PERPETUALS
+1 -1
View File
@@ -17,6 +17,6 @@ NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks
# Cosmic elevator flags # Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false NX_SUCCESSOR_MARKETS=false
# NX_STOP_ORDERS NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS # NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS # NX_PRODUCT_PERPETUALS
@@ -0,0 +1,19 @@
import { DepositContainer } from '@vegaprotocol/deposits';
import { GetStarted } from '../../components/welcome-dialog';
import { t } from '@vegaprotocol/i18n';
export const Deposit = () => {
return (
<div className="py-16 px-8 flex w-full justify-center">
<div className="lg:min-w-[700px] min-w-[300px] max-w-[700px]">
<h1 className="text-4xl xl:text-5xl uppercase font-alpha calt">
{t('Deposit')}
</h1>
<div className="mt-10">
<DepositContainer />
<GetStarted />
</div>
</div>
</div>
);
};
@@ -0,0 +1,3 @@
import { Deposit } from './deposit';
export default Deposit;
-1
View File
@@ -23,7 +23,6 @@ export const Home = () => {
replace: true, replace: true,
}); });
} else if (data) { } else if (data) {
update({ shouldDisplayWelcomeDialog: true });
const marketDataId = data[0]?.id; const marketDataId = data[0]?.id;
if (marketDataId) { if (marketDataId) {
navigate(Links[Routes.MARKET](marketDataId), { navigate(Links[Routes.MARKET](marketDataId), {
+35 -41
View File
@@ -10,7 +10,6 @@ import type { Market } from '@vegaprotocol/markets';
import { Filter } from '@vegaprotocol/orders'; import { Filter } from '@vegaprotocol/orders';
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit'; import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler'; import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { import {
ResizableGrid, ResizableGrid,
ResizableGridPanel, ResizableGridPanel,
@@ -58,7 +57,11 @@ const MainGrid = memo(
> >
<TradeGridChild> <TradeGridChild>
<Tabs storageKey="console-trade-grid-main-left"> <Tabs storageKey="console-trade-grid-main-left">
<Tab id="chart" name={t('Chart')}> <Tab
id="chart"
name={t('Chart')}
menu={<TradingViews.candles.menu />}
>
<TradingViews.candles.component marketId={marketId} /> <TradingViews.candles.component marketId={marketId} />
</Tab> </Tab>
<Tab id="depth" name={t('Depth')}> <Tab id="depth" name={t('Depth')}>
@@ -95,57 +98,48 @@ const MainGrid = memo(
<TradeGridChild> <TradeGridChild>
<Tabs storageKey="console-trade-grid-bottom"> <Tabs storageKey="console-trade-grid-bottom">
<Tab id="positions" name={t('Positions')}> <Tab id="positions" name={t('Positions')}>
<VegaWalletContainer> <TradingViews.positions.component
<TradingViews.positions.component onMarketClick={onMarketClick}
onMarketClick={onMarketClick} />
/>
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="open-orders" name={t('Open')}> <Tab id="open-orders" name={t('Open')}>
<VegaWalletContainer> <TradingViews.orders.component
<TradingViews.orders.component marketId={marketId}
marketId={marketId} filter={Filter.Open}
filter={Filter.Open} />
/>
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="closed-orders" name={t('Closed')}> <Tab id="closed-orders" name={t('Closed')}>
<VegaWalletContainer> <TradingViews.orders.component
<TradingViews.orders.component marketId={marketId}
marketId={marketId} filter={Filter.Closed}
filter={Filter.Closed} />
/>
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="rejected-orders" name={t('Rejected')}> <Tab id="rejected-orders" name={t('Rejected')}>
<VegaWalletContainer> <TradingViews.orders.component
<TradingViews.orders.component marketId={marketId}
marketId={marketId} filter={Filter.Rejected}
filter={Filter.Rejected} />
/>
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="orders" name={t('All')}> <Tab id="orders" name={t('All')}>
<VegaWalletContainer> <TradingViews.orders.component marketId={marketId} />
<TradingViews.orders.component marketId={marketId} />
</VegaWalletContainer>
</Tab> </Tab>
{FLAGS.STOP_ORDERS ? (
<Tab id="stop-orders" name={t('Stop orders')}>
<TradingViews.stopOrders.component />
</Tab>
) : null}
<Tab id="fills" name={t('Fills')}> <Tab id="fills" name={t('Fills')}>
<VegaWalletContainer> <TradingViews.fills.component
<TradingViews.fills.component marketId={marketId}
marketId={marketId} onMarketClick={onMarketClick}
onMarketClick={onMarketClick} />
/>
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="accounts" name={t('Collateral')}> <Tab id="accounts" name={t('Collateral')}>
<VegaWalletContainer> <TradingViews.collateral.component
<TradingViews.collateral.component pinnedAsset={pinnedAsset}
pinnedAsset={pinnedAsset} onMarketClick={onMarketClick}
onMarketClick={onMarketClick} hideButtons
hideButtons />
/>
</VegaWalletContainer>
</Tab> </Tab>
</Tabs> </Tabs>
</TradeGridChild> </TradeGridChild>
@@ -37,6 +37,7 @@ export const TradePanels = ({
const onOrderTypeClick = useMarketLiquidityClickHandler(); const onOrderTypeClick = useMarketLiquidityClickHandler();
const [view, setView] = useState<TradingView>('candles'); const [view, setView] = useState<TradingView>('candles');
const renderView = () => { const renderView = () => {
const Component = memo<{ const Component = memo<{
marketId: string; marketId: string;
@@ -65,8 +66,23 @@ export const TradePanels = ({
); );
}; };
const renderMenu = () => {
const viewCfg = TradingViews[view];
if ('menu' in viewCfg) {
const Menu = viewCfg.menu;
return (
<div className="flex gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
<Menu />
</div>
);
}
return null;
};
return ( return (
<div className="h-full grid grid-rows-[min-content_1fr_min-content]"> <div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
<div> <div>
{FLAGS.SUCCESSOR_MARKETS && ( {FLAGS.SUCCESSOR_MARKETS && (
<> <>
@@ -76,6 +92,7 @@ export const TradePanels = ({
)} )}
<OracleBanner marketId={market?.id || ''} /> <OracleBanner marketId={market?.id || ''} />
</div> </div>
<div>{renderMenu()}</div>
<div className="h-full"> <div className="h-full">
<AutoSizer> <AutoSizer>
{({ width, height }) => ( {({ width, height }) => (
@@ -2,7 +2,10 @@ import type { ComponentProps } from 'react';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { TradesContainer } from '@vegaprotocol/trades'; import { TradesContainer } from '@vegaprotocol/trades';
import { DepthChartContainer } from '@vegaprotocol/market-depth'; import { DepthChartContainer } from '@vegaprotocol/market-depth';
import { CandlesChartContainer } from '@vegaprotocol/candles-chart'; import {
CandlesChartContainer,
CandlesMenu,
} from '@vegaprotocol/candles-chart';
import { Filter } from '@vegaprotocol/orders'; import { Filter } from '@vegaprotocol/orders';
import { NO_MARKET } from './constants'; import { NO_MARKET } from './constants';
import { OrderbookContainer } from '../../components/orderbook-container'; import { OrderbookContainer } from '../../components/orderbook-container';
@@ -12,6 +15,7 @@ import { AccountsContainer } from '../../components/accounts-container';
import { LiquidityContainer } from '../../components/liquidity-container'; import { LiquidityContainer } from '../../components/liquidity-container';
import type { OrderContainerProps } from '../../components/orders-container'; import type { OrderContainerProps } from '../../components/orders-container';
import { OrdersContainer } from '../../components/orders-container'; import { OrdersContainer } from '../../components/orders-container';
import { StopOrdersContainer } from '../../components/stop-orders-container';
type MarketDependantView = type MarketDependantView =
| typeof CandlesChartContainer | typeof CandlesChartContainer
@@ -34,6 +38,7 @@ export const TradingViews = {
candles: { candles: {
label: 'Candles', label: 'Candles',
component: requiresMarket(CandlesChartContainer), component: requiresMarket(CandlesChartContainer),
menu: CandlesMenu,
}, },
depth: { depth: {
label: 'Depth', label: 'Depth',
@@ -74,6 +79,10 @@ export const TradingViews = {
label: 'All', label: 'All',
component: OrdersContainer, component: OrdersContainer,
}, },
stopOrders: {
label: 'Stop',
component: StopOrdersContainer,
},
collateral: { label: 'Collateral', component: AccountsContainer }, collateral: { label: 'Collateral', component: AccountsContainer },
fills: { label: 'Fills', component: FillsContainer }, fills: { label: 'Fills', component: FillsContainer },
}; };
@@ -18,7 +18,7 @@ export const MarketsPage = () => {
<div className="h-full pt-0.5 pb-3 px-1.5"> <div className="h-full pt-0.5 pb-3 px-1.5">
<div className="h-full my-1 border border-default rounded-sm"> <div className="h-full my-1 border border-default rounded-sm">
<Tabs storageKey="console-markets"> <Tabs storageKey="console-markets">
<Tab id="all-markets" name={t('All markets')}> <Tab id="open-markets" name={t('Open markets')}>
<Markets /> <Markets />
</Tab> </Tab>
<Tab id="proposed-markets" name={t('Proposed markets')}> <Tab id="proposed-markets" name={t('Proposed markets')}>
@@ -71,7 +71,7 @@ export const AccountHistoryContainer = () => {
const { data: assets } = useAssetsDataProvider(); const { data: assets } = useAssetsDataProvider();
if (!pubKey) { if (!pubKey) {
return <Splash>{t('Connect wallet')}</Splash>; return <Splash>{t('Please connect Vega wallet')}</Splash>;
} }
return ( return (
@@ -1,15 +1,12 @@
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import { DepositsTable } from '@vegaprotocol/deposits'; import { DepositsTable } from '@vegaprotocol/deposits';
import { depositsProvider } from '@vegaprotocol/deposits'; import { depositsProvider } from '@vegaprotocol/deposits';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { useSidebar, ViewType } from '../../components/sidebar'; import { useSidebar, ViewType } from '../../components/sidebar';
export const DepositsContainer = () => { export const DepositsContainer = () => {
const gridRef = useRef<AgGridReact | null>(null);
const { pubKey, isReadOnly } = useVegaWallet(); const { pubKey, isReadOnly } = useVegaWallet();
const { data, error } = useDataProvider({ const { data, error } = useDataProvider({
dataProvider: depositsProvider, dataProvider: depositsProvider,
@@ -17,11 +14,13 @@ export const DepositsContainer = () => {
skip: !pubKey, skip: !pubKey,
}); });
const setView = useSidebar((store) => store.setView); const setView = useSidebar((store) => store.setView);
if (!pubKey) {
return <Splash>{t('Please connect Vega wallet')}</Splash>;
}
return ( return (
<div className="h-full"> <div className="h-full">
<DepositsTable <DepositsTable
rowData={data} rowData={data}
ref={gridRef}
overlayNoRowsTemplate={error ? error.message : t('No deposits')} overlayNoRowsTemplate={error ? error.message : t('No deposits')}
/> />
{!isReadOnly && ( {!isReadOnly && (
@@ -13,7 +13,6 @@ import { FillsContainer } from '../../components/fills-container';
import { PositionsContainer } from '../../components/positions-container'; import { PositionsContainer } from '../../components/positions-container';
import { WithdrawalsContainer } from './withdrawals-container'; import { WithdrawalsContainer } from './withdrawals-container';
import { OrdersContainer } from '../../components/orders-container'; import { OrdersContainer } from '../../components/orders-container';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { LedgerContainer } from '../../components/ledger-container'; import { LedgerContainer } from '../../components/ledger-container';
import { AccountHistoryContainer } from './account-history-container'; import { AccountHistoryContainer } from './account-history-container';
import { import {
@@ -62,29 +61,19 @@ export const Portfolio = () => {
<PortfolioGridChild> <PortfolioGridChild>
<Tabs storageKey="console-portfolio-top"> <Tabs storageKey="console-portfolio-top">
<Tab id="account-history" name={t('Account history')}> <Tab id="account-history" name={t('Account history')}>
<VegaWalletContainer> <AccountHistoryContainer />
<AccountHistoryContainer />
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="positions" name={t('Positions')}> <Tab id="positions" name={t('Positions')}>
<VegaWalletContainer> <PositionsContainer onMarketClick={onMarketClick} allKeys />
<PositionsContainer onMarketClick={onMarketClick} allKeys />
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="orders" name={t('Orders')}> <Tab id="orders" name={t('Orders')}>
<VegaWalletContainer> <OrdersContainer />
<OrdersContainer />
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="fills" name={t('Fills')}> <Tab id="fills" name={t('Fills')}>
<VegaWalletContainer> <FillsContainer onMarketClick={onMarketClick} />
<FillsContainer onMarketClick={onMarketClick} />
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="ledger-entries" name={t('Ledger entries')}> <Tab id="ledger-entries" name={t('Ledger entries')}>
<VegaWalletContainer> <LedgerContainer />
<LedgerContainer />
</VegaWalletContainer>
</Tab> </Tab>
</Tabs> </Tabs>
</PortfolioGridChild> </PortfolioGridChild>
@@ -97,14 +86,10 @@ export const Portfolio = () => {
<PortfolioGridChild> <PortfolioGridChild>
<Tabs storageKey="console-portfolio-bottom"> <Tabs storageKey="console-portfolio-bottom">
<Tab id="collateral" name={t('Collateral')}> <Tab id="collateral" name={t('Collateral')}>
<VegaWalletContainer> <AccountsContainer />
<AccountsContainer />
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="deposits" name={t('Deposits')}> <Tab id="deposits" name={t('Deposits')}>
<VegaWalletContainer> <DepositsContainer />
<DepositsContainer />
</VegaWalletContainer>
</Tab> </Tab>
<Tab <Tab
id="withdrawals" id="withdrawals"
@@ -1,4 +1,4 @@
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import { import {
withdrawalProvider, withdrawalProvider,
WithdrawalsTable, WithdrawalsTable,
@@ -7,7 +7,6 @@ import {
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { ViewType, useSidebar } from '../../components/sidebar'; import { ViewType, useSidebar } from '../../components/sidebar';
export const WithdrawalsContainer = () => { export const WithdrawalsContainer = () => {
@@ -19,9 +18,11 @@ export const WithdrawalsContainer = () => {
}); });
const setView = useSidebar((store) => store.setView); const setView = useSidebar((store) => store.setView);
const { ready, delayed } = useIncompleteWithdrawals(); const { ready, delayed } = useIncompleteWithdrawals();
if (!pubKey) {
return <Splash>{t('Please connect Vega wallet')}</Splash>;
}
return ( return (
<VegaWalletContainer> <>
<div className="h-full relative"> <div className="h-full relative">
<WithdrawalsTable <WithdrawalsTable
data-testid="withdrawals-history" data-testid="withdrawals-history"
@@ -43,6 +44,6 @@ export const WithdrawalsContainer = () => {
</Button> </Button>
</div> </div>
)} )}
</VegaWalletContainer> </>
); );
}; };
@@ -1,8 +1,8 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import type { PinnedAsset } from '@vegaprotocol/accounts'; import type { PinnedAsset } from '@vegaprotocol/accounts';
import { AccountManager } from '@vegaprotocol/accounts'; import { AccountManager } from '@vegaprotocol/accounts';
+1 -1
View File
@@ -1,7 +1,7 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
export const THROTTLE_UPDATE_TIME = 500; export const THROTTLE_UPDATE_TIME = 500;
export const RISK_ACCEPTED_KEY = 'vega_risk_accepted'; export const ONBOARDING_VIEWED_KEY = 'vega_onboarding_viewed';
export const MAINNET_WELCOME_HEADER = t( export const MAINNET_WELCOME_HEADER = t(
'Trade cash settled futures on the fully decentralised Vega network.' 'Trade cash settled futures on the fully decentralised Vega network.'
); );
@@ -1,10 +1,10 @@
import { t } from '@vegaprotocol/i18n';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { FillsManager } from '@vegaprotocol/fills'; import { FillsManager } from '@vegaprotocol/fills';
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import { useDataGridEvents } from '@vegaprotocol/datagrid'; import { useDataGridEvents } from '@vegaprotocol/datagrid';
import { t } from '@vegaprotocol/i18n';
import { Splash } from '@vegaprotocol/ui-toolkit';
import type { DataGridSlice } from '../../stores/datagrid-store-slice'; import type { DataGridSlice } from '../../stores/datagrid-store-slice';
import { createDataGridSlice } from '../../stores/datagrid-store-slice'; import { createDataGridSlice } from '../../stores/datagrid-store-slice';
@@ -12,10 +12,9 @@ import {
NetworkParams, NetworkParams,
useNetworkParams, useNetworkParams,
} from '@vegaprotocol/network-parameters'; } from '@vegaprotocol/network-parameters';
import type { AgGridReact } from 'ag-grid-react';
import type { DataGridSlice } from '../../stores/datagrid-store-slice'; import type { DataGridSlice } from '../../stores/datagrid-store-slice';
import { createDataGridSlice } from '../../stores/datagrid-store-slice'; import { createDataGridSlice } from '../../stores/datagrid-store-slice';
import { useEffect, useRef } from 'react'; import { useEffect } from 'react';
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
@@ -26,8 +25,6 @@ export const LiquidityContainer = ({
marketId: string | undefined; marketId: string | undefined;
filter?: Filter; filter?: Filter;
}) => { }) => {
const gridRef = useRef<AgGridReact | null>(null);
const gridStore = useLiquidityStore((store) => store.gridStore); const gridStore = useLiquidityStore((store) => store.gridStore);
const updateGridStore = useLiquidityStore((store) => store.updateGridStore); const updateGridStore = useLiquidityStore((store) => store.updateGridStore);
@@ -60,7 +57,6 @@ export const LiquidityContainer = ({
return ( return (
<div className="h-full relative"> <div className="h-full relative">
<LiquidityTable <LiquidityTable
ref={gridRef}
rowData={data} rowData={data}
symbol={symbol} symbol={symbol}
assetDecimalPlaces={assetDecimalPlaces} assetDecimalPlaces={assetDecimalPlaces}
@@ -52,6 +52,9 @@ export const Navbar = ({
'bg-vega-yellow text-vega-clight-50': theme === 'yellow', 'bg-vega-yellow text-vega-clight-50': theme === 'yellow',
'text-default': theme === 'system', 'text-default': theme === 'system',
})} })}
style={{
background: theme === 'yellow' ? 'url(/testnet-logo-bg.png' : 'none',
}}
> >
<VLogo className="w-4" /> <VLogo className="w-4" />
</NavLink> </NavLink>
@@ -1,10 +1,14 @@
import { OrderbookManager } from '@vegaprotocol/market-depth'; import { OrderbookManager } from '@vegaprotocol/market-depth';
import { useCreateOrderStore } from '@vegaprotocol/orders'; import { useCreateOrderStore } from '@vegaprotocol/orders';
import { ViewType, useSidebar } from '../sidebar'; import { ViewType, useSidebar } from '../sidebar';
import { useStopOrderFormValues } from '@vegaprotocol/deal-ticket';
export const OrderbookContainer = ({ marketId }: { marketId: string }) => { export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
const useOrderStoreRef = useCreateOrderStore(); const useOrderStoreRef = useCreateOrderStore();
const updateOrder = useOrderStoreRef((store) => store.update); const updateOrder = useOrderStoreRef((store) => store.update);
const updateStoredFormValues = useStopOrderFormValues(
(state) => state.update
);
const setView = useSidebar((store) => store.setView); const setView = useSidebar((store) => store.setView);
return ( return (
<OrderbookManager <OrderbookManager
@@ -12,9 +16,11 @@ export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
onClick={({ price, size }) => { onClick={({ price, size }) => {
if (price) { if (price) {
updateOrder(marketId, { price }); updateOrder(marketId, { price });
updateStoredFormValues(marketId, { price });
} }
if (size) { if (size) {
updateOrder(marketId, { size }); updateOrder(marketId, { size });
updateStoredFormValues(marketId, { size });
} }
setView({ type: ViewType.Order }); setView({ type: ViewType.Order });
}} }}
@@ -1,7 +1,6 @@
import { useDataGridEvents } from '@vegaprotocol/datagrid'; import { useDataGridEvents } from '@vegaprotocol/datagrid';
import { Filter, OrderListManager } from '@vegaprotocol/orders';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { Filter } from '@vegaprotocol/orders';
import { OrderListManager } from '@vegaprotocol/orders';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { import {
@@ -1,7 +1,15 @@
import { act, render, screen } from '@testing-library/react'; import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { Sidebar, SidebarContent, ViewType, useSidebar } from './sidebar'; import {
Sidebar,
SidebarButton,
SidebarContent,
ViewType,
useSidebar,
} from './sidebar';
import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
jest.mock('../node-health', () => ({ jest.mock('../node-health', () => ({
NodeHealthContainer: () => <span data-testid="node-health" />, NodeHealthContainer: () => <span data-testid="node-health" />,
@@ -19,16 +27,23 @@ jest.mock('../settings', () => ({
Settings: () => <div data-testid="settings" />, Settings: () => <div data-testid="settings" />,
})); }));
jest.mock('../welcome-dialog', () => ({
GetStarted: () => <div data-testid="get-started" />,
}));
describe('Sidebar', () => { describe('Sidebar', () => {
it.each(['/markets/all', '/portfolio'])( it.each(['/markets/all', '/portfolio'])(
'does not render ticket and info', 'does not render ticket and info',
(path) => { (path) => {
render( render(
<MemoryRouter initialEntries={[path]}> <VegaWalletProvider>
<Sidebar /> <MemoryRouter initialEntries={[path]}>
</MemoryRouter> <Sidebar />
</MemoryRouter>
</VegaWalletProvider>
); );
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
@@ -43,11 +58,14 @@ describe('Sidebar', () => {
it('renders ticket and info on market pages', () => { it('renders ticket and info on market pages', () => {
render( render(
<MemoryRouter initialEntries={['/markets/ABC']}> <VegaWalletProvider>
<Sidebar /> <MemoryRouter initialEntries={['/markets/ABC']}>
</MemoryRouter> <Sidebar />
</MemoryRouter>
</VegaWalletProvider>
); );
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
@@ -61,9 +79,11 @@ describe('Sidebar', () => {
it('renders selected state', async () => { it('renders selected state', async () => {
render( render(
<MemoryRouter initialEntries={['/markets/ABC']}> <VegaWalletProvider>
<Sidebar /> <MemoryRouter initialEntries={['/markets/ABC']}>
</MemoryRouter> <Sidebar />
</MemoryRouter>
</VegaWalletProvider>
); );
const settingsButton = screen.getByTestId(ViewType.Settings); const settingsButton = screen.getByTestId(ViewType.Settings);
@@ -87,11 +107,13 @@ describe('Sidebar', () => {
describe('SidebarContent', () => { describe('SidebarContent', () => {
it('renders the correct content', () => { it('renders the correct content', () => {
const { container } = render( const { container } = render(
<MemoryRouter initialEntries={['/markets/ABC']}> <VegaWalletProvider>
<Routes> <MemoryRouter initialEntries={['/markets/ABC']}>
<Route path="/markets/:marketId" element={<SidebarContent />} /> <Routes>
</Routes> <Route path="/markets/:marketId" element={<SidebarContent />} />
</MemoryRouter> </Routes>
</MemoryRouter>
</VegaWalletProvider>
); );
expect(container).toBeEmptyDOMElement(); expect(container).toBeEmptyDOMElement();
@@ -111,11 +133,13 @@ describe('SidebarContent', () => {
it('closes sidebar if market id is required but not present', () => { it('closes sidebar if market id is required but not present', () => {
const { container } = render( const { container } = render(
<MemoryRouter initialEntries={['/portfolio']}> <VegaWalletProvider>
<Routes> <MemoryRouter initialEntries={['/portfolio']}>
<Route path="/portfolio" element={<SidebarContent />} /> <Routes>
</Routes> <Route path="/portfolio" element={<SidebarContent />} />
</MemoryRouter> </Routes>
</MemoryRouter>
</VegaWalletProvider>
); );
act(() => { act(() => {
@@ -137,3 +161,25 @@ describe('SidebarContent', () => {
expect(container).toBeEmptyDOMElement(); expect(container).toBeEmptyDOMElement();
}); });
}); });
describe('SidebarButton', () => {
it.each([ViewType.Info, ViewType.Deposit, ViewType.ViewAs])(
'runs given callback regardless of requested view (%s)',
async (view) => {
const onClick = jest.fn();
render(
<SidebarButton
icon={VegaIconNames.INFO}
tooltip="INFO"
onClick={onClick}
view={view}
/>
);
const btn = screen.getByTestId(view);
await userEvent.click(btn);
expect(onClick).toBeCalled();
}
);
});
+48 -16
View File
@@ -15,6 +15,8 @@ import { Tooltip } from '../../components/tooltip';
import { WithdrawContainer } from '../withdraw-container'; import { WithdrawContainer } from '../withdraw-container';
import { Routes as AppRoutes } from '../../pages/client-router'; import { Routes as AppRoutes } from '../../pages/client-router';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import { GetStarted } from '../welcome-dialog';
import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
const STORAGE_KEY = 'vega_sidebar_store'; const STORAGE_KEY = 'vega_sidebar_store';
@@ -25,6 +27,7 @@ export enum ViewType {
Withdraw = 'Withdraw', Withdraw = 'Withdraw',
Transfer = 'Transfer', Transfer = 'Transfer',
Settings = 'Settings', Settings = 'Settings',
ViewAs = 'ViewAs',
} }
type SidebarView = type SidebarView =
@@ -52,6 +55,8 @@ type SidebarView =
export const Sidebar = () => { export const Sidebar = () => {
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1'; const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
const { pubKeys } = useVegaWallet();
return ( return (
<div className="flex lg:flex-col gap-2 h-full p-1" data-testid="sidebar"> <div className="flex lg:flex-col gap-2 h-full p-1" data-testid="sidebar">
<nav className={navClasses}> <nav className={navClasses}>
@@ -104,6 +109,16 @@ export const Sidebar = () => {
</Routes> </Routes>
</nav> </nav>
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}> <nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}>
<SidebarButton
view={ViewType.ViewAs}
onClick={() => {
setViewAsDialogOpen(true);
}}
icon={VegaIconNames.EYE}
tooltip={t('View as party')}
disabled={Boolean(pubKeys)}
/>
<SidebarButton <SidebarButton
view={ViewType.Settings} view={ViewType.Settings}
icon={VegaIconNames.COG} icon={VegaIconNames.COG}
@@ -115,25 +130,43 @@ export const Sidebar = () => {
); );
}; };
const SidebarButton = ({ export const SidebarButton = ({
view, view,
icon, icon,
tooltip, tooltip,
disabled = false,
onClick,
}: { }: {
view: ViewType; view?: ViewType;
icon: VegaIconNames; icon: VegaIconNames;
tooltip: string; tooltip: string;
disabled?: boolean;
onClick?: () => void;
}) => { }) => {
const { currView, setView } = useSidebar((store) => ({ const { currView, setView } = useSidebar((store) => ({
currView: store.view, currView: store.view,
setView: store.setView, setView: store.setView,
})); }));
const buttonClasses = classNames('flex items-center p-1 rounded', {
'text-vega-clight-200 dark:text-vega-cdark-200 hover:bg-vega-clight-500 dark:hover:bg-vega-cdark-500': const onSelect = (view: SidebarView['type']) => {
view !== currView?.type, if (view === currView?.type) {
'bg-vega-yellow hover:bg-vega-yellow-550 text-black': setView(null);
view === currView?.type, } else {
}); setView({ type: view });
}
};
const buttonClasses = classNames(
'flex items-center p-1 rounded',
'disabled:cursor-not-allowed disabled:text-vega-clight-500 dark:disabled:text-vega-cdark-500',
{
'text-vega-clight-200 dark:text-vega-cdark-200 enabled:hover:bg-vega-clight-500 dark:enabled:hover:bg-vega-cdark-500':
!view || view !== currView?.type,
'bg-vega-yellow enabled:hover:bg-vega-yellow-550 text-black':
view && view === currView?.type,
}
);
return ( return (
<Tooltip <Tooltip
description={tooltip} description={tooltip}
@@ -145,13 +178,8 @@ const SidebarButton = ({
<button <button
className={buttonClasses} className={buttonClasses}
data-testid={view} data-testid={view}
onClick={() => { onClick={onClick || (() => onSelect(view as SidebarView['type']))}
if (view === currView?.type) { disabled={disabled}
setView(null);
} else {
setView({ type: view });
}
}}
> >
<VegaIcon name={icon} size={20} /> <VegaIcon name={icon} size={20} />
</button> </button>
@@ -184,6 +212,7 @@ export const SidebarContent = () => {
setView({ type: ViewType.Deposit, assetId }) setView({ type: ViewType.Deposit, assetId })
} }
/> />
<GetStarted />
</ContentWrapper> </ContentWrapper>
); );
} else { } else {
@@ -207,6 +236,7 @@ export const SidebarContent = () => {
return ( return (
<ContentWrapper title={t('Deposit')}> <ContentWrapper title={t('Deposit')}>
<DepositContainer assetId={view.assetId} /> <DepositContainer assetId={view.assetId} />
<GetStarted />
</ContentWrapper> </ContentWrapper>
); );
} }
@@ -215,6 +245,7 @@ export const SidebarContent = () => {
return ( return (
<ContentWrapper title={t('Withdraw')}> <ContentWrapper title={t('Withdraw')}>
<WithdrawContainer assetId={view.assetId} /> <WithdrawContainer assetId={view.assetId} />
<GetStarted />
</ContentWrapper> </ContentWrapper>
); );
} }
@@ -223,6 +254,7 @@ export const SidebarContent = () => {
return ( return (
<ContentWrapper title={t('Transfer')}> <ContentWrapper title={t('Transfer')}>
<TransferContainer assetId={view.assetId} /> <TransferContainer assetId={view.assetId} />
<GetStarted />
</ContentWrapper> </ContentWrapper>
); );
} }
@@ -277,7 +309,7 @@ export const useSidebar = create<{
view: null, view: null,
setView: (x) => setView: (x) =>
set(() => { set(() => {
if (x === null) { if (x == null) {
return { view: null, init: false }; return { view: null, init: false };
} }
@@ -0,0 +1 @@
export * from './stop-orders-container';
@@ -0,0 +1,41 @@
import { useDataGridEvents } from '@vegaprotocol/datagrid';
import { t } from '@vegaprotocol/i18n';
import { StopOrdersManager } from '@vegaprotocol/orders';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { DataGridSlice } from '../../stores/datagrid-store-slice';
import { createDataGridSlice } from '../../stores/datagrid-store-slice';
export const StopOrdersContainer = () => {
const { pubKey, isReadOnly } = useVegaWallet();
const onMarketClick = useMarketClickHandler(true);
const gridStore = useStopOrdersStore((store) => store.gridStore);
const updateGridStore = useStopOrdersStore((store) => store.updateGridStore);
const gridStoreCallbacks = useDataGridEvents(gridStore, (colState) => {
updateGridStore(colState);
});
if (!pubKey) {
return <Splash>{t('Please connect Vega wallet')}</Splash>;
}
return (
<StopOrdersManager
partyId={pubKey}
onMarketClick={onMarketClick}
isReadOnly={isReadOnly}
gridProps={gridStoreCallbacks}
/>
);
};
const useStopOrdersStore = create<DataGridSlice>()(
persist(createDataGridSlice, {
name: 'vega_fills_store',
})
);
@@ -27,6 +27,16 @@ describe('VegaWalletConnectButton', () => {
it('should fire dialog when not connected', () => { it('should fire dialog when not connected', () => {
render(generateJsx({ pubKey: null } as VegaWalletContextShape)); render(generateJsx({ pubKey: null } as VegaWalletContextShape));
const button = screen.getByTestId('connect-vega-wallet');
expect(button).toHaveTextContent('Get started');
fireEvent.click(button);
expect(mockUpdateDialogOpen).toHaveBeenCalled();
});
it('should render "Connect" when browser wallet is detected', () => {
window.vega = window.vega || ({} as Vega);
render(generateJsx({ pubKey: null } as VegaWalletContextShape));
const button = screen.getByTestId('connect-vega-wallet'); const button = screen.getByTestId('connect-vega-wallet');
expect(button).toHaveTextContent('Connect'); expect(button).toHaveTextContent('Connect');
fireEvent.click(button); fireEvent.click(button);
@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard'; import CopyToClipboard from 'react-copy-to-clipboard';
import { isBrowserWalletInstalled } from '@vegaprotocol/wallet';
import { truncateByChars } from '@vegaprotocol/utils'; import { truncateByChars } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { import {
@@ -37,7 +38,7 @@ export const VegaWalletConnectButton = () => {
fetchPubKeys, fetchPubKeys,
} = useVegaWallet(); } = useVegaWallet();
const isConnected = pubKey !== null; const isConnected = pubKey !== null;
const walletInstalled = isBrowserWalletInstalled();
const activeKey = useMemo(() => { const activeKey = useMemo(() => {
return pubKeys?.find((pk) => pk.publicKey === pubKey); return pubKeys?.find((pk) => pk.publicKey === pubKey);
}, [pubKey, pubKeys]); }, [pubKey, pubKeys]);
@@ -118,7 +119,9 @@ export const VegaWalletConnectButton = () => {
intent={Intent.None} intent={Intent.None}
icon={<VegaIcon name={VegaIconNames.ARROW_RIGHT} size={14} />} icon={<VegaIcon name={VegaIconNames.ARROW_RIGHT} size={14} />}
> >
<span className="whitespace-nowrap uppercase">{t('Connect')}</span> <span className="whitespace-nowrap uppercase">
{walletInstalled ? t('Connect') : t('Get started')}
</span>
</Button> </Button>
); );
}; };
@@ -0,0 +1,108 @@
import classNames from 'classnames';
import { t } from '@vegaprotocol/i18n';
import { ExternalLink, Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
import {
useVegaWallet,
useVegaWalletDialogStore,
isBrowserWalletInstalled,
} from '@vegaprotocol/wallet';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import * as constants from '../constants';
interface Props {
lead?: string;
}
export const GetStarted = ({ lead }: Props) => {
const { pubKey } = useVegaWallet();
const { VEGA_ENV, VEGA_NETWORKS } = useEnvironment();
const CANONICAL_URL = VEGA_NETWORKS[VEGA_ENV] || 'https://console.vega.xyz';
const [, setOnboardingViewed] = useLocalStorage(
constants.ONBOARDING_VIEWED_KEY
);
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const onButtonClick = () => {
openVegaWalletDialog();
setOnboardingViewed('true');
};
const wrapperClasses = classNames(
'flex flex-col py-4 px-6 gap-4 rounded',
'bg-vega-blue-300 dark:bg-vega-blue-700',
'border border-vega-blue-350 dark:border-vega-blue-650',
{ 'mt-8': !lead }
);
if (!isBrowserWalletInstalled()) {
return (
<div className={wrapperClasses} data-testid="get-started-banner">
{lead && <h2>{lead}</h2>}
<h3 className="text-lg">{t('Get started')}</h3>
<div>
<ul className="list-decimal list-inside">
<li>{t('Get a Vega wallet')}</li>
<li>{t('Connect')}</li>
<li>{t('Deposit funds')}</li>
<li>{t('Open a position')}</li>
</ul>
</div>
<div>
<TradingButton
intent={Intent.Info}
onClick={onButtonClick}
data-testid="get-started-button"
>
{t('Get started')}
</TradingButton>
</div>
{VEGA_ENV === Networks.MAINNET && (
<p className="text-sm">
{t('Experiment for free with virtual assets on')}{' '}
<ExternalLink href={CANONICAL_URL}>
{t('Fairground Testnet')}
</ExternalLink>
</p>
)}
{VEGA_ENV === Networks.TESTNET && (
<p className="text-sm">
{t('Ready to trade with real funds?')}{' '}
<ExternalLink href={CANONICAL_URL}>
{t('Switch to Mainnet')}
</ExternalLink>
</p>
)}
</div>
);
}
if (!pubKey) {
return (
<div className={wrapperClasses}>
<p className="text-sm mb-1">
You need a{' '}
<ExternalLink href="https://vega.xyz/wallet">
Vega wallet
</ExternalLink>{' '}
to start trading in this market.
</p>
<TradingButton
onClick={openVegaWalletDialog}
size="small"
data-testid="order-connect-wallet"
intent={Intent.Info}
>
{t('Connect wallet')}
</TradingButton>
</div>
);
}
return null;
};
@@ -1,2 +1,3 @@
export * from './welcome-dialog'; export * from './welcome-dialog';
export * from './risk-message'; export * from './risk-message';
export * from './get-started';
@@ -1,50 +0,0 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { RiskNoticeDialog } from './risk-notice-dialog';
jest.mock('@vegaprotocol/environment');
const mockEnvDefinitions = {
VEGA_CONFIG_URL: 'https://config.url',
VEGA_URL: 'https://test.url',
VEGA_NETWORKS: JSON.stringify({}),
};
describe('Risk notice dialog', () => {
const mockOnClose = jest.fn();
afterEach(() => {
jest.clearAllMocks();
});
it.each`
assertion | network
${'displays'} | ${Networks.CUSTOM}
${'displays'} | ${Networks.DEVNET}
${'displays'} | ${Networks.TESTNET}
`(
'$assertion a generic message on $network',
async ({ assertion, network }) => {
// @ts-ignore ignore mock implementation
useEnvironment.mockImplementation(() => ({
...mockEnvDefinitions,
VEGA_ENV: network,
}));
render(<RiskNoticeDialog onClose={mockOnClose} network={network} />);
expect(
screen.getByText(
new RegExp(
`This application for trading on Vega is connected to ${network}`
)
)
).toBeInTheDocument();
const button = screen.getByRole('button', {
name: 'Continue',
});
fireEvent.click(button);
expect(mockOnClose).toHaveBeenCalled();
}
);
});
@@ -1,89 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import {
Button,
Link,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { RISK_ACCEPTED_KEY } from '../constants';
import { TelemetryApproval } from './telemetry-approval';
import type { Networks } from '@vegaprotocol/environment';
import {
useEnvironment,
DocsLinks,
ExternalLinks,
} from '@vegaprotocol/environment';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
interface Props {
onClose: () => void;
network: Networks;
}
export const RiskNoticeDialog = ({ onClose, network }: Props) => {
const [, setValue] = useLocalStorage(RISK_ACCEPTED_KEY);
const handleAcceptRisk = () => {
onClose();
setValue('true');
};
return (
<TestnetContent network={network} handleAcceptRisk={handleAcceptRisk} />
);
};
const TestnetContent = ({
network,
handleAcceptRisk,
}: {
network: Networks;
handleAcceptRisk: () => void;
}) => {
const { GITHUB_FEEDBACK_URL } = useEnvironment();
return (
<>
<p className="mb-4">
{t(
'This application for trading on Vega is connected to %s, meaning you are free to try out trading with virtual assets and no risk.',
[network]
)}
</p>
<p className="mb-4">
{t(
'Your Vega wallet must also be connected to %s, and your Ethereum wallet must be connected to Sepolia.',
[network]
)}
</p>
{GITHUB_FEEDBACK_URL && DocsLinks && (
<ul className="list-disc pl-4">
<li className="mb-1">
<Link href={ExternalLinks.VEGA_WALLET_URL} target="_blank">
<span className="underline">{t('Get a Vega Wallet')}</span>{' '}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</Link>
</li>
<li className="mb-1">
<Link href={DocsLinks.VEGA_WALLET_TOOLS_URL} target="_blank">
<span className="underline">{t('Learn about Vega Wallet')}</span>{' '}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</Link>
</li>
<li className="mb-1">
<Link href={GITHUB_FEEDBACK_URL} target="_blank">
<span className="underline">{t('Provide feedback')}</span>{' '}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</Link>
</li>
</ul>
)}
<div className="my-4">
<TelemetryApproval
helpText={t(
'Help identify bugs and improve the service by sharing anonymous usage data. You can change this in your settings at any time.'
)}
/>
</div>
<Button onClick={handleAcceptRisk}>{t('Continue')}</Button>
</>
);
};
@@ -0,0 +1,159 @@
import { t } from '@vegaprotocol/i18n';
import { GetStarted } from './get-started';
import { TradingButton } from '@vegaprotocol/ui-toolkit';
import { useNavigate } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import * as constants from '../constants';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import type { ReactNode } from 'react';
export const WelcomeDialogContent = () => {
const { VEGA_ENV } = useEnvironment();
const [, setOnboardingViewed] = useLocalStorage(
constants.ONBOARDING_VIEWED_KEY
);
const navigate = useNavigate();
const browseMarkets = () => {
const link = Links[Routes.MARKETS]();
navigate(link);
setOnboardingViewed('true');
};
const lead =
VEGA_ENV === Networks.MAINNET
? t('Start trading on the worlds most advanced decentralised exchange.')
: t(
'Free from the risks of real trading, Fairground is a safe and fun place to try out Vega yourself with virtual assets.'
);
return (
<div className="flex flex-col sm:flex-row gap-8">
<div className="sm:w-1/2 flex flex-col justify-between pt-3">
<ul className="ml-0">
<ListItemContent
icon={<NonCustodialIcon />}
title={t('Non-custodial and pseudonymous')}
text={t('No third party has access to your funds.')}
/>
<ListItemContent
icon={<PurposeBuiltIcon />}
title={t('Purpose built proof of stake blockchain')}
text={t(
'Fully decentralised high performance peer-to-network trading.'
)}
/>
<ListItemContent
icon={<RewardIcon />}
title={t('Low fees and no cost to place orders')}
text={t(
'Fees work like a CEX with no per-transaction gas for orders'
)}
/>
</ul>
<TradingButton
onClick={browseMarkets}
className="block w-full"
data-testid="browse-markets-button"
>
{t('Browse the markets')}
</TradingButton>
</div>
<div className="sm:w-1/2">
<GetStarted lead={lead} />
</div>
</div>
);
};
const ListItemContent = ({
icon,
title,
text,
}: {
icon: ReactNode;
title: string;
text: string;
}) => {
return (
<li className="my-4 flex gap-3">
<div className="shrink-0 pt-1">{icon}</div>
<div>
<h3 className="text-lg leading-snug mb-2">{title}</h3>
<p className="text-sm text-secondary">{text}</p>
</div>
</li>
);
};
const PurposeBuiltIcon = () => {
return (
<svg width="40" height="40" viewBox="0 0 30 30" fill="currentColor">
<rect x="14" y="20" width="2" height="2" />
<rect x="12" y="18" width="2" height="2" />
<rect x="10" y="8" width="2" height="10" />
<rect x="16" y="18" width="2" height="2" />
<rect x="20" y="16" width="2" height="2" />
<rect x="18" y="8" width="2" height="8" />
<rect x="28" y="2" width="2" height="26" />
<rect y="2" width="2" height="26" />
<rect x="28" width="2" height="26" transform="rotate(90 28 0)" />
<rect x="28" y="28" width="2" height="26" transform="rotate(90 28 28)" />
</svg>
);
};
const NonCustodialIcon = () => {
return (
<svg width="40" height="40" viewBox="0 0 30 30" className="fill-current">
<rect x="8" y="22" width="14" height="2" />
<rect x="8" y="12" width="14" height="2" />
<rect
x="22"
y="24"
width="2"
height="12"
transform="rotate(-180 22 24)"
/>
<rect
x="10"
y="24"
width="2"
height="12"
transform="rotate(-180 10 24)"
/>
<rect x="12" y="12" width="2" height="2" transform="rotate(-180 12 12)" />
<rect x="20" y="12" width="2" height="2" transform="rotate(-180 20 12)" />
<rect x="12" y="10" width="2" height="2" transform="rotate(-180 12 10)" />
<rect x="20" y="10" width="2" height="2" transform="rotate(-180 20 10)" />
<rect x="18" y="8" width="2" height="2" transform="rotate(-180 18 8)" />
<rect x="14" y="8" width="2" height="2" transform="rotate(-180 14 8)" />
<rect x="16" y="8" width="2" height="2" transform="rotate(-180 16 8)" />
<rect x="28" y="2" width="2" height="26" />
<rect y="2" width="2" height="26" />
<rect x="28" width="2" height="26" transform="rotate(90 28 0)" />
<rect x="28" y="28" width="2" height="26" transform="rotate(90 28 28)" />
</svg>
);
};
const RewardIcon = () => {
return (
<svg width="40" height="40" viewBox="0 0 30 30" fill="currentColor">
<rect x="6" y="16" width="4" height="2" />
<rect x="10" y="14" width="2" height="2" />
<rect x="12" y="24" width="4" height="2" transform="rotate(-90 12 24)" />
<rect x="10" y="20" width="2" height="2" transform="rotate(-90 10 20)" />
<rect x="20" y="18" width="4" height="2" transform="rotate(-180 20 18)" />
<rect x="16" y="20" width="2" height="2" transform="rotate(-180 16 20)" />
<rect x="16" y="14" width="2" height="2" transform="rotate(90 16 14)" />
<rect x="22" y="10" width="2" height="2" transform="rotate(90 22 10)" />
<rect x="20" y="8" width="2" height="2" transform="rotate(90 20 8)" />
<rect x="24" y="8" width="2" height="2" transform="rotate(90 24 8)" />
<rect x="22" y="6" width="2" height="2" transform="rotate(90 22 6)" />
<rect x="12" y="10" width="2" height="4" />
<rect x="28" y="2" width="2" height="26" />
<rect y="2" width="2" height="26" />
<rect x="28" width="2" height="26" transform="rotate(90 28 0)" />
<rect x="28" y="28" width="2" height="26" transform="rotate(90 28 28)" />
</svg>
);
};
@@ -1,68 +1,52 @@
import React, { useCallback } from 'react'; import React from 'react';
import { useLocation } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Dialog } from '@vegaprotocol/ui-toolkit'; import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers'; import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { activeMarketsProvider } from '@vegaprotocol/markets';
import * as constants from '../constants';
import { RiskNoticeDialog } from './risk-notice-dialog';
import { WelcomeNoticeDialog } from './welcome-notice-dialog';
import { useGlobalStore } from '../../stores';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { Networks } from '@vegaprotocol/environment'; import { isBrowserWalletInstalled } from '@vegaprotocol/wallet';
import { isTestEnv } from '@vegaprotocol/utils'; import * as constants from '../constants';
import { WelcomeDialogContent } from './welcome-dialog-content';
import { getConfig } from '@vegaprotocol/wallet';
import { Links, Routes } from '../../pages/client-router';
import { useGlobalStore } from '../../stores';
export const WelcomeDialog = () => { export const WelcomeDialog = () => {
const { VEGA_ENV } = useEnvironment(); const { VEGA_ENV } = useEnvironment();
const { pathname } = useLocation(); const [onBoardingViewed, setOnboardingViewed] = useLocalStorage(
let dialogContent: React.ReactNode; constants.ONBOARDING_VIEWED_KEY
let title = ''; );
let size: 'small' | 'medium' = 'small'; const navigate = useNavigate();
let onClose: ((open: boolean) => void) | undefined = undefined; const isOnboardingDialogNeeded =
const [riskAccepted] = useLocalStorage(constants.RISK_ACCEPTED_KEY); onBoardingViewed !== 'true' && !isBrowserWalletInstalled() && !getConfig();
const { data } = useDataProvider({ const marketId = useGlobalStore((store) => store.marketId);
dataProvider: activeMarketsProvider,
variables: undefined,
});
const update = useGlobalStore((store) => store.update); const onClose = () => {
const shouldDisplayWelcomeDialog = useGlobalStore( setOnboardingViewed('true');
(store) => store.shouldDisplayWelcomeDialog const link = marketId
? Links[Routes.MARKET](marketId)
: Links[Routes.HOME]();
navigate(link);
};
const title = (
<span className="font-alpha calt" data-testid="welcome-title">
{t('Console')}{' '}
<span className="text-vega-clight-100 dark:text-vega-cdark-100">
{VEGA_ENV}
</span>
</span>
); );
const isRiskDialogNeeded = return isOnboardingDialogNeeded ? (
riskAccepted !== 'true' && VEGA_ENV !== Networks.MAINNET && !isTestEnv();
const isWelcomeDialogNeeded = pathname === '/' || shouldDisplayWelcomeDialog;
const onCloseDialog = useCallback(() => {
update({
shouldDisplayWelcomeDialog: isRiskDialogNeeded,
});
}, [update, isRiskDialogNeeded]);
if (isRiskDialogNeeded) {
dialogContent = (
<RiskNoticeDialog onClose={onCloseDialog} network={VEGA_ENV} />
);
title = t('Vega Console');
size = 'medium';
} else if (isWelcomeDialogNeeded && data?.length === 0) {
dialogContent = <WelcomeNoticeDialog />;
onClose = onCloseDialog;
} else {
dialogContent = null as React.ReactNode;
}
return (
<Dialog <Dialog
open={Boolean(dialogContent)} open
title={title} title={title}
size={size} size="medium"
onChange={onClose} onChange={onClose}
intent={Intent.None}
dataTestId="welcome-dialog"
> >
{dialogContent} <WelcomeDialogContent />
</Dialog> </Dialog>
); ) : null;
}; };
@@ -1,71 +0,0 @@
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import {
DApp,
Networks,
TOKEN_NEW_MARKET_PROPOSAL,
TOKEN_PROPOSALS,
useEnvironment,
useLinks,
ExternalLinks,
} from '@vegaprotocol/environment';
import { ProposedMarkets } from './proposed-markets';
import { TelemetryApproval } from './telemetry-approval';
export const WelcomeNoticeDialog = () => {
const { VEGA_ENV } = useEnvironment();
const tokenLink = useLinks(DApp.Token);
const consoleFairgroundLink = useLinks(DApp.Console, Networks.TESTNET);
const isMainnet = VEGA_ENV === Networks.MAINNET;
return (
<>
<h1
data-testid="welcome-notice-title"
className="text-2xl uppercase mb-6 text-center font-alpha calt"
>
{t('Welcome to Console')}
</h1>
<p className="leading-6 mb-4">
{t(
'There are no markets to trade on right now. Trading on Vega is now live, but markets need to pass a governance vote before they can be traded on. In the meantime:'
)}
</p>
<ul className="list-[square] pl-4 mb-4">
{isMainnet && (
<li className="mb-1">
<ExternalLink target="_blank" href={consoleFairgroundLink()}>
{t('Try out Console')}
</ExternalLink>
{t(' on Fairground, our Testnet')}
</li>
)}
<li className="mb-1">
<ExternalLink target="_blank" href={tokenLink(TOKEN_PROPOSALS)}>
{t('View and vote for proposed markets')}
</ExternalLink>
</li>
<li className="mb-1">
<ExternalLink
target="_blank"
href={tokenLink(TOKEN_NEW_MARKET_PROPOSAL)}
>
{t('Propose a market')}
</ExternalLink>
</li>
<li className="mb-1">
<ExternalLink target="_blank" href={ExternalLinks.BLOG}>
{t('Read about the mainnet launch')}
</ExternalLink>
</li>
</ul>
{isMainnet && (
<TelemetryApproval
helpText={t(
'Help identify bugs and improve the service by sharing anonymous usage data. You can change this in your settings at any time.'
)}
/>
)}
<ProposedMarkets />
</>
);
};
+7
View File
@@ -31,6 +31,10 @@ const LazyDisclaimer = dynamic(() => import('../client-pages/disclaimer'), {
ssr: false, ssr: false,
}); });
const LazyDeposit = dynamic(() => import('../client-pages/deposit'), {
ssr: false,
});
export enum Routes { export enum Routes {
HOME = '/', HOME = '/',
MARKET = '/markets/:marketId', MARKET = '/markets/:marketId',
@@ -38,6 +42,7 @@ export enum Routes {
PORTFOLIO = '/portfolio', PORTFOLIO = '/portfolio',
LIQUIDITY = '/liquidity/:marketId', LIQUIDITY = '/liquidity/:marketId',
DISCLAIMER = '/disclaimer', DISCLAIMER = '/disclaimer',
DEPOSIT = '/deposit',
} }
type ConsoleLinks = { [r in Routes]: (...args: string[]) => string }; type ConsoleLinks = { [r in Routes]: (...args: string[]) => string };
@@ -51,6 +56,7 @@ export const Links: ConsoleLinks = {
[Routes.LIQUIDITY]: (marketId: string) => [Routes.LIQUIDITY]: (marketId: string) =>
trimEnd(Routes.LIQUIDITY.replace(':marketId', marketId)), trimEnd(Routes.LIQUIDITY.replace(':marketId', marketId)),
[Routes.DISCLAIMER]: () => Routes.DISCLAIMER, [Routes.DISCLAIMER]: () => Routes.DISCLAIMER,
[Routes.DEPOSIT]: () => Routes.DEPOSIT,
}; };
const routerConfig: RouteObject[] = [ const routerConfig: RouteObject[] = [
@@ -97,6 +103,7 @@ const routerConfig: RouteObject[] = [
path: Routes.DISCLAIMER, path: Routes.DISCLAIMER,
element: <LazyDisclaimer />, element: <LazyDisclaimer />,
}, },
{ path: Routes.DEPOSIT, element: <LazyDeposit /> },
{ {
path: '*', path: '*',
element: ( element: (
+2 -1
View File
@@ -2,7 +2,7 @@ import {
AssetDetailsDialog, AssetDetailsDialog,
useAssetDetailsDialogStore, useAssetDetailsDialogStore,
} from '@vegaprotocol/assets'; } from '@vegaprotocol/assets';
import { VegaConnectDialog } from '@vegaprotocol/wallet'; import { VegaConnectDialog, ViewAsDialog } from '@vegaprotocol/wallet';
import { Connectors } from '../lib/vega-connectors'; import { Connectors } from '../lib/vega-connectors';
import { import {
Web3ConnectUncontrolledDialog, Web3ConnectUncontrolledDialog,
@@ -19,6 +19,7 @@ const DialogsContainer = () => {
connectors={Connectors} connectors={Connectors}
riskMessage={<RiskMessage />} riskMessage={<RiskMessage />}
/> />
<ViewAsDialog connector={Connectors.view} />
<AssetDetailsDialog <AssetDetailsDialog
assetId={id} assetId={id}
trigger={trigger || null} trigger={trigger || null}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

-2
View File
@@ -5,7 +5,6 @@ import produce from 'immer';
interface GlobalStore { interface GlobalStore {
marketId: string | null; marketId: string | null;
update: (store: Partial<Omit<GlobalStore, 'update'>>) => void; update: (store: Partial<Omit<GlobalStore, 'update'>>) => void;
shouldDisplayWelcomeDialog: boolean;
} }
interface PageTitleStore { interface PageTitleStore {
@@ -15,7 +14,6 @@ interface PageTitleStore {
export const useGlobalStore = create<GlobalStore>()((set) => ({ export const useGlobalStore = create<GlobalStore>()((set) => ({
marketId: LocalStorage.getItem('marketId') || null, marketId: LocalStorage.getItem('marketId') || null,
shouldDisplayWelcomeDialog: false,
update: (newState) => { update: (newState) => {
set( set(
produce((state: GlobalStore) => { produce((state: GlobalStore) => {
+1 -3
View File
@@ -75,7 +75,7 @@ export const AccountBreakdownDialog = memo(
}) => { }) => {
return ( return (
<Dialog <Dialog
size="medium" size="large"
open={Boolean(assetId)} open={Boolean(assetId)}
onChange={(isOpen) => { onChange={(isOpen) => {
if (!isOpen) { if (!isOpen) {
@@ -118,7 +118,6 @@ export const AccountManager = ({
onMarketClick, onMarketClick,
gridProps, gridProps,
}: AccountManagerProps) => { }: AccountManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [breakdownAssetId, setBreakdownAssetId] = useState<string>(); const [breakdownAssetId, setBreakdownAssetId] = useState<string>();
const { data, error } = useDataProvider({ const { data, error } = useDataProvider({
dataProvider: aggregatedAccountsDataProvider, dataProvider: aggregatedAccountsDataProvider,
@@ -138,7 +137,6 @@ export const AccountManager = ({
return ( return (
<div className="relative h-full"> <div className="relative h-full">
<AccountTable <AccountTable
ref={gridRef}
rowData={data} rowData={data}
onClickAsset={onClickAsset} onClickAsset={onClickAsset}
onClickDeposit={onClickDeposit} onClickDeposit={onClickDeposit}
+237 -243
View File
@@ -1,4 +1,4 @@
import { forwardRef, useMemo, useCallback } from 'react'; import { useMemo, useCallback } from 'react';
import { import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum, addDecimalsFormatNumberQuantum,
@@ -21,7 +21,7 @@ import type {
RowHeightParams, RowHeightParams,
ColDef, ColDef,
} from 'ag-grid-community'; } from 'ag-grid-community';
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react'; import type { AgGridReactProps } from 'ag-grid-react';
import type { AccountFields } from './accounts-data-provider'; import type { AccountFields } from './accounts-data-provider';
import type { Asset } from '@vegaprotocol/types'; import type { Asset } from '@vegaprotocol/types';
import { CenteredGridCellWrapper } from '@vegaprotocol/datagrid'; import { CenteredGridCellWrapper } from '@vegaprotocol/datagrid';
@@ -75,256 +75,250 @@ export interface AccountTableProps extends AgGridReactProps {
pinnedAsset?: PinnedAsset; pinnedAsset?: PinnedAsset;
} }
export const AccountTable = forwardRef<AgGridReact, AccountTableProps>( export const AccountTable = ({
( onClickAsset,
{ onClickWithdraw,
onClickAsset, onClickDeposit,
onClickWithdraw, onClickBreakdown,
onClickDeposit, onClickTransfer,
onClickBreakdown, rowData,
onClickTransfer, isReadOnly,
rowData, pinnedAsset,
isReadOnly, ...props
pinnedAsset, }: AccountTableProps) => {
...props const pinnedRow = useMemo(() => {
if (!pinnedAsset) {
return;
}
const currentPinnedAssetRow = rowData?.find(
(row) => row.asset.id === pinnedAsset?.id
);
if (!currentPinnedAssetRow) {
return {
asset: pinnedAsset,
available: '0',
used: '0',
total: '0',
balance: '0',
};
}
return currentPinnedAssetRow;
}, [pinnedAsset, rowData]);
const { getRowHeight } = props;
const getPinnedAssetRowHeight = useCallback(
(params: RowHeightParams) => {
if (
params.node.rowPinned &&
params.data.asset.id === pinnedAsset?.id &&
new BigNumber(params.data.total).isLessThanOrEqualTo(0)
) {
return 32;
}
return getRowHeight ? getRowHeight(params) : undefined;
}, },
ref [pinnedAsset?.id, getRowHeight]
) => { );
const pinnedRow = useMemo(() => {
if (!pinnedAsset) {
return;
}
const currentPinnedAssetRow = rowData?.find(
(row) => row.asset.id === pinnedAsset?.id
);
if (!currentPinnedAssetRow) {
return {
asset: pinnedAsset,
available: '0',
used: '0',
total: '0',
balance: '0',
};
}
return currentPinnedAssetRow;
}, [pinnedAsset, rowData]);
const { getRowHeight } = props; const showDepositButton = pinnedRow?.balance === '0';
const getPinnedAssetRowHeight = useCallback( const colDefs = useMemo(() => {
(params: RowHeightParams) => { const defs: ColDef[] = [
if ( {
params.node.rowPinned && headerName: t('Asset'),
params.data.asset.id === pinnedAsset?.id && field: 'asset.symbol',
new BigNumber(params.data.total).isLessThanOrEqualTo(0) headerTooltip: t(
) { 'Asset is the collateral that is deposited into the Vega protocol.'
return 32; ),
} cellClass: 'underline',
return getRowHeight ? getRowHeight(params) : undefined; onCellClicked: ({ data }) => {
if (data) {
onClickAsset(data.asset.id);
}
},
}, },
[pinnedAsset?.id, getRowHeight] {
); headerName: t('Used'),
type: 'rightAligned',
const showDepositButton = pinnedRow?.balance === '0'; field: 'used',
headerTooltip: t(
const colDefs = useMemo(() => { 'Currently allocated to a market as margin or bond. Check the breakdown for details.'
const defs: ColDef[] = [ ),
{ tooltipValueGetter: ({ value, data }) => {
headerName: t('Asset'), if (!value || !data) return null;
field: 'asset.symbol', return addDecimalsFormatNumber(value, data.asset.decimals);
headerTooltip: t(
'Asset is the collateral that is deposited into the Vega protocol.'
),
cellClass: 'underline',
onCellClicked: ({ data }) => {
if (data) {
onClickAsset(data.asset.id);
}
},
}, },
{ onCellClicked: ({ data }) => {
headerName: t('Used'), if (!data || !onClickBreakdown) return;
type: 'rightAligned', onClickBreakdown(data.asset.id);
field: 'used', },
headerTooltip: t( cellRenderer: ({
'Currently allocated to a market as margin or bond. Check the breakdown for details.' data,
), value,
tooltipValueGetter: ({ value, data }) => { }: VegaICellRendererParams<AccountFields, 'used'>) => {
if (!value || !data) return null; if (!value || !data) return '-';
return addDecimalsFormatNumber(value, data.asset.decimals); const percentageUsed = percentageValue(value, data.total);
}, const valueFormatted = addDecimalsFormatNumberQuantum(
onCellClicked: ({ data }) => {
if (!data || !onClickBreakdown) return;
onClickBreakdown(data.asset.id);
},
cellRenderer: ({
data,
value, value,
}: VegaICellRendererParams<AccountFields, 'used'>) => { data.asset.decimals,
if (!value || !data) return '-'; data.asset.quantum
const percentageUsed = percentageValue(value, data.total); );
const valueFormatted = addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
return data.breakdown ? ( return data.breakdown ? (
<> <>
<span className="underline">{valueFormatted}</span> <span className="underline">{valueFormatted}</span>
<span <span
className={classNames( className={classNames(
colorClass(percentageUsed), colorClass(percentageUsed),
'ml-1 inline-block w-14' 'ml-1 inline-block w-14'
)} )}
>
{percentageUsed.toFixed(2)}%
</span>
</>
) : (
<>
<span className="underline">{valueFormatted}</span>
<span className="ml-2 inline-block w-14 text-muted">
{t('0.00%')}
</span>
</>
);
},
},
{
headerName: t('Available'),
field: 'available',
type: 'rightAligned',
headerTooltip: t(
'Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
cellClass: ({ data }) => {
const percentageUsed = percentageValue(data?.used, data?.total);
return colorClass(percentageUsed, true);
},
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<AccountFields, 'available'>) => {
if (!value || !data) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
},
{
headerName: t('Total'),
type: 'rightAligned',
field: 'total',
headerTooltip: t(
'The total amount of each asset on this key. Includes used and available collateral.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<AccountFields, 'total'>) => {
if (!data || !value) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
},
{
colId: 'accounts-actions',
field: 'asset.id',
...COL_DEFS.actions,
minWidth: showDepositButton ? 130 : COL_DEFS.actions.minWidth,
maxWidth: showDepositButton ? 130 : COL_DEFS.actions.maxWidth,
cellRenderer: ({
value: assetId,
node,
}: VegaICellRendererParams<AccountFields, 'asset.id'>) => {
if (!assetId) return null;
if (node.rowPinned && node.data?.total === '0') {
return (
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
<Button
size="xs"
variant="primary"
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(assetId);
}}
> >
{percentageUsed.toFixed(2)}% <VegaIcon name={VegaIconNames.DEPOSIT} /> {t('Deposit')}
</span> </Button>
</> </CenteredGridCellWrapper>
) : (
<>
<span className="underline">{valueFormatted}</span>
<span className="ml-2 inline-block w-14 text-muted">
{t('0.00%')}
</span>
</>
); );
}, }
}, return isReadOnly ? null : (
{ <AccountsActionsDropdown
headerName: t('Available'), assetId={assetId}
field: 'available', assetContractAddress={
type: 'rightAligned', node.data?.asset.source?.__typename === 'ERC20'
headerTooltip: t( ? node.data.asset.source.contractAddress
'Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.' : undefined
), }
tooltipValueGetter: ({ value, data }) => { onClickDeposit={() => {
if (!value || !data) return null; onClickDeposit && onClickDeposit(assetId);
return addDecimalsFormatNumber(value, data.asset.decimals); }}
}, onClickWithdraw={() => {
cellClass: ({ data }) => { onClickWithdraw && onClickWithdraw(assetId);
const percentageUsed = percentageValue(data?.used, data?.total); }}
return colorClass(percentageUsed, true); onClickBreakdown={() => {
}, onClickBreakdown && onClickBreakdown(assetId);
valueFormatter: ({ }}
value, onClickTransfer={() => {
data, onClickTransfer && onClickTransfer(assetId);
}: VegaValueFormatterParams<AccountFields, 'available'>) => { }}
if (!value || !data) return '-'; />
return addDecimalsFormatNumberQuantum( );
value,
data.asset.decimals,
data.asset.quantum
);
},
}, },
},
];
return defs;
}, [
onClickAsset,
onClickBreakdown,
onClickDeposit,
onClickWithdraw,
onClickTransfer,
isReadOnly,
showDepositButton,
]);
{ const data = rowData?.filter((data) => data.asset.id !== pinnedAsset?.id);
headerName: t('Total'),
type: 'rightAligned',
field: 'total',
headerTooltip: t(
'The total amount of each asset on this key. Includes used and available collateral.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<AccountFields, 'total'>) => {
if (!data || !value) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
},
{
colId: 'accounts-actions',
field: 'asset.id',
...COL_DEFS.actions,
minWidth: showDepositButton ? 130 : COL_DEFS.actions.minWidth,
maxWidth: showDepositButton ? 130 : COL_DEFS.actions.maxWidth,
cellRenderer: ({
value: assetId,
node,
}: VegaICellRendererParams<AccountFields, 'asset.id'>) => {
if (!assetId) return null;
if (node.rowPinned && node.data?.total === '0') {
return (
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
<Button
size="xs"
variant="primary"
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(assetId);
}}
>
<VegaIcon name={VegaIconNames.DEPOSIT} /> {t('Deposit')}
</Button>
</CenteredGridCellWrapper>
);
}
return isReadOnly ? null : (
<AccountsActionsDropdown
assetId={assetId}
assetContractAddress={
node.data?.asset.source?.__typename === 'ERC20'
? node.data.asset.source.contractAddress
: undefined
}
onClickDeposit={() => {
onClickDeposit && onClickDeposit(assetId);
}}
onClickWithdraw={() => {
onClickWithdraw && onClickWithdraw(assetId);
}}
onClickBreakdown={() => {
onClickBreakdown && onClickBreakdown(assetId);
}}
onClickTransfer={() => {
onClickTransfer && onClickTransfer(assetId);
}}
/>
);
},
},
];
return defs;
}, [
onClickAsset,
onClickBreakdown,
onClickDeposit,
onClickWithdraw,
onClickTransfer,
isReadOnly,
showDepositButton,
]);
const data = rowData?.filter((data) => data.asset.id !== pinnedAsset?.id); return (
<AgGrid
return ( {...props}
<AgGrid style={{ width: '100%', height: '100%' }}
{...props} getRowId={({ data }: { data: AccountFields }) => data.asset.id}
style={{ width: '100%', height: '100%' }} tooltipShowDelay={500}
getRowId={({ data }: { data: AccountFields }) => data.asset.id} rowData={data}
ref={ref} defaultColDef={{
tooltipShowDelay={500} resizable: true,
rowData={data} tooltipComponent: TooltipCellComponent,
defaultColDef={{ sortable: true,
resizable: true, comparator: accountValuesComparator,
tooltipComponent: TooltipCellComponent, }}
sortable: true, columnDefs={colDefs}
comparator: accountValuesComparator, getRowHeight={getPinnedAssetRowHeight}
}} pinnedTopRowData={pinnedRow ? [pinnedRow] : undefined}
columnDefs={colDefs} />
getRowHeight={getPinnedAssetRowHeight} );
pinnedTopRowData={pinnedRow ? [pinnedRow] : undefined} };
/>
);
}
);
@@ -23,7 +23,7 @@ export const useAccountBalance = (assetId?: string) => {
}, },
[assetId] [assetId]
); );
useDataProvider({ const { loading, error } = useDataProvider({
dataProvider: accountsDataProvider, dataProvider: accountsDataProvider,
variables, variables,
skip: !pubKey || !assetId, skip: !pubKey || !assetId,
@@ -34,7 +34,9 @@ export const useAccountBalance = (assetId?: string) => {
() => ({ () => ({
accountBalance: pubKey ? accountBalance : '', accountBalance: pubKey ? accountBalance : '',
accountDecimals: pubKey ? accountDecimals : null, accountDecimals: pubKey ? accountDecimals : null,
loading,
error,
}), }),
[accountBalance, accountDecimals, pubKey] [accountBalance, accountDecimals, pubKey, loading, error]
); );
}; };
@@ -22,7 +22,7 @@ export const useMarketAccountBalance = (marketId: string) => {
}, },
[marketId] [marketId]
); );
useDataProvider({ const { loading, error } = useDataProvider({
dataProvider: accountsDataProvider, dataProvider: accountsDataProvider,
variables: { partyId: pubKey || '' }, variables: { partyId: pubKey || '' },
skip: !pubKey || !marketId, skip: !pubKey || !marketId,
@@ -33,7 +33,9 @@ export const useMarketAccountBalance = (marketId: string) => {
() => ({ () => ({
accountBalance: pubKey ? accountBalance : '', accountBalance: pubKey ? accountBalance : '',
accountDecimals: pubKey ? accountDecimals : null, accountDecimals: pubKey ? accountDecimals : null,
loading,
error,
}), }),
[accountBalance, accountDecimals, pubKey] [accountBalance, accountDecimals, pubKey, loading, error]
); );
}; };
@@ -1,71 +0,0 @@
import { CandlesChartContainer } from './candles-chart';
import { render, screen, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { CandlesEventsDocument } from './__generated__/Candles';
import type { CandlesEventsSubscription } from './__generated__/Candles';
const candles: CandlesEventsSubscription = {
candles: {
lastUpdateInPeriod: 0,
periodStart: 0,
open: '0',
high: '0',
low: '0',
close: '0',
volume: '0',
},
};
const mocks = [
{
request: {
query: CandlesEventsDocument,
variables: { marketId: 'market-id', interval: 'INTERVAL_I15M' },
},
result: { data: candles },
},
];
describe('TradingChart', () => {
it('should render successfully', () => {
const { baseElement } = render(
<MockedProvider>
<VegaWalletContext.Provider value={{} as never}>
<CandlesChartContainer marketId={'market-id'} />
</VegaWalletContext.Provider>
</MockedProvider>
);
expect(baseElement).toBeTruthy();
});
it('volume study should be preselected', async () => {
act(() => {
render(
<MockedProvider mocks={mocks}>
<VegaWalletContext.Provider value={{} as never}>
<CandlesChartContainer marketId={'market-id'} />
</VegaWalletContext.Provider>
</MockedProvider>
);
});
await waitFor(() => {
expect(
screen.getByText('Studies', {
selector: '[type="button"]',
})
).toBeInTheDocument();
});
act(() => {
userEvent.click(
screen.getByText('Studies', {
selector: '[type="button"]',
})
);
});
await waitFor(() => {
expect(screen.getByRole('menu')).toBeInTheDocument();
});
expect(screen.getByText('Volume')).toHaveAttribute('data-state', 'checked');
});
});
+24 -233
View File
@@ -1,92 +1,12 @@
import 'pennant/dist/style.css'; import 'pennant/dist/style.css';
import { import { CandlestickChart } from 'pennant';
CandlestickChart,
ChartType,
Interval,
Overlay,
Study,
chartTypeLabels,
intervalLabels,
overlayLabels,
studyLabels,
} from 'pennant';
import { VegaDataSource } from './data-source'; import { VegaDataSource } from './data-source';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
useThemeSwitcher,
getValidItem,
getValidSubset,
} from '@vegaprotocol/react-helpers';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItemIndicator,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
Icon,
} from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@blueprintjs/icons';
import { IconNames } from '@blueprintjs/icons';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { create } from 'zustand'; import { useCandlesChartSettings } from './use-candles-chart-settings';
import { persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface StoredSettings {
interval?: Interval;
type?: ChartType;
overlays?: Overlay[];
studies?: Study[];
}
export const useCandlesChartSettings = create<
StoredSettings & {
merge: (settings: StoredSettings) => void;
setType: (type: ChartType) => void;
setInterval: (interval: Interval) => void;
setOverlays: (overlays: Overlay[]) => void;
setStudies: (studies: Study[]) => void;
}
>()(
persist(
immer((set) => ({
merge: (settings: StoredSettings) =>
set((state) => {
Object.assign(state, settings);
}),
setType: (type: ChartType) =>
set((state) => {
state.type = type;
}),
setInterval: (interval: Interval) =>
set((state) => {
state.interval = interval;
}),
setOverlays: (overlays: Overlay[]) =>
set((state) => {
state.overlays = overlays;
}),
setStudies: (studies: Study[]) =>
set((state) => {
state.studies = studies;
}),
})),
{
name: 'console-candles',
}
)
);
const chartTypeIcon = new Map<ChartType, IconName>([
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
[ChartType.CANDLE, IconNames.WATERFALL_CHART],
[ChartType.LINE, IconNames.TIMELINE_LINE_CHART],
[ChartType.OHLC, IconNames.WATERFALL_CHART],
]);
export type CandlesChartContainerProps = { export type CandlesChartContainerProps = {
marketId: string; marketId: string;
@@ -99,161 +19,32 @@ export const CandlesChartContainer = ({
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const { theme } = useThemeSwitcher(); const { theme } = useThemeSwitcher();
const settings = useCandlesChartSettings(); const { interval, chartType, overlays, studies, merge } =
useCandlesChartSettings();
const interval: Interval = getValidItem(
settings.interval,
Object.values(Interval),
Interval.I15M
);
const chartType: ChartType = getValidItem(
settings.type,
Object.values(ChartType),
ChartType.CANDLE
);
const overlays: Overlay[] = getValidSubset(
settings.overlays,
Object.values(Overlay),
[]
);
const studies: Study[] = getValidSubset(
settings.studies,
Object.values(Study),
[Study.VOLUME]
);
const dataSource = useMemo(() => { const dataSource = useMemo(() => {
return new VegaDataSource(client, marketId, pubKey); return new VegaDataSource(client, marketId, pubKey);
}, [client, marketId, pubKey]); }, [client, marketId, pubKey]);
return ( return (
<div className="h-full flex flex-col"> <CandlestickChart
<div className="px-3 lg:px-4 py-2 flex flex-row flex-wrap gap-2 bg-vega-clight-700 dark:bg-vega-cdark-700"> dataSource={dataSource}
<DropdownMenu options={{
trigger={ chartType,
<DropdownMenuTrigger> overlays,
{t(`Interval: ${intervalLabels[interval]}`)} studies,
</DropdownMenuTrigger> notEnoughDataText: (
} <span className="text-xs text-center">{t('No data')}</span>
> ),
<DropdownMenuContent> }}
<DropdownMenuRadioGroup interval={interval}
value={interval} theme={theme}
onValueChange={(value) => { onOptionsChanged={(options) => {
settings.setInterval(value as Interval); merge({
}} overlays: options.overlays,
> studies: options.studies,
{Object.values(Interval).map((timeInterval) => ( });
<DropdownMenuRadioItem }}
key={timeInterval} />
inset
value={timeInterval}
>
{intervalLabels[timeInterval]}
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={
<DropdownMenuTrigger>
<Icon name={chartTypeIcon.get(chartType) as IconName} />
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
<DropdownMenuRadioGroup
value={chartType}
onValueChange={(value) => {
settings.setType(value as ChartType);
}}
>
{Object.values(ChartType).map((type) => (
<DropdownMenuRadioItem key={type} inset value={type}>
{chartTypeLabels[type]}
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={<DropdownMenuTrigger>{t('Overlays')}</DropdownMenuTrigger>}
>
<DropdownMenuContent>
{Object.values(Overlay).map((overlay) => (
<DropdownMenuCheckboxItem
key={overlay}
checked={overlays.includes(overlay)}
onCheckedChange={() => {
const newOverlays = [...overlays];
const index = overlays.findIndex((item) => item === overlay);
index !== -1
? newOverlays.splice(index, 1)
: newOverlays.push(overlay);
settings.setOverlays(newOverlays);
}}
>
{overlayLabels[overlay]}
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={<DropdownMenuTrigger>{t('Studies')}</DropdownMenuTrigger>}
>
<DropdownMenuContent>
{Object.values(Study).map((study) => (
<DropdownMenuCheckboxItem
key={study}
checked={studies.includes(study)}
onCheckedChange={() => {
const newStudies = [...studies];
const index = studies.findIndex((item) => item === study);
index !== -1
? newStudies.splice(index, 1)
: newStudies.push(study);
settings.setStudies(newStudies);
}}
>
{studyLabels[study]}
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex-1">
<CandlestickChart
dataSource={dataSource}
options={{
chartType: chartType,
overlays: overlays,
studies: studies,
notEnoughDataText: (
<span className="text-xs text-center">{t('No data')}</span>
),
}}
interval={interval}
theme={theme}
onOptionsChanged={(options) => {
settings.merge({
overlays: options.overlays,
studies: options.studies,
});
}}
/>
</div>
</div>
); );
}; };
@@ -0,0 +1,17 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CandlesMenu } from './candles-menu';
describe('CandlesMenu', () => {
it('should render with volume study showing by default', async () => {
render(<CandlesMenu />);
await userEvent.click(
screen.getByText('Studies', {
selector: '[type="button"]',
})
);
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(screen.getByText('Volume')).toHaveAttribute('data-state', 'checked');
});
});
+160
View File
@@ -0,0 +1,160 @@
import 'pennant/dist/style.css';
import {
ChartType,
Interval,
Overlay,
Study,
chartTypeLabels,
intervalLabels,
overlayLabels,
studyLabels,
} from 'pennant';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItemIndicator,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
Icon,
} from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@blueprintjs/icons';
import { IconNames } from '@blueprintjs/icons';
import { t } from '@vegaprotocol/i18n';
import { useCandlesChartSettings } from './use-candles-chart-settings';
const chartTypeIcon = new Map<ChartType, IconName>([
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
[ChartType.CANDLE, IconNames.WATERFALL_CHART],
[ChartType.LINE, IconNames.TIMELINE_LINE_CHART],
[ChartType.OHLC, IconNames.WATERFALL_CHART],
]);
export const CandlesMenu = () => {
const {
interval,
chartType,
studies,
overlays,
setInterval,
setType,
setStudies,
setOverlays,
} = useCandlesChartSettings();
const triggerClasses = 'text-xs';
const contentAlign = 'end';
return (
<>
<DropdownMenu
trigger={
<DropdownMenuTrigger className={triggerClasses}>
{t(`Interval: ${intervalLabels[interval]}`)}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent align={contentAlign}>
<DropdownMenuRadioGroup
value={interval}
onValueChange={(value) => {
setInterval(value as Interval);
}}
>
{Object.values(Interval).map((timeInterval) => (
<DropdownMenuRadioItem
key={timeInterval}
inset
value={timeInterval}
>
{intervalLabels[timeInterval]}
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={
<DropdownMenuTrigger className={triggerClasses}>
<Icon name={chartTypeIcon.get(chartType) as IconName} />
</DropdownMenuTrigger>
}
>
<DropdownMenuContent align={contentAlign}>
<DropdownMenuRadioGroup
value={chartType}
onValueChange={(value) => {
setType(value as ChartType);
}}
>
{Object.values(ChartType).map((type) => (
<DropdownMenuRadioItem key={type} inset value={type}>
{chartTypeLabels[type]}
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={
<DropdownMenuTrigger className={triggerClasses}>
{t('Overlays')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent align={contentAlign}>
{Object.values(Overlay).map((overlay) => (
<DropdownMenuCheckboxItem
key={overlay}
checked={overlays.includes(overlay)}
onCheckedChange={() => {
const newOverlays = [...overlays];
const index = overlays.findIndex((item) => item === overlay);
index !== -1
? newOverlays.splice(index, 1)
: newOverlays.push(overlay);
setOverlays(newOverlays);
}}
>
{overlayLabels[overlay]}
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={
<DropdownMenuTrigger className={triggerClasses}>
{t('Studies')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent align={contentAlign}>
{Object.values(Study).map((study) => (
<DropdownMenuCheckboxItem
key={study}
checked={studies.includes(study)}
onCheckedChange={() => {
const newStudies = [...studies];
const index = studies.findIndex((item) => item === study);
index !== -1
? newStudies.splice(index, 1)
: newStudies.push(study);
setStudies(newStudies);
}}
>
{studyLabels[study]}
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</>
);
};
+1
View File
@@ -1,4 +1,5 @@
export * from './__generated__/Candles'; export * from './__generated__/Candles';
export * from './__generated__/Chart'; export * from './__generated__/Chart';
export * from './candles-chart'; export * from './candles-chart';
export * from './candles-menu';
export * from './data-source'; export * from './data-source';
@@ -0,0 +1,99 @@
import { getValidItem, getValidSubset } from '@vegaprotocol/react-helpers';
import { ChartType, Interval, Study } from 'pennant';
import { Overlay } from 'pennant';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface StoredSettings {
interval: Interval;
type: ChartType;
overlays: Overlay[];
studies: Study[];
}
const DEFAULT_CHART_SETTINGS = {
interval: Interval.I15M,
type: ChartType.CANDLE,
overlays: [],
studies: [Study.VOLUME],
};
export const useCandlesChartSettingsStore = create<
StoredSettings & {
merge: (settings: Partial<StoredSettings>) => void;
setType: (type: ChartType) => void;
setInterval: (interval: Interval) => void;
setOverlays: (overlays: Overlay[]) => void;
setStudies: (studies: Study[]) => void;
}
>()(
persist(
immer((set) => ({
...DEFAULT_CHART_SETTINGS,
merge: (settings: Partial<StoredSettings>) =>
set((state) => {
Object.assign(state, settings);
}),
setType: (type) =>
set((state) => {
state.type = type;
}),
setInterval: (interval) =>
set((state) => {
state.interval = interval;
}),
setOverlays: (overlays) =>
set((state) => {
state.overlays = overlays;
}),
setStudies: (studies) =>
set((state) => {
state.studies = studies;
}),
})),
{
name: 'vega_candles_chart_store',
}
)
);
export const useCandlesChartSettings = () => {
const settings = useCandlesChartSettingsStore();
const interval: Interval = getValidItem(
settings.interval,
Object.values(Interval),
Interval.I15M
);
const chartType: ChartType = getValidItem(
settings.type,
Object.values(ChartType),
ChartType.CANDLE
);
const overlays: Overlay[] = getValidSubset(
settings.overlays,
Object.values(Overlay),
[]
);
const studies: Study[] = getValidSubset(
settings.studies,
Object.values(Study),
[Study.VOLUME]
);
return {
interval,
chartType,
overlays,
studies,
setInterval: settings.setInterval,
setType: settings.setType,
setStudies: settings.setStudies,
setOverlays: settings.setOverlays,
merge: settings.merge,
};
};
+2
View File
@@ -12,6 +12,7 @@ import { addUpdateCapsuleMultiSig } from './lib/commands/add-validators-to-multi
import { import {
addVegaWalletConnect, addVegaWalletConnect,
addSetVegaWallet, addSetVegaWallet,
addSetOnBoardingViewed,
} from './lib/commands/vega-wallet-connect'; } from './lib/commands/vega-wallet-connect';
import { addMockTransactionResponse } from './lib/commands/mock-transaction-response'; import { addMockTransactionResponse } from './lib/commands/mock-transaction-response';
import { addCreateMarket } from './lib/commands/create-market'; import { addCreateMarket } from './lib/commands/create-market';
@@ -38,6 +39,7 @@ addGetNetworkParameters();
addUpdateCapsuleMultiSig(); addUpdateCapsuleMultiSig();
addVegaWalletConnect(); addVegaWalletConnect();
addSetVegaWallet(); addSetVegaWallet();
addSetOnBoardingViewed();
addMockTransactionResponse(); addMockTransactionResponse();
addCreateMarket(); addCreateMarket();
addConnectPublicKey(); addConnectPublicKey();
@@ -14,6 +14,10 @@ declare global {
interface Chainable<Subject> { interface Chainable<Subject> {
setVegaWallet(): void; setVegaWallet(): void;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
setOnBoardingViewed(): void;
}
} }
} }
@@ -60,7 +64,7 @@ export function addVegaWalletConnect() {
export function addSetVegaWallet() { export function addSetVegaWallet() {
Cypress.Commands.add('setVegaWallet', () => { Cypress.Commands.add('setVegaWallet', () => {
cy.window().then((win) => { cy.window().then((win) => {
win.localStorage.setItem('vega_risk_accepted', 'true'); win.localStorage.setItem('vega_onboarding_viewed', 'true');
win.localStorage.setItem( win.localStorage.setItem(
'vega_wallet_config', 'vega_wallet_config',
JSON.stringify({ JSON.stringify({
@@ -72,3 +76,11 @@ export function addSetVegaWallet() {
}); });
}); });
} }
export function addSetOnBoardingViewed() {
Cypress.Commands.add('setOnBoardingViewed', () => {
cy.window().then((win) => {
win.localStorage.setItem('vega_onboarding_viewed', 'true');
});
});
}
-1
View File
@@ -21,5 +21,4 @@ export * from './lib/type-helpers';
export * from './lib/cells/grid-progress-bar'; export * from './lib/cells/grid-progress-bar';
export * from './lib/ag-grid-update';
export * from './lib/use-datagrid-events'; export * from './lib/use-datagrid-events';
-20
View File
@@ -1,20 +0,0 @@
import type { MutableRefObject, RefObject } from 'react';
import type { AgGridReact } from 'ag-grid-react';
type AnyArray = Array<any> | null; // eslint-disable-line @typescript-eslint/no-explicit-any
export const isXOrWasEmpty = (prev?: AnyArray, curr?: AnyArray) =>
Boolean(Number(!!prev?.length) ^ Number(!!curr?.length));
export const updateGridData = (
dataRef: MutableRefObject<AnyArray>,
data: AnyArray,
gridRef: RefObject<AgGridReact>
) => {
const rerender = isXOrWasEmpty(dataRef.current, data);
dataRef.current = data;
if (gridRef.current?.api?.getModel().getType() === 'infinite') {
gridRef.current?.api?.refreshInfiniteCache();
}
return !rerender;
};
@@ -3,7 +3,7 @@ import { useCallback } from 'react';
import get from 'lodash/get'; import get from 'lodash/get';
interface MarketNameCellProps { interface MarketNameCellProps {
value?: string; value?: string | null;
data?: { id?: string; marketId?: string; market?: { id: string } }; data?: { id?: string; marketId?: string; market?: { id: string } };
idPath?: string; idPath?: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void; onMarketClick?: (marketId: string, metaKey?: boolean) => void;
@@ -1,5 +1,5 @@
import type { Control } from 'react-hook-form'; import type { Control } from 'react-hook-form';
import type { Market, MarketData } from '@vegaprotocol/markets'; import type { Market, StaticMarketData } from '@vegaprotocol/markets';
import { DealTicketMarketAmount } from './deal-ticket-market-amount'; import { DealTicketMarketAmount } from './deal-ticket-market-amount';
import { DealTicketLimitAmount } from './deal-ticket-limit-amount'; import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
@@ -9,7 +9,8 @@ import type { OrderFormFields } from '../../hooks/use-order-form';
export interface DealTicketAmountProps { export interface DealTicketAmountProps {
control: Control<OrderFormFields>; control: Control<OrderFormFields>;
orderType: Schema.OrderType; orderType: Schema.OrderType;
marketData: MarketData; marketData: StaticMarketData;
marketPrice?: string;
market: Market; market: Market;
sizeError?: string; sizeError?: string;
priceError?: string; priceError?: string;
@@ -21,11 +22,18 @@ export interface DealTicketAmountProps {
export const DealTicketAmount = ({ export const DealTicketAmount = ({
orderType, orderType,
marketData, marketData,
marketPrice,
...props ...props
}: DealTicketAmountProps) => { }: DealTicketAmountProps) => {
switch (orderType) { switch (orderType) {
case Schema.OrderType.TYPE_MARKET: case Schema.OrderType.TYPE_MARKET:
return <DealTicketMarketAmount {...props} marketData={marketData} />; return (
<DealTicketMarketAmount
{...props}
marketData={marketData}
marketPrice={marketPrice}
/>
);
case Schema.OrderType.TYPE_LIMIT: case Schema.OrderType.TYPE_LIMIT:
return <DealTicketLimitAmount {...props} />; return <DealTicketLimitAmount {...props} />;
default: { default: {
@@ -4,9 +4,10 @@ import classNames from 'classnames';
interface Props { interface Props {
side: Side; side: Side;
label?: string;
} }
export const DealTicketButton = ({ side }: Props) => { export const DealTicketButton = ({ side, label }: Props) => {
const buttonClasses = classNames( const buttonClasses = classNames(
'px-10 py-2 uppercase rounded-md text-white w-full', 'px-10 py-2 uppercase rounded-md text-white w-full',
{ {
@@ -17,7 +18,7 @@ export const DealTicketButton = ({ side }: Props) => {
return ( return (
<div className="mb-2"> <div className="mb-2">
<button type="submit" data-testid="place-order" className={buttonClasses}> <button type="submit" data-testid="place-order" className={buttonClasses}>
{t('Place order')} {label || t('Place order')}
</button> </button>
</div> </div>
); );
@@ -1,11 +1,20 @@
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
import {
DealTicketType,
useDealTicketTypeStore,
} from '../../hooks/use-type-store';
import { StopOrder } from './deal-ticket-stop-order';
import {
useStaticMarketData,
useMarket,
useMarketPrice,
} from '@vegaprotocol/markets';
import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
import { useMarket, marketDataProvider } from '@vegaprotocol/markets';
import { DealTicket } from './deal-ticket'; import { DealTicket } from './deal-ticket';
import { FLAGS } from '@vegaprotocol/environment';
export interface DealTicketContainerProps { interface DealTicketContainerProps {
marketId: string; marketId: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void; onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onClickCollateral?: () => void; onClickCollateral?: () => void;
@@ -14,10 +23,9 @@ export interface DealTicketContainerProps {
export const DealTicketContainer = ({ export const DealTicketContainer = ({
marketId, marketId,
onMarketClick, ...props
onClickCollateral,
onDeposit,
}: DealTicketContainerProps) => { }: DealTicketContainerProps) => {
const type = useDealTicketTypeStore((state) => state.type[marketId]);
const { const {
data: market, data: market,
error: marketError, error: marketError,
@@ -29,15 +37,9 @@ export const DealTicketContainer = ({
error: marketDataError, error: marketDataError,
loading: marketDataLoading, loading: marketDataLoading,
reload, reload,
} = useThrottledDataProvider( } = useStaticMarketData(marketId);
{ const { data: marketPrice } = useMarketPrice(market?.id);
dataProvider: marketDataProvider,
variables: { marketId },
},
1000
);
const create = useVegaTransactionStore((state) => state.create); const create = useVegaTransactionStore((state) => state.create);
return ( return (
<AsyncRenderer <AsyncRenderer
data={market && marketData} data={market && marketData}
@@ -46,14 +48,23 @@ export const DealTicketContainer = ({
reload={reload} reload={reload}
> >
{market && marketData ? ( {market && marketData ? (
<DealTicket FLAGS.STOP_ORDERS &&
market={market} (type === DealTicketType.StopLimit ||
marketData={marketData} type === DealTicketType.StopMarket) ? (
submit={(orderSubmission) => create({ orderSubmission })} <StopOrder
onClickCollateral={onClickCollateral} market={market}
onMarketClick={onMarketClick} marketPrice={marketPrice}
onDeposit={onDeposit} submit={(stopOrdersSubmission) => create({ stopOrdersSubmission })}
/> />
) : (
<DealTicket
{...props}
market={market}
marketPrice={marketPrice}
marketData={marketData}
submit={(orderSubmission) => create({ orderSubmission })}
/>
)
) : ( ) : (
<Splash> <Splash>
<p>{t('Could not load market')}</p> <p>{t('Could not load market')}</p>
@@ -4,11 +4,11 @@ import classnames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { FeesBreakdown } from '@vegaprotocol/markets'; import { FeesBreakdown } from '@vegaprotocol/markets';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import type { Market } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets';
import type { EstimatePositionQuery } from '@vegaprotocol/positions'; import type { EstimatePositionQuery } from '@vegaprotocol/positions';
import type { EstimateFeesQuery } from '../../hooks/__generated__/EstimateOrder';
import { AccountBreakdownDialog } from '@vegaprotocol/accounts'; import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
import { formatRange, formatValue } from '@vegaprotocol/utils'; import { formatRange, formatValue } from '@vegaprotocol/utils';
@@ -24,6 +24,7 @@ import {
EST_TOTAL_MARGIN_TOOLTIP_TEXT, EST_TOTAL_MARGIN_TOOLTIP_TEXT,
MARGIN_ACCOUNT_TOOLTIP_TEXT, MARGIN_ACCOUNT_TOOLTIP_TEXT,
} from '../../constants'; } from '../../constants';
import { useEstimateFees } from '../../hooks';
const emptyValue = '-'; const emptyValue = '-';
@@ -76,26 +77,82 @@ export const DealTicketFeeDetail = ({
}; };
export interface DealTicketFeeDetailsProps { export interface DealTicketFeeDetailsProps {
assetSymbol: string;
order: OrderSubmissionBody['orderSubmission'];
market: Market;
notionalSize: string | null;
}
export const DealTicketFeeDetails = ({
assetSymbol,
order,
market,
notionalSize,
}: DealTicketFeeDetailsProps) => {
const feeEstimate = useEstimateFees(order);
const { settlementAsset: asset } =
market.tradableInstrument.instrument.product;
const { decimals: assetDecimals, quantum } = asset;
const marketDecimals = market.decimalPlaces;
const quoteName = market.tradableInstrument.instrument.product.quoteName;
return (
<>
<DealTicketFeeDetail
label={t('Notional')}
value={formatValue(notionalSize, marketDecimals)}
formattedValue={formatValue(notionalSize, marketDecimals)}
symbol={quoteName}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
/>
<DealTicketFeeDetail
label={t('Fees')}
value={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
}
formattedValue={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
}
labelDescription={
<>
<span>
{t(
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
)}
</span>
<FeesBreakdown
fees={feeEstimate?.fees}
feeFactors={market.fees.factors}
symbol={assetSymbol}
decimals={assetDecimals}
/>
</>
}
symbol={assetSymbol}
/>
</>
);
};
export interface DealTicketMarginDetailsProps {
generalAccountBalance?: string; generalAccountBalance?: string;
marginAccountBalance?: string; marginAccountBalance?: string;
market: Market; market: Market;
onMarketClick?: (marketId: string, metaKey?: boolean) => void; onMarketClick?: (marketId: string, metaKey?: boolean) => void;
assetSymbol: string; assetSymbol: string;
notionalSize: string | null;
feeEstimate: EstimateFeesQuery['estimateFees'] | undefined;
positionEstimate: EstimatePositionQuery['estimatePosition']; positionEstimate: EstimatePositionQuery['estimatePosition'];
} }
export const DealTicketFeeDetails = ({ export const DealTicketMarginDetails = ({
marginAccountBalance, marginAccountBalance,
generalAccountBalance, generalAccountBalance,
assetSymbol, assetSymbol,
feeEstimate,
market, market,
onMarketClick, onMarketClick,
notionalSize,
positionEstimate, positionEstimate,
}: DealTicketFeeDetailsProps) => { }: DealTicketMarginDetailsProps) => {
const [breakdownDialog, setBreakdownDialog] = useState(false); const [breakdownDialog, setBreakdownDialog] = useState(false);
const { pubKey: partyId } = useVegaWallet(); const { pubKey: partyId } = useVegaWallet();
const { data: currentMargins } = useDataProvider({ const { data: currentMargins } = useDataProvider({
@@ -110,7 +167,6 @@ export const DealTicketFeeDetails = ({
const { settlementAsset: asset } = const { settlementAsset: asset } =
market.tradableInstrument.instrument.product; market.tradableInstrument.instrument.product;
const { decimals: assetDecimals, quantum } = asset; const { decimals: assetDecimals, quantum } = asset;
const marketDecimals = market.decimalPlaces;
let marginRequiredBestCase: string | undefined = undefined; let marginRequiredBestCase: string | undefined = undefined;
let marginRequiredWorstCase: string | undefined = undefined; let marginRequiredWorstCase: string | undefined = undefined;
if (marginEstimate) { if (marginEstimate) {
@@ -251,41 +307,7 @@ export const DealTicketFeeDetails = ({
const quoteName = market.tradableInstrument.instrument.product.quoteName; const quoteName = market.tradableInstrument.instrument.product.quoteName;
return ( return (
<div> <>
<DealTicketFeeDetail
label={t('Notional')}
value={formatValue(notionalSize, marketDecimals)}
formattedValue={formatValue(notionalSize, marketDecimals)}
symbol={quoteName}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
/>
<DealTicketFeeDetail
label={t('Fees')}
value={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
}
formattedValue={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
}
labelDescription={
<>
<span>
{t(
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
)}
</span>
<FeesBreakdown
fees={feeEstimate?.fees}
feeFactors={market.fees.factors}
symbol={assetSymbol}
decimals={assetDecimals}
/>
</>
}
symbol={assetSymbol}
/>
<DealTicketFeeDetail <DealTicketFeeDetail
label={t('Margin required')} label={t('Margin required')}
value={formatRange( value={formatRange(
@@ -351,6 +373,6 @@ export const DealTicketFeeDetails = ({
onClose={onAccountBreakdownDialogClose} onClose={onAccountBreakdownDialogClose}
/> />
)} )}
</div> </>
); );
}; };
@@ -44,12 +44,12 @@ export const DealTicketLimitAmount = ({
return ( return (
<div className="mb-2"> <div className="mb-2">
<div className="flex items-center gap-4"> <div className="flex items-start gap-4">
<div className="flex-1"> <div className="flex-1">
<FormGroup <FormGroup
label={t('Size')} label={t('Size')}
labelFor="input-order-size-limit" labelFor="input-order-size-limit"
className="!mb-1" className="!mb-0"
> >
<Controller <Controller
name="size" name="size"
@@ -78,16 +78,13 @@ export const DealTicketLimitAmount = ({
/> />
</FormGroup> </FormGroup>
</div> </div>
<div className="flex-0 items-center"> <div className="pt-7 leading-10">@</div>
<div className="flex">&nbsp;</div>
<div className="flex">@</div>
</div>
<div className="flex-1"> <div className="flex-1">
<FormGroup <FormGroup
labelFor="input-price-quote" labelFor="input-price-quote"
label={t(`Price (${quoteName})`)} label={t(`Price (${quoteName})`)}
labelAlign="right" labelAlign="right"
className="!mb-1" className="!mb-0"
> >
<Controller <Controller
name="price" name="price"
@@ -5,10 +5,10 @@ import {
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit'; import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit';
import { isMarketInAuction } from '../../utils'; import { isMarketInAuction } from '@vegaprotocol/markets';
import type { DealTicketAmountProps } from './deal-ticket-amount'; import type { DealTicketAmountProps } from './deal-ticket-amount';
import { getMarketPrice } from '../../utils/get-price';
import { Controller } from 'react-hook-form'; import { Controller } from 'react-hook-form';
import classNames from 'classnames';
export type DealTicketMarketAmountProps = Omit< export type DealTicketMarketAmountProps = Omit<
DealTicketAmountProps, DealTicketAmountProps,
@@ -19,37 +19,26 @@ export const DealTicketMarketAmount = ({
control, control,
market, market,
marketData, marketData,
marketPrice,
sizeError, sizeError,
update, update,
size, size,
}: DealTicketMarketAmountProps) => { }: DealTicketMarketAmountProps) => {
const quoteName = market.tradableInstrument.instrument.product.quoteName; const quoteName = market.tradableInstrument.instrument.product.quoteName;
const sizeStep = toDecimal(market?.positionDecimalPlaces); const sizeStep = toDecimal(market?.positionDecimalPlaces);
const price = getMarketPrice(marketData); const price = marketPrice;
const priceFormatted = price const priceFormatted = price
? addDecimalsFormatNumber(price, market.decimalPlaces) ? addDecimalsFormatNumber(price, market.decimalPlaces)
: undefined; : undefined;
const inAuction = isMarketInAuction(marketData.marketTradingMode);
return ( return (
<div className="mb-2"> <div className="mb-2">
<div className="flex items-end gap-4 mb-2"> <div className="flex items-start gap-4">
<div className="flex-1 text-sm">{t('Size')}</div>
<div />
<div className="flex-2 text-sm text-right">
{isMarketInAuction(marketData.marketTradingMode) && (
<Tooltip
description={t(
'This market is in auction. The uncrossing price is an indication of what the price is expected to be when the auction ends.'
)}
>
<div>{t(`Indicative price`)}</div>
</Tooltip>
)}
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex-1"> <div className="flex-1">
<div className="mb-2 text-sm">{t('Size')}</div>
<Controller <Controller
name="size" name="size"
control={control} control={control}
@@ -76,15 +65,29 @@ export const DealTicketMarketAmount = ({
)} )}
/> />
</div> </div>
<div>@</div> <div className="pt-7 leading-10">@</div>
<div className="flex-1 text-sm text-right" data-testid="last-price"> <div className="flex-1 text-sm text-right">
{priceFormatted && quoteName ? ( {inAuction && (
<> <Tooltip
~{priceFormatted} {quoteName} description={t(
</> 'This market is in auction. The uncrossing price is an indication of what the price is expected to be when the auction ends.'
) : ( )}
'-' >
<div className="mb-2">{t(`Indicative price`)}</div>
</Tooltip>
)} )}
<div
data-testid="last-price"
className={classNames('leading-10', { 'pt-7': !inAuction })}
>
{priceFormatted && quoteName ? (
<>
~{priceFormatted} {quoteName}
</>
) : (
'-'
)}
</div>
</div> </div>
</div> </div>
{sizeError && ( {sizeError && (
@@ -0,0 +1,311 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { generateMarket } from '../../test-helpers';
import { StopOrder } from './deal-ticket-stop-order';
import * as Schema from '@vegaprotocol/types';
import { MockedProvider } from '@apollo/client/testing';
import type { StopOrderFormValues } from '../../hooks/use-stop-order-form-values';
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
import type { FeatureFlags } from '@vegaprotocol/environment';
jest.mock('zustand');
jest.mock('./deal-ticket-fee-details', () => ({
DealTicketFeeDetails: () => <div data-testid="deal-ticket-fee-details" />,
}));
jest.mock('@vegaprotocol/environment', () => {
const actual = jest.requireActual('@vegaprotocol/environment');
return {
...actual,
FLAGS: {
...actual.FLAGS,
STOP_ORDERS: true,
} as FeatureFlags,
};
});
const marketPrice = '200';
const market = generateMarket();
const submit = jest.fn();
function generateJsx(pubKey: string | null = 'pubKey', isReadOnly = false) {
return (
<MockedProvider>
<VegaWalletContext.Provider value={{ pubKey, isReadOnly } as any}>
<StopOrder market={market} marketPrice={marketPrice} submit={submit} />
</VegaWalletContext.Provider>
</MockedProvider>
);
}
const submitButton = 'place-order';
const sizeInput = 'order-size';
const priceInput = 'order-price';
const triggerPriceInput = 'triggerPrice';
const triggerTrailingPercentOffsetInput = 'triggerTrailingPercentOffset';
const orderTypeTrigger = 'order-type-Stop';
const orderTypeLimit = 'order-type-StopLimit';
const orderTypeMarket = 'order-type-StopMarket';
const orderSideBuy = 'order-side-SIDE_BUY';
const orderSideSell = 'order-side-SIDE_SELL';
const triggerDirectionRisesAbove = 'triggerDirection-risesAbove';
// const triggerDirectionFallsBelow = 'triggerDirection-fallsBelow';
const expiryStrategySubmit = 'expiryStrategy-submit';
const expiryStrategyCancel = 'expiryStrategy-cancel';
const triggerTypePrice = 'triggerType-price';
const triggerTypeTrailingPercentOffset = 'triggerType-trailingPercentOffset';
const expire = 'expire';
const datePicker = 'date-picker-field';
const timeInForce = 'order-tif';
const sizeErrorMessage = 'stop-order-error-message-size';
const priceErrorMessage = 'stop-order-error-message-price';
const triggerPriceErrorMessage = 'stop-order-error-message-trigger-price';
const triggerTrailingPercentOffsetErrorMessage =
'stop-order-error-message-trigger-trailing-percent-offset';
describe('StopOrder', () => {
beforeEach(() => {
localStorage.clear();
});
afterEach(() => {
localStorage.clear();
jest.clearAllMocks();
});
it('should display ticket defaults', async () => {
render(generateJsx());
// place order button should always be enabled
expect(screen.getByTestId(submitButton)).toBeEnabled();
// Assert defaults are used
await userEvent.click(screen.getByTestId(orderTypeTrigger));
expect(screen.getByTestId(orderTypeLimit).dataset.state).toEqual('checked');
await userEvent.click(screen.getByTestId(orderTypeLimit));
expect(screen.getByTestId(orderSideBuy).dataset.state).toEqual('checked');
expect(screen.getByTestId(sizeInput)).toHaveDisplayValue('0');
expect(screen.getByTestId(timeInForce)).toHaveValue(
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
);
expect(
screen.getByTestId(triggerDirectionRisesAbove).dataset.state
).toEqual('checked');
expect(screen.getByTestId(triggerTypePrice).dataset.state).toEqual(
'checked'
);
expect(screen.getByTestId(expire).dataset.state).toEqual('unchecked');
await userEvent.click(screen.getByTestId(expire));
await waitFor(() => {
expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual(
'checked'
);
});
});
it('should display trigger price as price for market type order', async () => {
render(generateJsx());
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeMarket));
await userEvent.type(screen.getByTestId(triggerPriceInput), '10');
expect(screen.getByTestId('price')).toHaveTextContent('10.0');
});
it('should use local storage state for initial values', async () => {
const values: Partial<StopOrderFormValues> = {
type: Schema.OrderType.TYPE_LIMIT,
side: Schema.Side.SIDE_SELL,
size: '0.1',
price: '300.22',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
expire: true,
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS,
expiresAt: '2023-07-27T16:43:27.000',
};
useStopOrderFormValues.setState({
formValues: {
[market.id]: values,
},
});
render(generateJsx());
// Assert correct defaults are used from store
await userEvent.click(screen.getByTestId(orderTypeTrigger));
expect(screen.queryByTestId(orderTypeLimit)).toBeChecked();
expect(screen.getByTestId(orderSideSell).dataset.state).toEqual('checked');
expect(screen.getByTestId(sizeInput)).toHaveDisplayValue(
values.size as string
);
expect(screen.getByTestId('order-tif')).toHaveValue(values.timeInForce);
expect(screen.getByTestId(priceInput)).toHaveDisplayValue(
values.price as string
);
expect(screen.getByTestId(expire).dataset.state).toEqual('checked');
expect(screen.getByTestId(expiryStrategyCancel).dataset.state).toEqual(
'checked'
);
expect(screen.getByTestId(datePicker)).toHaveDisplayValue(
values.expiresAt as string
);
});
it('does not submit if no wallet connected', async () => {
render(generateJsx(null));
await userEvent.type(screen.getByTestId(sizeInput), '1');
await userEvent.type(screen.getByTestId(priceInput), '1');
await userEvent.type(screen.getByTestId(triggerPriceInput), '1');
await userEvent.click(screen.getByTestId(submitButton));
expect(submit).not.toBeCalled();
});
it('calls submit if form is valid', async () => {
render(generateJsx());
await userEvent.type(screen.getByTestId(sizeInput), '1');
await userEvent.type(screen.getByTestId(priceInput), '1');
await userEvent.type(screen.getByTestId(triggerPriceInput), '1');
await userEvent.click(screen.getByTestId(submitButton));
expect(submit).toBeCalled();
});
it('validates size field', async () => {
render(generateJsx());
await userEvent.click(screen.getByTestId(submitButton));
// default value should be invalid
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(screen.getByTestId(sizeInput), '0.01');
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(screen.getByTestId(sizeInput));
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
expect(screen.queryByTestId(sizeErrorMessage)).toBeNull();
});
it('validates price field', async () => {
render(generateJsx());
await userEvent.click(screen.getByTestId(submitButton));
// price error message should not show if size has error
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
await userEvent.type(screen.getByTestId(priceInput), '0.001');
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
// switch to market order type error should disappear
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeMarket));
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
// switch back to limit type
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeLimit));
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(screen.getByTestId(priceInput), '0.001');
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(screen.getByTestId(priceInput));
await userEvent.type(screen.getByTestId(priceInput), '0.01');
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
});
it('validates trigger price field', async () => {
render(generateJsx());
await userEvent.click(screen.getByTestId(submitButton));
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// switch to trailing percentage offset trigger type
await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset));
expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
// switch back to price trigger type
await userEvent.click(screen.getByTestId(triggerTypePrice));
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.001');
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(screen.getByTestId(triggerPriceInput));
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.01');
expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
});
it('validates trigger trailing percentage offset field', async () => {
render(generateJsx());
// should not show error with default form values
await userEvent.click(screen.getByTestId(submitButton));
expect(
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeNull();
// switch to trailing percentage offset trigger type
await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset));
expect(
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(
screen.getByTestId(triggerTrailingPercentOffsetInput),
'0.09'
);
expect(
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(
screen.getByTestId(triggerTrailingPercentOffsetInput)
);
await userEvent.type(
screen.getByTestId(triggerTrailingPercentOffsetInput),
'0.1'
);
expect(
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeNull();
// to big value should be invalid
await userEvent.clear(
screen.getByTestId(triggerTrailingPercentOffsetInput)
);
await userEvent.type(
screen.getByTestId(triggerTrailingPercentOffsetInput),
'99.91'
);
expect(
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(
screen.getByTestId(triggerTrailingPercentOffsetInput)
);
await userEvent.type(
screen.getByTestId(triggerTrailingPercentOffsetInput),
'99.9'
);
expect(
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeNull();
});
});
@@ -0,0 +1,588 @@
import type { FormEventHandler } from 'react';
import { useRef, useCallback, useEffect } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
import {
formatNumber,
removeDecimal,
toDecimal,
validateAmount,
} from '@vegaprotocol/utils';
import { useForm, Controller } from 'react-hook-form';
import * as Schema from '@vegaprotocol/types';
import {
Radio,
RadioGroup,
Input,
Checkbox,
FormGroup,
InputError,
Select,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
import { t } from '@vegaprotocol/i18n';
import { ExpirySelector } from './expiry-selector';
import { SideSelector } from './side-selector';
import { timeInForceLabel, useOrder } from '@vegaprotocol/orders';
import {
NoWalletWarning,
REDUCE_ONLY_TOOLTIP,
useNotionalSize,
} from './deal-ticket';
import { TypeToggle } from './type-selector';
import {
useStopOrderFormValues,
type StopOrderFormValues,
} from '../../hooks/use-stop-order-form-values';
import {
DealTicketType,
useDealTicketTypeStore,
} from '../../hooks/use-type-store';
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-stop-order-submission';
import { DealTicketButton } from './deal-ticket-button';
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
import { validateExpiration } from '../../utils';
export interface StopOrderProps {
market: Market;
marketPrice?: string | null;
submit: (order: StopOrdersSubmission) => void;
}
const defaultValues: Partial<StopOrderFormValues> = {
type: Schema.OrderType.TYPE_LIMIT,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
triggerType: 'price',
triggerDirection:
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT,
size: '0',
};
const stopSubmit: FormEventHandler = (e) => e.preventDefault();
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
const { pubKey, isReadOnly } = useVegaWallet();
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
const [, updateOrder] = useOrder(market.id);
const updateStoredFormValues = useStopOrderFormValues(
(state) => state.update
);
const storedFormValues = useStopOrderFormValues(
(state) => state.formValues[market.id]
);
const { handleSubmit, setValue, watch, control, formState } =
useForm<StopOrderFormValues>({
defaultValues: { ...defaultValues, ...storedFormValues },
});
const { errors } = formState;
const lastSubmitTime = useRef(0);
const onSubmit = useCallback(
(data: StopOrderFormValues) => {
const now = new Date().getTime();
if (lastSubmitTime.current && now - lastSubmitTime.current < 1000) {
return;
}
submit(
mapFormValuesToStopOrdersSubmission(
data,
market.id,
market.decimalPlaces,
market.positionDecimalPlaces
)
);
lastSubmitTime.current = now;
},
[market.id, market.decimalPlaces, market.positionDecimalPlaces, submit]
);
const side = watch('side');
const expire = watch('expire');
const triggerType = watch('triggerType');
const triggerPrice = watch('triggerPrice');
const timeInForce = watch('timeInForce');
const type = watch('type');
const rawPrice = watch('price');
const rawSize = watch('size');
if (storedFormValues?.size && rawSize !== storedFormValues?.size) {
setValue('size', storedFormValues.size);
}
if (storedFormValues?.price && rawPrice !== storedFormValues?.price) {
setValue('price', storedFormValues.price);
}
const isPriceTrigger = triggerType === 'price';
const size = removeDecimal(rawSize, market.positionDecimalPlaces);
const price =
marketPrice &&
getDerivedPrice(
{
type,
price: rawPrice && removeDecimal(rawPrice, market.decimalPlaces),
},
type === Schema.OrderType.TYPE_MARKET && isPriceTrigger && triggerPrice
? removeDecimal(triggerPrice, market.decimalPlaces)
: marketPrice
);
const notionalSize = useNotionalSize(
price,
size,
market.decimalPlaces,
market.positionDecimalPlaces
);
useEffect(() => {
const subscription = watch((value, { name, type }) => {
updateStoredFormValues(market.id, value);
});
return () => subscription.unsubscribe();
}, [watch, market.id, updateStoredFormValues]);
const { quoteName, settlementAsset: asset } =
market.tradableInstrument.instrument.product;
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const priceStep = toDecimal(market?.decimalPlaces);
const trailingPercentOffsetStep = '0.1';
const priceFormatted =
isPriceTrigger && triggerPrice
? formatNumber(triggerPrice, market.decimalPlaces)
: undefined;
return (
<form
onSubmit={isReadOnly || !pubKey ? stopSubmit : handleSubmit(onSubmit)}
noValidate
>
<Controller
name="type"
control={control}
render={({ field }) => {
const { value } = field;
return (
<TypeToggle
value={
value === Schema.OrderType.TYPE_LIMIT
? DealTicketType.StopLimit
: DealTicketType.StopMarket
}
onValueChange={(value) => {
const type = value as DealTicketType;
setDealTicketType(market.id, type);
if (
type === DealTicketType.Limit ||
type === DealTicketType.Market
) {
updateOrder({
type:
type === DealTicketType.Limit
? Schema.OrderType.TYPE_LIMIT
: Schema.OrderType.TYPE_MARKET,
});
return;
}
setValue(
'type',
type === DealTicketType.StopLimit
? Schema.OrderType.TYPE_LIMIT
: Schema.OrderType.TYPE_MARKET
);
}}
/>
);
}}
/>
{errors.type && (
<InputError testId="stop-order-error-message-type">
{errors.type.message}
</InputError>
)}
<Controller
name="side"
control={control}
render={({ field }) => (
<SideSelector value={field.value} onValueChange={field.onChange} />
)}
/>
<FormGroup label={t('Trigger')} compact={true} labelFor="">
<Controller
name="triggerDirection"
control={control}
render={({ field }) => {
const { onChange, value } = field;
return (
<RadioGroup
name="triggerDirection"
onChange={onChange}
value={value}
orientation="horizontal"
className="mb-2"
>
<Radio
value={
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_RISES_ABOVE
}
id="triggerDirection-risesAbove"
label={'Rises above'}
/>
<Radio
value={
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_FALLS_BELOW
}
id="triggerDirection-fallsBelow"
label={'Falls below'}
/>
</RadioGroup>
);
}}
/>
{isPriceTrigger && (
<div className="mb-2">
<Controller
name="triggerPrice"
rules={{
required: t('You need provide a price'),
min: {
value: priceStep,
message: t('Price cannot be lower than ' + priceStep),
},
validate: validateAmount(priceStep, 'Price'),
}}
control={control}
render={({ field }) => {
const { value, ...props } = field;
return (
<div className="mb-2">
<Input
data-testid="triggerPrice"
type="number"
step={priceStep}
appendElement={asset.symbol}
value={value || ''}
{...props}
/>
</div>
);
}}
/>
{errors.triggerPrice && (
<InputError testId="stop-order-error-message-trigger-price">
{errors.triggerPrice.message}
</InputError>
)}
</div>
)}
{!isPriceTrigger && (
<div className="mb-2">
<Controller
name="triggerTrailingPercentOffset"
control={control}
rules={{
required: t('You need provide a trailing percent offset'),
min: {
value: trailingPercentOffsetStep,
message: t(
'Trailing percent offset cannot be lower than ' +
trailingPercentOffsetStep
),
},
max: {
value: '99.9',
message: t(
'Trailing percent offset cannot be higher than 99.9'
),
},
validate: validateAmount(
trailingPercentOffsetStep,
'Trailing percentage offset'
),
}}
render={({ field }) => {
const { value, ...props } = field;
return (
<div className="mb-2">
<Input
type="number"
step={trailingPercentOffsetStep}
appendElement="%"
data-testid="triggerTrailingPercentOffset"
value={value || ''}
{...props}
/>
</div>
);
}}
/>
{errors.triggerTrailingPercentOffset && (
<InputError testId="stop-order-error-message-trigger-trailing-percent-offset">
{errors.triggerTrailingPercentOffset.message}
</InputError>
)}
</div>
)}
<Controller
name="triggerType"
control={control}
rules={{ deps: ['triggerTrailingPercentOffset', 'triggerPrice'] }}
render={({ field }) => {
const { onChange, value } = field;
return (
<RadioGroup
onChange={onChange}
value={value}
orientation="horizontal"
>
<Radio value="price" id="triggerType-price" label={'Price'} />
<Radio
value="trailingPercentOffset"
id="triggerType-trailingPercentOffset"
label={'Trailing Percent Offset'}
/>
</RadioGroup>
);
}}
/>
</FormGroup>
<div className="mb-2">
<div className="flex items-start gap-4">
<FormGroup
labelFor="input-price-quote"
label={t(`Size`)}
className="!mb-0 flex-1"
>
<Controller
name="size"
control={control}
rules={{
required: t('You need to provide a size'),
min: {
value: sizeStep,
message: t('Size cannot be lower than ' + sizeStep),
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={({ field }) => {
const { value, ...props } = field;
return (
<Input
id="order-size"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
value={value || ''}
{...props}
/>
);
}}
/>
</FormGroup>
<div className="pt-7 leading-10">@</div>
<div className="flex-1">
{type === Schema.OrderType.TYPE_LIMIT ? (
<FormGroup
labelFor="input-price-quote"
label={t(`Price (${quoteName})`)}
labelAlign="right"
className="!mb-0"
>
<Controller
name="price"
control={control}
rules={{
deps: 'type',
required: t('You need provide a price'),
min: {
value: priceStep,
message: t('Price cannot be lower than ' + priceStep),
},
validate: validateAmount(priceStep, 'Price'),
}}
render={({ field }) => {
const { value, ...props } = field;
return (
<Input
id="input-price-quote"
className="w-full"
type="number"
step={priceStep}
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
value={value || ''}
{...props}
/>
);
}}
/>
</FormGroup>
) : (
<div
className="text-sm text-right pt-7 leading-10"
data-testid="price"
>
{priceFormatted && quoteName
? `~${priceFormatted} ${quoteName}`
: '-'}
</div>
)}
</div>
</div>
{errors.size && (
<InputError testId="stop-order-error-message-size">
{errors.size.message}
</InputError>
)}
{!errors.size &&
errors.price &&
type === Schema.OrderType.TYPE_LIMIT && (
<InputError testId="stop-order-error-message-price">
{errors.price.message}
</InputError>
)}
</div>
<div className="mb-2">
<FormGroup
label={t('Time in force')}
labelFor="select-time-in-force"
compact={true}
>
<Controller
name="timeInForce"
control={control}
render={({ field }) => (
<Select
id="select-time-in-force"
className="w-full"
data-testid="order-tif"
{...field}
>
<option
key={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
value={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
>
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_IOC)}
</option>
<option
key={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
value={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
>
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_FOK)}
</option>
</Select>
)}
/>
</FormGroup>
{errors.timeInForce && (
<InputError testId="stop-error-message-tif">
{errors.timeInForce.message}
</InputError>
)}
</div>
<div className="flex gap-2 pb-2 justify-between">
<Controller
name="expire"
control={control}
render={({ field }) => {
const { onChange: onCheckedChange, value } = field;
return (
<Checkbox
onCheckedChange={onCheckedChange}
checked={value}
name="expire"
label={<span className="text-xs">{t('Expire')}</span>}
/>
);
}}
/>
<Checkbox
name="reduce-only"
checked={true}
disabled={true}
label={
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
<span className="text-xs">{t('Reduce only')}</span>
</Tooltip>
}
/>
</div>
{expire && (
<>
<FormGroup
label={t('Strategy')}
labelFor="expiryStrategy"
compact={true}
>
<Controller
name="expiryStrategy"
control={control}
render={({ field }) => {
return (
<RadioGroup orientation="horizontal" {...field}>
<Radio
value={
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
}
id="expiryStrategy-submit"
label={'Submit'}
/>
<Radio
value={
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
}
id="expiryStrategy-cancel"
label={'Cancel'}
/>
</RadioGroup>
);
}}
/>
</FormGroup>
<div className="mb-2">
<Controller
name="expiresAt"
control={control}
rules={{
validate: validateExpiration,
}}
render={({ field }) => {
const { value, onChange: onSelect } = field;
return (
<ExpirySelector
value={value}
onSelect={onSelect}
errorMessage={errors.expiresAt?.message}
/>
);
}}
/>
</div>
</>
)}
<NoWalletWarning isReadOnly={isReadOnly} />
<DealTicketButton side={side} label={t('Submit Stop Order')} />
<DealTicketFeeDetails
order={{
marketId: market.id,
price: price || undefined,
side,
size,
timeInForce,
type,
}}
notionalSize={notionalSize}
assetSymbol={asset.symbol}
market={market}
/>
</form>
);
};

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