Compare commits

..
Author SHA1 Message Date
Edd 6e50c42182 fix(explorer): properly render issue signatures tx 2023-08-23 16:49:40 +01:00
79 changed files with 1773 additions and 2303 deletions
+1 -1
View File
@@ -125,7 +125,7 @@ jobs:
#----------------------------------------------
- name: Run tests
working-directory: ./console-test
run: poetry run pytest -v -s --numprocesses auto --dist loadfile --durations=20
run: poetry run pytest -s --numprocesses auto --dist loadfile
- name: Check files
run: |
ls -al .
@@ -1,26 +0,0 @@
{
"proposalSubmission": {
"rationale": {
"title": "Test new asset proposal",
"description": "E2E test for proposals"
},
"terms": {
"newAsset": {
"changes": {
"name": "USDT Coin",
"symbol": "USDT",
"decimals": "18",
"quantum": "1",
"erc20": {
"contractAddress": "0xb404c51bbc10dcbe948077f18a4b8e553d160084",
"withdrawThreshold": "10",
"lifetimeLimit": "10"
}
}
},
"closingTimestamp": 1724339572,
"enactmentTimestamp": 1724339572,
"validationTimestamp": 1692799617
}
}
}
@@ -1,10 +1,9 @@
import { getNewAssetTxBody } from '../support/governance.functions';
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('/');
@@ -12,8 +11,6 @@ context('Proposal page', { tags: '@smoke' }, function () {
});
it('Able to view proposal', function () {
const proposalTitle = 'Add Lorem Ipsum market';
cy.navigate_to('governanceProposals');
cy.getByTestId(proposalHeading).should('be.visible');
cy.contains(proposalTitle)
@@ -25,9 +22,6 @@ context('Proposal page', { tags: '@smoke' }, function () {
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.getByTestId('vote-progress-bar-for')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
cy.get('[col-id="cDate"]')
.invoke('text')
.should('match', dateTimeRegex);
@@ -41,12 +35,9 @@ context('Proposal page', { tags: '@smoke' }, function () {
});
cy.getByTestId('dialog-title').should('have.text', proposalTitle);
cy.get('.language-json').should('exist');
cy.getByTestId('icon-cross').click();
});
it.skip('Proposal page displayed on mobile', function () {
const proposalTitle = 'Add Lorem Ipsum market';
cy.common_switch_to_mobile_and_click_toggle();
cy.navigate_to('governanceProposals', true);
cy.getByTestId(proposalHeading).should('be.visible');
@@ -54,40 +45,5 @@ context('Proposal page', { tags: '@smoke' }, function () {
cy.get_element_by_col_id('title').should('have.text', proposalTitle);
});
});
it('Able to view new asset proposal', function () {
const proposalTitle = 'Test new asset proposal';
const newAssetProposalBody = getNewAssetTxBody();
cy.VegaWalletSubmitProposal(newAssetProposalBody);
cy.visit('/');
cy.navigate_to('governanceProposals');
cy.contains(proposalTitle)
.parent()
.parent()
.parent()
.within(() => {
cy.get_element_by_col_id('title').should('have.text', proposalTitle);
cy.get_element_by_col_id('type').should('have.text', 'NewAsset');
cy.get_element_by_col_id('state').should(
'have.text',
'Waiting for Node Vote'
);
cy.getByTestId('vote-progress').should('be.visible');
cy.getByTestId('vote-progress-bar-against')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
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();
});
});
});
});
@@ -1,18 +1,8 @@
import { addSeconds, millisecondsToSeconds } from 'date-fns';
export function createSuccessorMarketProposal(parentMarketId) {
cy.VegaWalletSubmitProposal(getSuccessorTxBody(parentMarketId));
}
function getSuccessorTxBody(parentMarketId) {
const MIN_CLOSE_SEC = 500;
const MIN_ENACT_SEC = 700;
const closingDate = addSeconds(new Date(), MIN_CLOSE_SEC);
const enactmentDate = addSeconds(closingDate, MIN_ENACT_SEC);
const closingTimestamp = millisecondsToSeconds(closingDate.getTime());
const enactmentTimestamp = millisecondsToSeconds(enactmentDate.getTime());
return {
proposalSubmission: {
rationale: {
@@ -132,49 +122,8 @@ function getSuccessorTxBody(parentMarketId) {
},
},
},
closingTimestamp,
enactmentTimestamp,
},
},
};
}
export function getNewAssetTxBody() {
const MIN_CLOSE_SEC = 500;
const MIN_ENACT_SEC = 700;
const MIN_VALID_SEC = 60;
const closingDate = addSeconds(new Date(), MIN_CLOSE_SEC);
const enactmentDate = addSeconds(closingDate, MIN_ENACT_SEC);
const validationDate = addSeconds(new Date(), MIN_VALID_SEC);
const closingTimestamp = millisecondsToSeconds(closingDate.getTime());
const enactmentTimestamp = millisecondsToSeconds(enactmentDate.getTime());
const validationTimestamp = millisecondsToSeconds(validationDate.getTime());
return {
proposalSubmission: {
rationale: {
title: 'Test new asset proposal',
description: 'E2E test for proposals',
},
terms: {
newAsset: {
changes: {
name: 'USDT Coin',
symbol: 'USDT',
decimals: '18',
quantum: '1',
erc20: {
contractAddress: '0xb404c51bbc10dcbe948077f18a4b8e553d160084',
withdrawThreshold: '10',
lifetimeLimit: '10',
},
},
},
closingTimestamp,
enactmentTimestamp,
validationTimestamp,
closingTimestamp: 1695666618,
enactmentTimestamp: 1695666618,
},
},
};
+2
View File
@@ -22,6 +22,8 @@ NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
#Test configuration variables
CYPRESS_FAIRGROUND=false
+1 -4
View File
@@ -19,9 +19,6 @@ NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_VEGA_REST_URL=http://localhost:3008/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
@@ -29,4 +26,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
CYPRESS_FAIRGROUND=false
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_SUCCESSOR_MARKETS=false
+1 -4
View File
@@ -14,11 +14,8 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_REST_URL=https://api.n00.devnet1.vega.xyz/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_SUCCESSOR_MARKETS=true
+1 -3
View File
@@ -14,11 +14,9 @@ NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.vega.community/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_SUCCESSOR_MARKETS=false
+1 -3
View File
@@ -13,11 +13,9 @@ NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-mirror-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.mainnet-mirror.vega.rocks/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_SUCCESSOR_MARKETS=false
+1 -4
View File
@@ -10,11 +10,8 @@ NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_SUCCESSOR_MARKETS=true
+1 -4
View File
@@ -15,11 +15,8 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_REST_URL=https://api.n07.testnet.vega.xyz/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_SUCCESSOR_MARKETS=true
+1 -4
View File
@@ -12,11 +12,8 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_REST_URL=https://api-validators-testnet.vega.rocks/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_SUCCESSOR_MARKETS=false
@@ -1,27 +1,44 @@
import ReactMarkdown from 'react-markdown';
import { RoundedWrapper, ShowMore } from '@vegaprotocol/ui-toolkit';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
export const ProposalDescription = ({
description,
}: {
description: string;
}) => (
<section data-testid="proposal-description">
<RoundedWrapper paddingBottom={true} marginBottomLarge={true}>
<div className="p-2">
<ShowMore>
<ReactMarkdown
className="react-markdown-container"
/* Prevents HTML embedded in the description from rendering */
skipHtml={true}
/* Stops users embedding images which could be used for tracking */
disallowedElements={['img']}
linkTarget="_blank"
>
{description}
</ReactMarkdown>
</ShowMore>
</div>
</RoundedWrapper>
</section>
);
}) => {
const { t } = useTranslation();
const [showDescription, setShowDescription] = useState(false);
return (
<section data-testid="proposal-description">
<CollapsibleToggle
toggleState={showDescription}
setToggleState={setShowDescription}
dataTestId={'proposal-description-toggle'}
>
<SubHeading title={t('proposalDescription')} />
</CollapsibleToggle>
{showDescription && (
<RoundedWrapper paddingBottom={true} marginBottomLarge={true}>
<div className="p-2">
<ReactMarkdown
className="react-markdown-container"
/* Prevents HTML embedded in the description from rendering */
skipHtml={true}
/* Stops users embedding images which could be used for tracking */
disallowedElements={['img']}
linkTarget="_blank"
>
{description}
</ReactMarkdown>
</div>
</RoundedWrapper>
)}
</section>
);
};
@@ -15,6 +15,8 @@ import {
SettlementAssetInfoPanel,
} from '@vegaprotocol/markets';
import {
Accordion,
AccordionItem,
Button,
CopyWithTooltip,
Dialog,
@@ -41,9 +43,6 @@ export const useMarketDataDialogStore = create<MarketDataDialogState>(
})
);
const marketDataHeaderStyles =
'font-alpha calt text-base border-b border-vega-dark-200 mt-2 py-2';
export const ProposalMarketData = ({
marketData,
parentMarketData,
@@ -77,14 +76,6 @@ export const ProposalMarketData = ({
parentTerminationData !== undefined &&
isEqual(terminationData, parentTerminationData);
const showParentPriceMonitoringBounds =
parentMarketData?.priceMonitoringSettings?.parameters?.triggers !==
undefined &&
!isEqual(
marketData.priceMonitoringSettings?.parameters?.triggers,
parentMarketData?.priceMonitoringSettings?.parameters?.triggers
);
const getSigners = (data: DataSourceDefinition) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
@@ -117,141 +108,164 @@ export const ProposalMarketData = ({
</Button>
</div>
<div className="mb-10">
<h2 className={marketDataHeaderStyles}>{t('Key details')}</h2>
<KeyDetailsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Instrument')}</h2>
<InstrumentInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
{isEqual(
getSigners(settlementData),
getSigners(terminationData)
) ? (
<>
<h2 className={marketDataHeaderStyles}>{t('Oracle')}</h2>
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual ? undefined : parentMarketData
<Accordion>
<AccordionItem
itemId="key-details"
title={t('Key details')}
content={
<KeyDetailsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="instrument"
title={t('Instrument')}
content={
<InstrumentInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
{isEqual(
getSigners(settlementData),
getSigners(terminationData)
) ? (
<AccordionItem
itemId="oracles"
title={t('Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual
? undefined
: parentMarketData
}
/>
}
/>
</>
) : (
<>
<h2 className={marketDataHeaderStyles}>
{t('Settlement Oracle')}
</h2>
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual ? undefined : parentMarketData
}
/>
<h2 className={marketDataHeaderStyles}>
{t('Termination Oracle')}
</h2>
<OracleInfoPanel
market={marketData}
type="termination"
parentMarket={
isParentTerminationDataEqual ? undefined : parentMarketData
}
/>
</>
)}
{/*Note: successor markets will not differ in their settlement*/}
{/*assets, so no need to pass in parent market data for comparison.*/}
<h2 className={marketDataHeaderStyles}>{t('Settlement assets')}</h2>
<SettlementAssetInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Metadata')}</h2>
<MetadataInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Risk model')}</h2>
<RiskModelInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Risk parameters')}</h2>
<RiskParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Risk factors')}</h2>
<RiskFactorsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
{showParentPriceMonitoringBounds &&
(
parentMarketData?.priceMonitoringSettings?.parameters
?.triggers || []
).map((_, triggerIndex) => (
) : (
<>
<h2 className={marketDataHeaderStyles}>
{t(`Parent price monitoring bounds ${triggerIndex + 1}`)}
</h2>
<AccordionItem
itemId="settlement-oracle"
title={t('Settlement Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual
? undefined
: parentMarketData
}
/>
}
/>
<div className="text-vega-dark-300 line-through">
<AccordionItem
itemId="termination-oracle"
title={t('Termination Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="termination"
parentMarket={
isParentTerminationDataEqual
? undefined
: parentMarketData
}
/>
}
/>
</>
)}
{/*Note: successor markets will not differ in their settlement*/}
{/*assets, so no need to pass in parent market data for comparison.*/}
<AccordionItem
itemId="settlement-asset"
title={t('Settlement asset')}
content={<SettlementAssetInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="metadata"
title={t('Metadata')}
content={
<MetadataInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="risk-model"
title={t('Risk model')}
content={
<RiskModelInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="risk-parameters"
title={t('Risk parameters')}
content={
<RiskParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="risk-factors"
title={t('Risk factors')}
content={
<RiskFactorsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
{(
marketData.priceMonitoringSettings?.parameters?.triggers || []
).map((_, triggerIndex) => (
<AccordionItem
itemId={`trigger-${triggerIndex}`}
title={t(`Price monitoring bounds ${triggerIndex + 1}`)}
content={
<PriceMonitoringBoundsInfoPanel
market={parentMarketData}
market={marketData}
parentMarket={parentMarketData}
triggerIndex={triggerIndex}
/>
</div>
</>
))}
{(
marketData.priceMonitoringSettings?.parameters?.triggers || []
).map((_, triggerIndex) => (
<>
<h2 className={marketDataHeaderStyles}>
{t(`Price monitoring bounds ${triggerIndex + 1}`)}
</h2>
<PriceMonitoringBoundsInfoPanel
market={marketData}
triggerIndex={triggerIndex}
}
/>
</>
))}
<h2 className={marketDataHeaderStyles}>
{t('Liquidity monitoring parameters')}
</h2>
<LiquidityMonitoringParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>
{t('Liquidity price range')}
</h2>
<LiquidityPriceRangeInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
))}
<AccordionItem
itemId="liqudity-monitoring-parameters"
title={t('Liquidity monitoring parameters')}
content={
<LiquidityMonitoringParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="liquidity-price-range"
title={t('Liquidity price range')}
content={
<LiquidityPriceRangeInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
</Accordion>
</div>
</>
)}
+1 -1
View File
@@ -36,4 +36,4 @@ CYPRESS_VEGA_WALLET_API_TOKEN=
# Cosmic elevator flags (MUST be doubled with CYPRESS_ prefix)
NX_SUCCESSOR_MARKETS=true
CYPRESS_NX_SUCCESSOR_MARKETS=true
CYPRESS_NX_SUCCESSOR_MARKETS=true
@@ -0,0 +1,239 @@
import { aliasGQLQuery, checkSorting } from '@vegaprotocol/cypress';
import type { ProposalsListQuery } from '@vegaprotocol/proposals';
const rowSelector =
'[data-testid="tab-proposed-markets"] .ag-center-cols-container .ag-row';
const colMarketId = '[col-id="market"] [data-testid="market-code"]';
describe('markets proposed table', { tags: '@smoke' }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
});
it('can see table headers', () => {
const headers = [
'Market',
'Description',
'Settlement asset',
'State',
'Parent market',
'Voting',
'Closing date',
'Enactment date',
'',
];
cy.getByTestId('tab-proposed-markets').within(($headers) => {
cy.wrap($headers)
.get('.ag-header-cell-text')
.each(($header, i) => {
cy.wrap($header).should('have.text', headers[i]);
});
});
});
it('renders markets correctly', () => {
// 6001-MARK-049
cy.get(rowSelector).first().find(colMarketId).should('have.text', 'ETHUSD');
// 6001-MARK-050
cy.get(rowSelector)
.first()
.find('[col-id="description"]')
.should('have.text', 'ETHUSD');
// 6001-MARK-074
cy.get(rowSelector)
.first()
.find('[title="Future"]')
.should('have.text', 'Futr');
// 6001-MARK-051
cy.get(rowSelector)
.first()
.find('[col-id="asset"]')
.should('have.text', 'tDAI TEST');
// 6001-MARK-052
// 6001-MARK-053
cy.get(rowSelector)
.first()
.find('[col-id="state"]')
.should('have.text', 'Open');
// 6001-MARK-054
cy.get(rowSelector)
.first()
.find('[col-id="voting"]')
.should('have.text', '');
// 6001-MARK-056
cy.get(rowSelector)
.first()
.find('[col-id="closing-date"]')
.should('not.be.empty');
// 6001-MARK-057
cy.get(rowSelector)
.first()
.find('[col-id="enactment-date"]')
.should('not.be.empty');
});
it('can open row actions', () => {
// 6001-MARK-058
cy.get('.ag-pinned-right-cols-container')
.find('[col-id="proposal-actions"]')
.first()
.find('button')
.click();
const dropdownContent = '[data-testid="proposal-actions-content"]';
const dropdownContentItem = '[role="menuitem"]';
// 6001-MARK-059
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(0)
.find('a')
.should('have.text', 'View proposal')
.and(
'have.attr',
'href',
`${Cypress.env(
'VEGA_TOKEN_URL'
)}/proposals/e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829`
);
});
// 6001-MARK-060
it('can see proposed market link', () => {
cy.getByTestId('tab-proposed-markets')
.find('[data-testid="external-link"]')
.should('have.length', 11)
.last()
.should('have.text', 'Propose a new market')
.and(
'have.attr',
'href',
`${Cypress.env('VEGA_TOKEN_URL')}/proposals/propose/new-market`
);
});
it('proposed markets tab should be sorted properly', () => {
// 6001-MARK-062
cy.get('[data-testid="Proposed markets"]').click({ force: true });
const marketColDefault = [
'ETHUSD',
'LINKUSD',
'ETHUSD',
'ETHDAI.MF21',
'AAPL.MF21',
'BTCUSD.MF21',
'TSLA.QM21',
'AAVEDAI.MF21',
'ETHBTC.QM21',
'UNIDAI.MF21',
];
const marketColAsc = [
'AAPL.MF21',
'AAVEDAI.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'ETHDAI.MF21',
'ETHUSD',
'ETHUSD',
'LINKUSD',
'TSLA.QM21',
'UNIDAI.MF21',
];
const marketColDesc = [
'UNIDAI.MF21',
'TSLA.QM21',
'LINKUSD',
'ETHUSD',
'ETHUSD',
'ETHDAI.MF21',
'ETHBTC.QM21',
'BTCUSD.MF21',
'AAVEDAI.MF21',
'AAPL.MF21',
];
checkSorting(
'market',
marketColDefault,
marketColAsc,
marketColDesc,
' [data-testid="market-code"]'
);
const stateColDefault = [
'Open',
'Passed',
'Waiting for Node Vote',
'Open',
'Passed',
'Open',
'Passed',
'Open',
'Waiting for Node Vote',
'Open',
];
const stateColAsc = [
'Open',
'Open',
'Open',
'Open',
'Open',
'Passed',
'Passed',
'Passed',
'Waiting for Node Vote',
'Waiting for Node Vote',
];
const stateColDesc = [
'Waiting for Node Vote',
'Waiting for Node Vote',
'Passed',
'Passed',
'Passed',
'Open',
'Open',
'Open',
'Open',
'Open',
];
checkSorting('state', stateColDefault, stateColAsc, stateColDesc);
});
it('can drag and drop columns', () => {
// 6001-MARK-063
cy.get(colMarketId).realMouseDown().realMouseMove(700, 15).realMouseUp();
cy.get(colMarketId).should(($element) => {
const attributeValue = $element.attr('aria-colindex');
expect(attributeValue).not.to.equal('1');
});
});
});
describe('no markets proposed', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
const proposal: ProposalsListQuery = {};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ProposalsList', proposal);
});
cy.mockSubscription();
cy.setOnBoardingViewed();
});
it.skip('can see no markets message', () => {
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
// 6001-MARK-061
cy.getByTestId('tab-proposed-markets').should('contain.text', 'No markets');
});
});
@@ -0,0 +1,110 @@
import * as Schema from '@vegaprotocol/types';
import { testOrderSubmission } from '../support/order-validation';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { createOrder } from '../support/create-order';
describe.skip('must submit order', { tags: '@smoke' }, () => {
// 7002-SORD-039
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
beforeEach(() => {
cy.setVegaWallet();
});
it('successfully places market buy order', () => {
// 7002-SORD-010
// 0003-WTXN-012
// 0003-WTXN-003
cy.mockVegaWalletTransaction();
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_MARKET,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
postOnly: false,
reduceOnly: false,
size: '100',
};
createOrder(order);
testOrderSubmission(order);
});
it('successfully places market sell order', () => {
cy.mockVegaWalletTransaction();
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_MARKET,
side: Schema.Side.SIDE_SELL,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
size: '100',
postOnly: false,
reduceOnly: false,
};
createOrder(order);
testOrderSubmission(order);
});
it('successfully places limit buy order', () => {
// 7002-SORD-017
cy.mockVegaWalletTransaction();
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_LIMIT,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
size: '100',
postOnly: false,
reduceOnly: false,
price: '200',
};
createOrder(order);
testOrderSubmission(order, { price: '20000000' });
});
it('successfully places limit sell order', () => {
cy.mockVegaWalletTransaction();
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_LIMIT,
side: Schema.Side.SIDE_SELL,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GFN,
size: '100',
postOnly: false,
reduceOnly: false,
price: '50000',
};
createOrder(order);
testOrderSubmission(order, { price: '5000000000' });
});
it('successfully places GTT limit buy order', () => {
cy.mockVegaWalletTransaction();
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_LIMIT,
side: Schema.Side.SIDE_SELL,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
size: '100',
price: '1.00',
expiresAt: expiresAt.toISOString().substring(0, 16),
postOnly: false,
reduceOnly: false,
};
createOrder(order);
testOrderSubmission(order, {
price: '100000',
expiresAt:
new Date(order.expiresAt as string).getTime().toString() + '000000',
postOnly: false,
reduceOnly: false,
});
});
});
@@ -23,7 +23,7 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
// 0003-WTXN-001
cy.getByTestId('connect-vega-wallet'); // Not connected
cy.getByTestId(placeOrderBtn).should('exist');
cy.getByTestId('order-connect-wallet').should('exist');
cy.getByTestId('get-started-button').should('exist');
});
it('must be able to select order direction - long/short', function () {
@@ -44,7 +44,7 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
mockConnectWallet();
cy.getByTestId(toggleLimit).click();
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('connectors-list')
.find('[data-testid="connector-jsonRpc"]')
@@ -64,7 +64,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderPriceField).clear().type('1.123456');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('deal-ticket-error-message-price').should(
cy.getByTestId('deal-ticket-error-message-price-limit').should(
'have.text',
'Price accepts up to 5 decimal places'
);
@@ -87,7 +87,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.getByTestId(orderSizeField).clear().type('1.234');
// 7002-SORD-060
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('deal-ticket-error-message-size').should(
cy.getByTestId('deal-ticket-error-message-size-market').should(
'have.text',
'Size must be whole numbers for this market'
);
@@ -96,7 +96,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
it('must warn if order size is set to 0', function () {
cy.getByTestId(orderSizeField).clear().type('0');
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('deal-ticket-error-message-size').should(
cy.getByTestId('deal-ticket-error-message-size-market').should(
'have.text',
'Size cannot be lower than 1'
);
+2 -1
View File
@@ -12,7 +12,8 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
-2
View File
@@ -12,8 +12,6 @@ NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_ETH_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supply green clown session"
-2
View File
@@ -13,8 +13,6 @@ NX_VEGA_DOCS_URL=#
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
-2
View File
@@ -13,8 +13,6 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-mainnet/codfcglpplgmmlokgilfkpcjnmkbfiel
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet-mainnet
# TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.20.21-core-0.71.6
-2
View File
@@ -13,8 +13,6 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.mainnet-mirror.vega.rocks
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-mainnet/codfcglpplgmmlokgilfkpcjnmkbfiel
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet-mainnet
# TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.20.19-core-0.71.6
+1 -3
View File
@@ -13,11 +13,9 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
# NX_PRODUCT_PERPETUALS
+1 -3
View File
@@ -14,8 +14,6 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
@@ -24,4 +22,4 @@ NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
-3
View File
@@ -15,9 +15,6 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
+8
View File
@@ -1,2 +1,10 @@
import { t } from '@vegaprotocol/i18n';
export const THROTTLE_UPDATE_TIME = 500;
export const ONBOARDING_VIEWED_KEY = 'vega_onboarding_viewed';
export const MAINNET_WELCOME_HEADER = t(
'Trade cash settled futures on the fully decentralised Vega network.'
);
export const TESTNET_WELCOME_HEADER = t(
'Try out trading cash settled futures on the fully decentralised Vega network (Testnet).'
);
@@ -1,4 +1,4 @@
import { Fragment, useState } from 'react';
import { useState } from 'react';
import type {
SuccessorProposalListFieldsFragment,
NewMarketSuccessorFieldsFragment,
@@ -52,7 +52,7 @@ export const MarketSuccessorProposalBanner = ({
TOKEN_PROPOSAL.replace(':id', item.id || '')
);
return (
<Fragment key={i}>
<>
<ExternalLink href={externalLink} key={i}>
{
(item.terms?.change as NewMarketSuccessorFieldsFragment)
@@ -60,7 +60,7 @@ export const MarketSuccessorProposalBanner = ({
}
</ExternalLink>
{i < successors.length - 1 && ', '}
</Fragment>
</>
);
})}
</div>
@@ -3,7 +3,7 @@ import { Header, HeaderTitle } from '../header';
import { useParams } from 'react-router-dom';
import { MarketSelector } from '../../components/market-selector/market-selector';
import { MarketHeaderStats } from '../../client-pages/market/market-header-stats';
import { useMarket, useMarketList } from '@vegaprotocol/markets';
import { useMarket } from '@vegaprotocol/markets';
import { useState } from 'react';
export const MarketHeader = () => {
@@ -11,10 +11,6 @@ export const MarketHeader = () => {
const { data } = useMarket(marketId);
const [open, setOpen] = useState(false);
// Ensure that markets are kept cached so opening the list
// shows all markets instantly
useMarketList();
if (!data) return null;
return (
@@ -132,7 +132,6 @@ describe('MarketSelector', () => {
data: markets,
loading: false,
error: undefined,
reload: jest.fn(),
});
it('Button "All" should be selected by default', () => {
@@ -1,6 +1,6 @@
import { t } from '@vegaprotocol/i18n';
import uniqBy from 'lodash/uniqBy';
import { type MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
import {
TradingInput,
TinyScroll,
@@ -8,7 +8,7 @@ import {
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { CSSProperties } from 'react';
import { useCallback, useState, useMemo, useRef, useEffect } from 'react';
import { useCallback, useState, useMemo, useRef } from 'react';
import { FixedSizeList } from 'react-window';
import { useMarketSelectorList } from './use-market-selector-list';
import type { ProductType } from './product-selector';
@@ -44,12 +44,7 @@ export const MarketSelector = ({
assets: [],
});
const allProducts = filter.product === Product.All;
const { markets, data, loading, error, reload } =
useMarketSelectorList(filter);
useEffect(() => {
reload();
}, [reload]);
const { markets, data, loading, error } = useMarketSelectorList(filter);
return (
<div data-testid="market-selector">
@@ -15,7 +15,6 @@ export const Sort = {
Gained: 'Gained',
Lost: 'Lost',
New: 'New',
TopTraded: 'TopTraded',
} as const;
export type SortType = keyof typeof Sort;
@@ -27,7 +26,6 @@ export const SortTypeMapping: {
[Sort.Gained]: 'Top gaining',
[Sort.Lost]: 'Top losing',
[Sort.New]: 'New markets',
[Sort.TopTraded]: 'Top traded',
};
const SortIconMapping: {
@@ -37,7 +35,6 @@ const SortIconMapping: {
[Sort.Gained]: VegaIconNames.TREND_UP,
[Sort.Lost]: VegaIconNames.TREND_DOWN,
[Sort.New]: VegaIconNames.STAR,
[Sort.TopTraded]: VegaIconNames.ARROW_UP,
};
export const SortDropdown = ({
@@ -1,11 +1,7 @@
import { useMemo } from 'react';
import orderBy from 'lodash/orderBy';
import { MarketState } from '@vegaprotocol/types';
import {
calcCandleVolume,
calcTradedFactor,
useMarketList,
} from '@vegaprotocol/markets';
import { calcCandleVolume, useMarketList } from '@vegaprotocol/markets';
import { priceChangePercentage } from '@vegaprotocol/utils';
import type { Filter } from '../../components/market-selector/market-selector';
import { Sort } from './sort-dropdown';
@@ -24,7 +20,7 @@ export const useMarketSelectorList = ({
sort,
searchTerm,
}: Filter) => {
const { data, loading, error, reload } = useMarketList();
const { data, loading, error } = useMarketList();
const markets = useMemo(() => {
if (!data?.length) return [];
@@ -98,14 +94,10 @@ export const useMarketSelectorList = ({
);
}
if (sort === Sort.TopTraded) {
return orderBy(markets, [(m) => calcTradedFactor(m)], ['desc']);
}
return markets;
}, [data, product, searchTerm, assets, sort]);
return { markets, data, loading, error, reload };
return { markets, data, loading, error };
};
export const isMarketActive = (state: MarketState) => {
@@ -1,6 +1,6 @@
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { MarketSelector } from '../market-selector';
import { useMarket, useMarketList } from '@vegaprotocol/markets';
import { useMarket } from '@vegaprotocol/markets';
import { t } from '@vegaprotocol/i18n';
import { useParams } from 'react-router-dom';
import * as PopoverPrimitive from '@radix-ui/react-popover';
@@ -14,10 +14,6 @@ export const NavHeader = () => {
const { data } = useMarket(marketId);
const [open, setOpen] = useState(false);
// Ensure that markets are kept cached so opening the list
// shows all markets instantly
useMarketList();
if (!marketId) return null;
return (
@@ -36,6 +36,6 @@ export const StopOrdersContainer = () => {
const useStopOrdersStore = create<DataGridSlice>()(
persist(createDataGridSlice, {
name: 'vega_stop_orders_store',
name: 'vega_fills_store',
})
);
@@ -1,36 +1,16 @@
import { MemoryRouter } from 'react-router-dom';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { GetStarted } from './get-started';
import { render, screen } from '@testing-library/react';
let mockStep = 1;
jest.mock('./use-get-onboarding-step', () => ({
...jest.requireActual('./use-get-onboarding-step'),
useGetOnboardingStep: jest.fn(() => mockStep),
}));
describe('GetStarted', () => {
const renderComponent = (context: Partial<VegaWalletContextShape> = {}) => {
return render(
<MemoryRouter>
<VegaWalletContext.Provider value={context as VegaWalletContextShape}>
<GetStarted />
</VegaWalletContext.Provider>
</MemoryRouter>
<VegaWalletContext.Provider value={context as VegaWalletContextShape}>
<GetStarted />
</VegaWalletContext.Provider>
);
};
const checkTicks = (elements: Element[]) => {
elements.forEach((item, i) => {
if (i + 1 < mockStep) {
expect(item.querySelector('[data-testid="icon-tick"]')).toBeTruthy();
}
});
};
beforeEach(() => {
jest.clearAllMocks();
});
it('renders full get started content if not connected and no browser wallet detected', () => {
renderComponent();
@@ -40,71 +20,12 @@ describe('GetStarted', () => {
it('renders connect prompt if no pubKey but wallet installed', () => {
globalThis.window.vega = {} as Vega;
renderComponent();
expect(screen.getByTestId('get-started-banner')).toBeInTheDocument();
expect(screen.getByTestId('order-connect-wallet')).toBeInTheDocument();
globalThis.window.vega = undefined as unknown as Vega;
});
it('renders nothing if connected', () => {
mockStep = 0;
const { container } = renderComponent({ pubKey: 'my-pubkey' });
expect(container).toBeEmptyDOMElement();
});
it('steps should be ticked', () => {
const navigatorGetter: jest.SpyInstance = jest.spyOn(
window.navigator,
'userAgent',
'get'
);
navigatorGetter.mockReturnValue('Chrome');
mockStep = 1;
const { rerender, container } = renderComponent();
expect(screen.queryByTestId('icon-tick')).not.toBeInTheDocument();
expect(screen.getByTestId('get-wallet-button')).toBeInTheDocument();
mockStep = 2;
rerender(
<MemoryRouter>
<VegaWalletContext.Provider value={{} as VegaWalletContextShape}>
<GetStarted />
</VegaWalletContext.Provider>
</MemoryRouter>
);
checkTicks(screen.getAllByRole('listitem'));
expect(screen.getByRole('button', { name: 'Connect' })).toBeInTheDocument();
mockStep = 3;
rerender(
<MemoryRouter>
<VegaWalletContext.Provider value={{} as VegaWalletContextShape}>
<GetStarted />
</VegaWalletContext.Provider>
</MemoryRouter>
);
checkTicks(screen.getAllByRole('listitem'));
expect(screen.getByRole('button', { name: 'Deposit' })).toBeInTheDocument();
mockStep = 4;
rerender(
<MemoryRouter>
<VegaWalletContext.Provider value={{} as VegaWalletContextShape}>
<GetStarted />
</VegaWalletContext.Provider>
</MemoryRouter>
);
checkTicks(screen.getAllByRole('listitem'));
expect(screen.getByRole('button', { name: 'Dismiss' })).toBeInTheDocument();
mockStep = 5;
rerender(
<MemoryRouter>
<VegaWalletContext.Provider
value={{ pubKey: 'my-pubkey' } as VegaWalletContextShape}
>
<GetStarted />
</VegaWalletContext.Provider>
</MemoryRouter>
);
expect(container).toBeEmptyDOMElement();
});
});
@@ -1,96 +1,36 @@
import classNames from 'classnames';
import { t } from '@vegaprotocol/i18n';
import { ExternalLink, Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
import {
ExternalLink,
Intent,
TradingButton,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import {
GetWalletButton,
useVegaWallet,
useVegaWalletDialogStore,
isBrowserWalletInstalled,
} from '@vegaprotocol/wallet';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { useNavigate } from 'react-router-dom';
import {
OnboardingStep,
useGetOnboardingStep,
} from './use-get-onboarding-step';
import { Links, Routes } from '../../pages/client-router';
import { useGlobalStore } from '../../stores';
import { useSidebar, ViewType } from '../sidebar';
import * as constants from '../constants';
interface Props {
lead?: string;
}
const GetStartedButton = ({ step }: { step: OnboardingStep }) => {
const navigate = useNavigate();
const [, setOnboardingViewed] = useLocalStorage(
constants.ONBOARDING_VIEWED_KEY
);
const update = useGlobalStore((store) => store.update);
const marketId = useGlobalStore((store) => store.marketId);
const link = marketId ? Links[Routes.MARKET](marketId) : Links[Routes.HOME]();
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const setView = useSidebar((store) => store.setView);
let buttonText = t('Get started');
let onClickHandle = () => {
openVegaWalletDialog();
};
if (step === OnboardingStep.ONBOARDING_WALLET_STEP) {
return <GetWalletButton className="justify-between" />;
} else if (step === OnboardingStep.ONBOARDING_CONNECT_STEP) {
buttonText = t('Connect');
} else if (step === OnboardingStep.ONBOARDING_DEPOSIT_STEP) {
buttonText = t('Deposit');
onClickHandle = () => {
navigate(link);
setView({ type: ViewType.Deposit });
update({ onBoardingDismissed: true });
};
} else if (step === OnboardingStep.ONBOARDING_ORDER_STEP) {
buttonText = t('Dismiss');
onClickHandle = () => {
navigate(link);
setView({ type: ViewType.Order });
setOnboardingViewed('true');
};
}
return (
<TradingButton
onClick={onClickHandle}
size="small"
data-testid="get-started-button"
intent={Intent.Info}
>
{buttonText}
</TradingButton>
);
};
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 [onBoardingViewed] = useLocalStorage(constants.ONBOARDING_VIEWED_KEY);
const currentStep = useGetOnboardingStep();
const [, setOnboardingViewed] = useLocalStorage(
constants.ONBOARDING_VIEWED_KEY
);
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const getStartedNeeded =
onBoardingViewed !== 'true' &&
currentStep &&
currentStep < OnboardingStep.ONBOARDING_COMPLETE_STEP;
const onButtonClick = () => {
openVegaWalletDialog();
setOnboardingViewed('true');
};
const wrapperClasses = classNames(
'flex flex-col py-4 px-6 gap-4 rounded',
@@ -99,39 +39,27 @@ export const GetStarted = ({ lead }: Props) => {
{ 'mt-8': !lead }
);
if (getStartedNeeded) {
if (!pubKey && !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-none">
<Step
step={1}
text={t('Get a Vega wallet')}
complete={currentStep > OnboardingStep.ONBOARDING_WALLET_STEP}
/>
<Step
step={2}
text={t('Connect')}
complete={Boolean(
currentStep > OnboardingStep.ONBOARDING_CONNECT_STEP || pubKey
)}
/>
<Step
step={3}
text={t('Deposit funds')}
complete={currentStep > OnboardingStep.ONBOARDING_DEPOSIT_STEP}
/>
<Step
step={4}
text={t('Open a position')}
complete={currentStep > OnboardingStep.ONBOARDING_ORDER_STEP}
/>
<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>
<GetStartedButton step={currentStep} />
<TradingButton
intent={Intent.Info}
onClick={onButtonClick}
data-testid="get-started-button"
>
{t('Get started')}
</TradingButton>
</div>
{VEGA_ENV === Networks.MAINNET && (
<p className="text-sm">
@@ -156,7 +84,7 @@ export const GetStarted = ({ lead }: Props) => {
if (!pubKey) {
return (
<div className={wrapperClasses}>
<p className="mb-1 text-sm">
<p className="text-sm mb-1">
You need a{' '}
<ExternalLink href="https://vega.xyz/wallet">
Vega wallet
@@ -177,34 +105,3 @@ export const GetStarted = ({ lead }: Props) => {
return null;
};
const Step = ({
step,
text,
complete,
}: {
step: number;
text: string;
complete: boolean;
}) => {
return (
<li
className={classNames('flex', {
'text-vega-clight-200 dark:text-vega-cdark-200': complete,
})}
>
<div className="flex justify-center w-5">
{complete ? <Tick /> : <span>{step}.</span>}
</div>
<div className="ml-1">{text}</div>
</li>
);
};
const Tick = () => {
return (
<span className="relative right-[2px]">
<VegaIcon name={VegaIconNames.TICK} size={18} />
</span>
);
};
@@ -1,97 +0,0 @@
import type { ReactNode } from 'react';
import { renderHook } from '@testing-library/react';
import {
useGetOnboardingStep,
OnboardingStep,
} from './use-get-onboarding-step';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { depositsProvider } from '@vegaprotocol/deposits';
import { aggregatedAccountsDataProvider } from '@vegaprotocol/accounts';
import { ordersWithMarketProvider } from '@vegaprotocol/orders';
import { positionsDataProvider } from '@vegaprotocol/positions';
let mockData: object[] | null = [{ id: 'item-id' }];
jest.mock('@vegaprotocol/data-provider', () => ({
...jest.requireActual('@vegaprotocol/data-provider'),
useDataProvider: jest.fn(() => ({ data: mockData })),
}));
let mockContext: Partial<VegaWalletContextShape> = { pubKey: 'test-pubkey' };
describe('useGetOnboardingStep', () => {
beforeEach(() => {
jest.clearAllMocks();
mockData = [{ id: 'item-id' }];
mockContext = { pubKey: 'test-pubkey' };
globalThis.window.vega = {} as Vega;
});
const wrapper = ({ children }: { children: ReactNode }) => (
<VegaWalletContext.Provider
value={mockContext as unknown as VegaWalletContextShape}
>
{children}
</VegaWalletContext.Provider>
);
it('should return properly ONBOARDING_UNKNOWN_STEP', () => {
mockData = null;
const { result } = renderHook(() => useGetOnboardingStep(), { wrapper });
expect(result.current).toEqual(OnboardingStep.ONBOARDING_UNKNOWN_STEP);
});
it('should return properly ONBOARDING_WALLET_STEP', () => {
// @ts-ignore test only purpose
globalThis.window.vega = undefined;
const { result } = renderHook(() => useGetOnboardingStep(), { wrapper });
expect(result.current).toEqual(OnboardingStep.ONBOARDING_WALLET_STEP);
});
it('should return properly ONBOARDING_CONNECT_STEP', () => {
mockContext = { pubKey: null };
const { result } = renderHook(() => useGetOnboardingStep(), { wrapper });
expect(result.current).toEqual(OnboardingStep.ONBOARDING_CONNECT_STEP);
});
it('should return properly ONBOARDING_DEPOSIT_STEP', async () => {
(useDataProvider as jest.Mock).mockImplementation((args) => {
if (
args.dataProvider === depositsProvider ||
args.dataProvider === aggregatedAccountsDataProvider
) {
return { data: [] };
}
return { data: mockData };
});
const { result } = renderHook(() => useGetOnboardingStep(), { wrapper });
await expect(result.current).toEqual(
OnboardingStep.ONBOARDING_DEPOSIT_STEP
);
});
it('should return properly ONBOARDING_ORDER_STEP', async () => {
(useDataProvider as jest.Mock).mockImplementation((args) => {
if (
args.dataProvider === ordersWithMarketProvider ||
args.dataProvider === positionsDataProvider
) {
return { data: [] };
}
return { data: mockData };
});
const { result } = renderHook(() => useGetOnboardingStep(), { wrapper });
await expect(result.current).toEqual(OnboardingStep.ONBOARDING_ORDER_STEP);
});
it('should return properly ONBOARDING_COMPLETE_STEP', async () => {
(useDataProvider as jest.Mock).mockImplementation(() => {
return { data: mockData };
});
const { result } = renderHook(() => useGetOnboardingStep(), { wrapper });
await expect(result.current).toEqual(
OnboardingStep.ONBOARDING_COMPLETE_STEP
);
});
});
@@ -1,82 +0,0 @@
import { isBrowserWalletInstalled, useVegaWallet } from '@vegaprotocol/wallet';
import { depositsProvider } from '@vegaprotocol/deposits';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { ordersWithMarketProvider } from '@vegaprotocol/orders';
import * as Types from '@vegaprotocol/types';
import { aggregatedAccountsDataProvider } from '@vegaprotocol/accounts';
import { positionsDataProvider } from '@vegaprotocol/positions';
import { useGlobalStore } from '../../stores';
export enum OnboardingStep {
ONBOARDING_UNKNOWN_STEP,
ONBOARDING_WALLET_STEP,
ONBOARDING_CONNECT_STEP,
ONBOARDING_DEPOSIT_STEP,
ONBOARDING_ORDER_STEP,
ONBOARDING_COMPLETE_STEP,
}
export const useGetOnboardingStep = () => {
const connecting = useGlobalStore((store) => store.eagerConnecting);
const { pubKey = '', pubKeys } = useVegaWallet();
const { data: depositsData } = useDataProvider({
dataProvider: depositsProvider,
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const { data: collateralData } = useDataProvider({
dataProvider: aggregatedAccountsDataProvider,
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const collaterals = Boolean(collateralData?.length);
const deposits =
depositsData?.some(
(item) => item.status === Types.DepositStatus.STATUS_FINALIZED
) || false;
const { data: ordersData } = useDataProvider({
dataProvider: ordersWithMarketProvider,
variables: {
partyId: pubKey || '',
pagination: {
first: 1,
},
},
skip: !pubKey,
});
const orders = Boolean(ordersData?.length);
const partyIds = pubKeys?.map((item) => item.publicKey) || [];
const { data: positionsData } = useDataProvider({
dataProvider: positionsDataProvider,
variables: {
partyIds,
},
skip: !partyIds?.length,
});
const positions = Boolean(positionsData?.length);
const isLoading = Boolean(
(connecting || pubKey) &&
(depositsData === null ||
ordersData === null ||
collateralData === null ||
positionsData === null)
);
if (isLoading) {
return OnboardingStep.ONBOARDING_UNKNOWN_STEP;
}
if (!isBrowserWalletInstalled()) {
return OnboardingStep.ONBOARDING_WALLET_STEP;
}
if (!pubKey) {
return OnboardingStep.ONBOARDING_CONNECT_STEP;
}
if (!deposits && !collaterals) {
return OnboardingStep.ONBOARDING_DEPOSIT_STEP;
}
if (!orders && !positions) {
return OnboardingStep.ONBOARDING_ORDER_STEP;
}
return OnboardingStep.ONBOARDING_COMPLETE_STEP;
};
@@ -3,19 +3,21 @@ 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';
import { useGlobalStore } from '../../stores';
export const WelcomeDialogContent = () => {
const { VEGA_ENV } = useEnvironment();
const update = useGlobalStore((store) => store.update);
const [, setOnboardingViewed] = useLocalStorage(
constants.ONBOARDING_VIEWED_KEY
);
const navigate = useNavigate();
const browseMarkets = () => {
const link = Links[Routes.MARKETS]();
navigate(link);
update({ onBoardingDismissed: true });
setOnboardingViewed('true');
};
const lead =
VEGA_ENV === Networks.MAINNET
@@ -55,7 +57,7 @@ export const WelcomeDialogContent = () => {
{t('Browse the markets')}
</TradingButton>
</div>
<div className="sm:w-1/2 flex grow">
<div className="sm:w-1/2">
<GetStarted lead={lead} />
</div>
</div>
@@ -2,38 +2,31 @@ import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useEnvironment } from '@vegaprotocol/environment';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { useEnvironment } from '@vegaprotocol/environment';
import { isBrowserWalletInstalled } from '@vegaprotocol/wallet';
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';
import {
useGetOnboardingStep,
OnboardingStep,
} from './use-get-onboarding-step';
import * as constants from '../constants';
export const WelcomeDialog = () => {
const { VEGA_ENV } = useEnvironment();
const [onBoardingViewed] = useLocalStorage(constants.ONBOARDING_VIEWED_KEY);
const update = useGlobalStore((store) => store.update);
const dismissed = useGlobalStore((store) => store.onBoardingDismissed);
const currentStep = useGetOnboardingStep();
const [onBoardingViewed, setOnboardingViewed] = useLocalStorage(
constants.ONBOARDING_VIEWED_KEY
);
const navigate = useNavigate();
const isOnboardingDialogNeeded =
onBoardingViewed !== 'true' &&
currentStep &&
currentStep < OnboardingStep.ONBOARDING_COMPLETE_STEP &&
!dismissed;
onBoardingViewed !== 'true' && !isBrowserWalletInstalled() && !getConfig();
const marketId = useGlobalStore((store) => store.marketId);
const onClose = () => {
setOnboardingViewed('true');
const link = marketId
? Links[Routes.MARKET](marketId)
: Links[Routes.HOME]();
navigate(link);
update({ onBoardingDismissed: true });
};
const title = (
<span className="font-alpha calt" data-testid="welcome-title">
+3 -7
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import Head from 'next/head';
import type { AppProps } from 'next/app';
import { t } from '@vegaprotocol/i18n';
@@ -23,7 +23,7 @@ import {
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import './styles.css';
import { useGlobalStore, usePageTitleStore } from '../stores';
import { usePageTitleStore } from '../stores';
import DialogsContainer from './dialogs-container';
import ToastsManager from './toasts-manager';
import {
@@ -170,8 +170,7 @@ const PartyData = () => {
const MaybeConnectEagerly = () => {
const { VEGA_ENV, SENTRY_DSN } = useEnvironment();
const update = useGlobalStore((store) => store.update);
const eagerConnecting = useVegaEagerConnect(Connectors);
useVegaEagerConnect(Connectors);
const [isTelemetryApproved] = useTelemetryApproval();
useEthereumEagerConnect(
isTelemetryApproved ? { dsn: SENTRY_DSN, env: VEGA_ENV } : {}
@@ -183,8 +182,5 @@ const MaybeConnectEagerly = () => {
if (query && !pubKey) {
connect(Connectors['view']);
}
useEffect(() => {
update({ eagerConnecting });
}, [update, eagerConnecting]);
return null;
};
+1 -17
View File
@@ -15,10 +15,6 @@ body,
@apply h-full;
}
.font-mono {
@apply tracking-tighter;
}
.text-default {
@apply text-vega-clight-50 dark:text-vega-cdark-50;
}
@@ -151,7 +147,7 @@ html [data-theme='dark'] {
}
.vega-ag-grid .ag-header-row {
@apply font-normal font-alpha;
@apply font-alpha font-normal;
}
/* Light variables */
@@ -213,15 +209,3 @@ html [data-theme='dark'] {
box-shadow: inset 0 0 6px rgb(0 0 0 / 30%);
background-color: #999;
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type='number'] {
-moz-appearance: textfield;
}
-4
View File
@@ -4,8 +4,6 @@ import produce from 'immer';
interface GlobalStore {
marketId: string | null;
onBoardingDismissed: boolean;
eagerConnecting: boolean;
update: (store: Partial<Omit<GlobalStore, 'update'>>) => void;
}
@@ -16,8 +14,6 @@ interface PageTitleStore {
export const useGlobalStore = create<GlobalStore>()((set) => ({
marketId: LocalStorage.getItem('marketId') || null,
onBoardingDismissed: false,
eagerConnecting: false,
update: (newState) => {
set(
produce((state: GlobalStore) => {
@@ -153,10 +153,8 @@ export function waitForProposal(id: string): Promise<{ id: string }> {
try {
const res = await getProposal(id);
if (
(res.proposal !== null &&
res.proposal.state === Schema.ProposalState.STATE_OPEN) ||
res.proposal.state ===
Schema.ProposalState.STATE_WAITING_FOR_NODE_VOTE
res.proposal !== null &&
res.proposal.state === Schema.ProposalState.STATE_OPEN
) {
clearInterval(interval);
resolve(res.proposal);
@@ -0,0 +1,39 @@
import type { Control } from 'react-hook-form';
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
import * as Schema from '@vegaprotocol/types';
import type { OrderFormValues } from '../../hooks/use-form-values';
export interface DealTicketAmountProps {
control: Control<OrderFormValues>;
type: Schema.OrderType;
marketData: StaticMarketData;
marketPrice?: string;
market: Market;
sizeError?: string;
priceError?: string;
}
export const DealTicketAmount = ({
type,
marketData,
marketPrice,
...props
}: DealTicketAmountProps) => {
switch (type) {
case Schema.OrderType.TYPE_MARKET:
return (
<DealTicketMarketAmount
{...props}
marketData={marketData}
marketPrice={marketPrice}
/>
);
case Schema.OrderType.TYPE_LIMIT:
return <DealTicketLimitAmount {...props} />;
default: {
throw new Error('Invalid ticket type ' + type);
}
}
};
@@ -0,0 +1,120 @@
import {
TradingFormGroup,
TradingInput,
TradingInputError,
} from '@vegaprotocol/ui-toolkit';
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { DealTicketAmountProps } from './deal-ticket-amount';
import { Controller } from 'react-hook-form';
export type DealTicketLimitAmountProps = Omit<
DealTicketAmountProps,
'marketData' | 'type'
>;
export const DealTicketLimitAmount = ({
control,
market,
sizeError,
priceError,
}: DealTicketLimitAmountProps) => {
const priceStep = toDecimal(market?.decimalPlaces);
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const quoteName = market.tradableInstrument.instrument.product.quoteName;
const renderError = () => {
if (sizeError) {
return (
<TradingInputError testId="deal-ticket-error-message-size-limit">
{sizeError}
</TradingInputError>
);
}
if (priceError) {
return (
<TradingInputError testId="deal-ticket-error-message-price-limit">
{priceError}
</TradingInputError>
);
}
return null;
};
return (
<div className="mb-2">
<div className="flex items-start gap-4">
<div className="flex-1">
<TradingFormGroup
label={t('Size')}
labelFor="input-order-size-limit"
className="!mb-0"
>
<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, fieldState }) => (
<TradingInput
id="input-order-size-limit"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
data-testid="order-size"
onWheel={(e) => e.currentTarget.blur()}
hasError={!!fieldState.error}
{...field}
/>
)}
/>
</TradingFormGroup>
</div>
<div className="pt-5 leading-10">@</div>
<div className="flex-1">
<TradingFormGroup
labelFor="input-price-quote"
label={t(`Price (${quoteName})`)}
labelAlign="right"
className="!mb-0"
>
<Controller
name="price"
control={control}
rules={{
required: t('You need provide a price'),
min: {
value: priceStep,
message: t('Price cannot be lower than ' + priceStep),
},
validate: validateAmount(priceStep, 'Price'),
}}
render={({ field, fieldState }) => (
<TradingInput
id="input-price-quote"
className="w-full"
type="number"
step={priceStep}
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
hasError={!!fieldState.error}
{...field}
/>
)}
/>
</TradingFormGroup>
</div>
</div>
{renderError()}
</div>
);
};
@@ -0,0 +1,102 @@
import {
addDecimalsFormatNumber,
toDecimal,
validateAmount,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
TradingInput,
TradingInputError,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { isMarketInAuction } from '@vegaprotocol/markets';
import type { DealTicketAmountProps } from './deal-ticket-amount';
import { Controller } from 'react-hook-form';
import classNames from 'classnames';
export type DealTicketMarketAmountProps = Omit<DealTicketAmountProps, 'type'>;
export const DealTicketMarketAmount = ({
control,
market,
marketData,
marketPrice,
sizeError,
}: DealTicketMarketAmountProps) => {
const quoteName = market.tradableInstrument.instrument.product.quoteName;
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const price = marketPrice;
const priceFormatted = price
? addDecimalsFormatNumber(price, market.decimalPlaces)
: undefined;
const inAuction = isMarketInAuction(marketData.marketTradingMode);
return (
<div className="mb-2">
<div className="flex items-start gap-4">
<div className="flex-1">
<div className="mb-2 text-xs">{t('Size')}</div>
<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, fieldState }) => (
<TradingInput
id="input-order-size-market"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
hasError={!!fieldState.error}
{...field}
/>
)}
/>
</div>
<div className="pt-5 leading-10">@</div>
<div className="flex-1 text-sm text-right">
{inAuction && (
<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 className="mb-2">{t(`Indicative price`)}</div>
</Tooltip>
)}
<div
data-testid="last-price"
className={classNames('leading-10', { 'pt-5': !inAuction })}
>
{priceFormatted && quoteName ? (
<>
~{priceFormatted} {quoteName}
</>
) : (
'-'
)}
</div>
</div>
</div>
{sizeError && (
<TradingInputError
intent="danger"
testId="deal-ticket-error-message-size-market"
>
{sizeError}
</TradingInputError>
)}
</div>
);
};
@@ -72,7 +72,6 @@ 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 triggerPriceWarningMessage = 'stop-order-warning-message-trigger-price';
const triggerTrailingPercentOffsetErrorMessage =
'stop-order-error-message-trigger-trailing-percent-offset';
@@ -115,6 +114,14 @@ describe('StopOrder', () => {
});
});
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,
@@ -196,8 +203,8 @@ describe('StopOrder', () => {
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.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();
@@ -242,17 +249,10 @@ describe('StopOrder', () => {
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.001');
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// clear and fill using value causing immediate trigger
// 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();
expect(
screen.queryByTestId(triggerPriceWarningMessage)
).toBeInTheDocument();
// change to correct value
await userEvent.type(screen.getByTestId(triggerPriceInput), '2');
expect(screen.queryByTestId(triggerPriceWarningMessage)).toBeNull();
});
it('validates trigger trailing percentage offset field', async () => {
@@ -2,22 +2,21 @@ import { useRef, useCallback, useEffect } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
import {
formatForInput,
formatNumber,
removeDecimal,
toDecimal,
validateAmount,
} from '@vegaprotocol/utils';
import type { Control, UseFormWatch } from 'react-hook-form';
import { useForm, Controller, useController } from 'react-hook-form';
import * as Schema from '@vegaprotocol/types';
import {
TradingRadio as Radio,
TradingRadioGroup as RadioGroup,
TradingInput as Input,
TradingCheckbox as Checkbox,
TradingFormGroup as FormGroup,
TradingInputError as InputError,
TradingSelect as Select,
TradingRadio,
TradingRadioGroup,
TradingInput,
TradingCheckbox,
TradingFormGroup,
TradingInputError,
TradingSelect,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
@@ -50,8 +49,6 @@ export interface StopOrderProps {
submit: (order: StopOrdersSubmission) => void;
}
const trailingPercentOffsetStep = '0.1';
const getDefaultValues = (
type: Schema.OrderType,
storedValues?: Partial<StopOrderFormValues>
@@ -65,426 +62,9 @@ const getDefaultValues = (
expire: false,
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT,
size: '0',
oco: false,
ocoType: type,
ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
ocoTriggerType: 'price',
ocoSize: '0',
...storedValues,
});
const Trigger = ({
control,
watch,
priceStep,
assetSymbol,
oco,
marketPrice,
decimalPlaces,
}: {
control: Control<StopOrderFormValues>;
watch: UseFormWatch<StopOrderFormValues>;
priceStep: string;
assetSymbol: string;
oco?: boolean;
marketPrice?: string | null;
decimalPlaces: number;
}) => {
const triggerType = watch(oco ? 'ocoTriggerType' : 'triggerType');
const triggerDirection = watch('triggerDirection');
const isPriceTrigger = triggerType === 'price';
return (
<FormGroup label={t('Trigger')} labelFor="">
<Controller
name="triggerDirection"
control={control}
render={({ field }) => {
const { value, onChange } = field;
return (
<RadioGroup
name="triggerDirection"
onChange={onChange}
value={value}
orientation="horizontal"
className="mb-2"
>
<Radio
value={
oco
? Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_FALLS_BELOW
: Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_RISES_ABOVE
}
id={`triggerDirection-risesAbove${oco ? '-oco' : ''}`}
label={'Rises above'}
/>
<Radio
value={
!oco
? Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_FALLS_BELOW
: Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_RISES_ABOVE
}
id={`triggerDirection-fallsBelow${oco ? '-oco' : ''}`}
label={'Falls below'}
/>
</RadioGroup>
);
}}
/>
{isPriceTrigger && (
<div className="mb-2">
<Controller
name={oco ? 'ocoTriggerPrice' : '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, fieldState }) => {
const { value, ...props } = field;
let triggerWarning = false;
if (marketPrice && value) {
const condition =
(!oco &&
triggerDirection ===
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_RISES_ABOVE) ||
(oco &&
triggerDirection ===
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_FALLS_BELOW)
? '>'
: '<';
const diff =
BigInt(marketPrice) -
BigInt(removeDecimal(value, decimalPlaces));
if (
(condition === '>' && diff > 0) ||
(condition === '<' && diff < 0)
) {
triggerWarning = true;
}
}
return (
<>
<div className="mb-2">
<Input
data-testid={`triggerPrice${oco ? '-oco' : ''}`}
type="number"
step={priceStep}
appendElement={assetSymbol}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
</div>
{fieldState.error && (
<InputError
testId={`stop-order-error-message-trigger-price${
oco ? '-oco' : ''
}`}
>
{fieldState.error.message}
</InputError>
)}
{!fieldState.error && triggerWarning && (
<InputError
intent="warning"
testId={`stop-order-warning-message-trigger-price${
oco ? '-oco' : ''
}`}
>
{t('Stop order will be triggered immediately')}
</InputError>
)}
</>
);
}}
/>
</div>
)}
{!isPriceTrigger && (
<div className="mb-2">
<Controller
name={
oco
? 'ocoTriggerTrailingPercentOffset'
: '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, fieldState }) => {
const { value, ...props } = field;
return (
<>
<div className="mb-2">
<Input
type="number"
step={trailingPercentOffsetStep}
appendElement="%"
data-testid={`triggerTrailingPercentOffset${
oco ? '-oco' : ''
}`}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
</div>
{fieldState.error && (
<InputError
testId={`stop-order-error-message-trigger-trailing-percent-offset${
oco ? '-oco' : ''
}`}
>
{fieldState.error.message}
</InputError>
)}
</>
);
}}
/>
</div>
)}
<Controller
name={oco ? 'ocoTriggerType' : 'triggerType'}
control={control}
rules={{
deps: oco
? ['ocoTriggerTrailingPercentOffset', 'ocoTriggerPrice']
: ['triggerTrailingPercentOffset', 'triggerPrice'],
}}
render={({ field }) => {
const { onChange, value } = field;
return (
<RadioGroup
onChange={onChange}
value={value}
orientation="horizontal"
>
<Radio
value="price"
id={`triggerType-price${oco ? '-oco' : ''}`}
label={'Price'}
/>
<Radio
value="trailingPercentOffset"
id={`triggerType-trailingPercentOffset${oco ? '-oco' : ''}`}
label={'Trailing Percent Offset'}
/>
</RadioGroup>
);
}}
/>
</FormGroup>
);
};
const Size = ({
control,
sizeStep,
oco,
}: {
control: Control<StopOrderFormValues>;
sizeStep: string;
oco?: boolean;
}) => {
return (
<Controller
name={oco ? 'ocoSize' : '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, fieldState }) => {
const { value, ...props } = field;
const id = `order-size${oco ? '-oco' : ''}`;
return (
<div className="mb-4">
<FormGroup labelFor={id} label={t(`Size`)} compact>
<Input
id={id}
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid={id}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
</FormGroup>
{fieldState.error && (
<InputError
testId={`stop-order-error-message-size${oco ? '-oco' : ''}`}
>
{fieldState.error.message}
</InputError>
)}
</div>
);
}}
/>
);
};
const Price = ({
control,
watch,
priceStep,
quoteName,
oco,
}: {
control: Control<StopOrderFormValues>;
watch: UseFormWatch<StopOrderFormValues>;
priceStep: string;
quoteName: string;
oco?: boolean;
}) => {
if (watch(oco ? 'ocoType' : 'type') === Schema.OrderType.TYPE_MARKET) {
return null;
}
return (
<Controller
name={oco ? 'ocoPrice' : '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, fieldState }) => {
const { value, ...props } = field;
const id = `order-price${oco ? '-oco' : ''}`;
return (
<div className="mb-4">
<FormGroup
labelFor={id}
label={t(`Price (${quoteName})`)}
compact={true}
>
<Input
id={id}
className="w-full"
type="number"
step={priceStep}
data-testid={id}
onWheel={(e) => e.currentTarget.blur()}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
</FormGroup>
{fieldState.error && (
<InputError
testId={`stop-order-error-message-price${oco ? '-oco' : ''}`}
>
{fieldState.error.message}
</InputError>
)}
</div>
);
}}
/>
);
};
const TimeInForce = ({
control,
oco,
}: {
control: Control<StopOrderFormValues>;
oco?: boolean;
}) => (
<Controller
name="timeInForce"
control={control}
render={({ field, fieldState }) => {
const id = `select-time-in-force${oco ? '-oco' : ''}`;
return (
<div className="mb-2">
<FormGroup label={t('Time in force')} labelFor={id} compact={true}>
<Select
id={id}
className="w-full"
data-testid="order-tif"
hasError={!!fieldState.error}
{...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>
{fieldState.error && (
<InputError testId={`stop-error-message-tif${oco ? '-oco' : ''}`}>
{fieldState.error.message}
</InputError>
)}
</div>
);
}}
/>
);
const ReduceOnly = () => (
<Checkbox
name="reduce-only"
checked={true}
disabled={true}
label={
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
<>{t('Reduce only')}</>
</Tooltip>
}
/>
);
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
const { pubKey, isReadOnly } = useVegaWallet();
const setType = useDealTicketFormValues((state) => state.setType);
@@ -527,8 +107,6 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
const timeInForce = watch('timeInForce');
const rawPrice = watch('price');
const rawSize = watch('size');
const oco = watch('oco');
const expiresAt = watch('expiresAt');
useEffect(() => {
const size = storedFormValues?.[dealTicketType]?.size;
@@ -577,6 +155,12 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const priceStep = toDecimal(market?.decimalPlaces);
const trailingPercentOffsetStep = '0.1';
const priceFormatted =
isPriceTrigger && triggerPrice
? formatNumber(triggerPrice, market.decimalPlaces)
: undefined;
useController({
name: 'type',
@@ -603,9 +187,9 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
}}
/>
{errors.type && (
<InputError testId="stop-order-error-message-type">
<TradingInputError testId="stop-order-error-message-type">
{errors.type.message}
</InputError>
</TradingInputError>
)}
<Controller
@@ -615,128 +199,303 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
<SideSelector value={field.value} onValueChange={field.onChange} />
)}
/>
<Trigger
control={control}
watch={watch}
priceStep={priceStep}
assetSymbol={asset.symbol}
marketPrice={marketPrice}
decimalPlaces={market.decimalPlaces}
/>
<hr className="mb-4 border-vega-clight-500 dark:border-vega-cdark-500" />
<Price
control={control}
watch={watch}
priceStep={priceStep}
quoteName={quoteName}
/>
<Size control={control} sizeStep={sizeStep} />
<TimeInForce control={control} />
<div className="flex gap-2 pb-3 justify-end">
<ReduceOnly />
</div>
<hr className="mb-4 border-vega-clight-500 dark:border-vega-cdark-500" />
<div className="flex gap-2 pb-2 justify-between">
<TradingFormGroup label={t('Trigger')} compact={true} labelFor="">
<Controller
name="oco"
name="triggerDirection"
control={control}
render={({ field }) => {
const { onChange, value } = field;
return (
<Checkbox
onCheckedChange={(state) => {
onChange(state);
setValue(
'expiryStrategy',
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
);
}}
checked={value}
name="oco"
label={
<Tooltip
description={<span>{t('One cancels another')}</span>}
>
<>{t('OCO')}</>
</Tooltip>
}
/>
<TradingRadioGroup
name="triggerDirection"
onChange={onChange}
value={value}
orientation="horizontal"
className="mb-2"
>
<TradingRadio
value={
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_RISES_ABOVE
}
id="triggerDirection-risesAbove"
label={'Rises above'}
/>
<TradingRadio
value={
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_FALLS_BELOW
}
id="triggerDirection-fallsBelow"
label={'Falls below'}
/>
</TradingRadioGroup>
);
}}
/>
</div>
{oco && (
<>
<FormGroup label={t('Type')} labelFor="">
{isPriceTrigger && (
<div className="mb-2">
<Controller
name={`ocoType`}
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 { onChange, value } = field;
render={({ field, fieldState }) => {
const { value, ...props } = field;
return (
<RadioGroup
onChange={onChange}
value={value}
orientation="horizontal"
>
<Radio
value={Schema.OrderType.TYPE_MARKET}
id={`ocoTypeMarket`}
label={'Market'}
<div className="mb-2">
<TradingInput
data-testid="triggerPrice"
type="number"
step={priceStep}
appendElement={asset.symbol}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
<Radio
value={Schema.OrderType.TYPE_LIMIT}
id={`ocoTypeLimit`}
label={'Limit'}
/>
</RadioGroup>
</div>
);
}}
/>
</FormGroup>
<Trigger
control={control}
watch={watch}
priceStep={priceStep}
assetSymbol={asset.symbol}
marketPrice={marketPrice}
decimalPlaces={market.decimalPlaces}
oco
/>
<hr className="mb-2 border-vega-clight-500 dark:border-vega-cdark-500" />
<Price
control={control}
watch={watch}
priceStep={priceStep}
quoteName={quoteName}
oco
/>
<Size control={control} sizeStep={sizeStep} oco />
<TimeInForce control={control} oco />
<div className="flex gap-2 mb-2 justify-end">
<ReduceOnly />
{errors.triggerPrice && (
<TradingInputError testId="stop-order-error-message-trigger-price">
{errors.triggerPrice.message}
</TradingInputError>
)}
</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, fieldState }) => {
const { value, ...props } = field;
return (
<div className="mb-2">
<TradingInput
type="number"
step={trailingPercentOffsetStep}
appendElement="%"
data-testid="triggerTrailingPercentOffset"
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
</div>
);
}}
/>
{errors.triggerTrailingPercentOffset && (
<TradingInputError testId="stop-order-error-message-trigger-trailing-percent-offset">
{errors.triggerTrailingPercentOffset.message}
</TradingInputError>
)}
</div>
)}
<Controller
name="triggerType"
control={control}
rules={{ deps: ['triggerTrailingPercentOffset', 'triggerPrice'] }}
render={({ field }) => {
const { onChange, value } = field;
return (
<TradingRadioGroup
onChange={onChange}
value={value}
orientation="horizontal"
>
<TradingRadio
value="price"
id="triggerType-price"
label={'Price'}
/>
<TradingRadio
value="trailingPercentOffset"
id="triggerType-trailingPercentOffset"
label={'Trailing Percent Offset'}
/>
</TradingRadioGroup>
);
}}
/>
</TradingFormGroup>
<div className="mb-2">
<div className="flex items-start gap-4">
<TradingFormGroup
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, fieldState }) => {
const { value, ...props } = field;
return (
<TradingInput
id="order-size"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
);
}}
/>
</TradingFormGroup>
<div className="pt-5 leading-10">@</div>
<div className="flex-1">
{type === Schema.OrderType.TYPE_LIMIT ? (
<TradingFormGroup
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, fieldState }) => {
const { value, ...props } = field;
return (
<TradingInput
id="input-price-quote"
className="w-full"
type="number"
step={priceStep}
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
);
}}
/>
</TradingFormGroup>
) : (
<div
className="text-sm text-right pt-5 leading-10"
data-testid="price"
>
{priceFormatted && quoteName
? `~${priceFormatted} ${quoteName}`
: '-'}
</div>
)}
</div>
</div>
{errors.size && (
<TradingInputError testId="stop-order-error-message-size">
{errors.size.message}
</TradingInputError>
)}
{!errors.size &&
errors.price &&
type === Schema.OrderType.TYPE_LIMIT && (
<TradingInputError testId="stop-order-error-message-price">
{errors.price.message}
</TradingInputError>
)}
</div>
<div className="mb-2">
<TradingFormGroup
label={t('Time in force')}
labelFor="select-time-in-force"
compact={true}
>
<Controller
name="timeInForce"
control={control}
render={({ field, fieldState }) => (
<TradingSelect
id="select-time-in-force"
className="w-full"
data-testid="order-tif"
hasError={!!fieldState.error}
{...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>
</TradingSelect>
)}
/>
</TradingFormGroup>
{errors.timeInForce && (
<TradingInputError testId="stop-error-message-tif">
{errors.timeInForce.message}
</TradingInputError>
)}
</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={(value) => {
if (
value &&
(!expiresAt || new Date(expiresAt).getTime() < Date.now())
) {
setValue('expiresAt', formatForInput(new Date()), {
shouldValidate: true,
});
}
onCheckedChange(value);
}}
<TradingCheckbox
onCheckedChange={onCheckedChange}
checked={value}
name="expire"
label={t('Expire')}
@@ -744,47 +503,54 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
);
}}
/>
<TradingCheckbox
name="reduce-only"
checked={true}
disabled={true}
label={
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
<>{t('Reduce only')}</>
</Tooltip>
}
/>
</div>
{expire && (
<>
<FormGroup label={t('Strategy')} labelFor="expiryStrategy">
<TradingFormGroup
label={t('Strategy')}
labelFor="expiryStrategy"
compact={true}
>
<Controller
name="expiryStrategy"
control={control}
render={({ field }) => {
const { onChange, value } = field;
return (
<RadioGroup
onChange={onChange}
value={value}
orientation="horizontal"
>
<Radio
disabled={oco}
<TradingRadioGroup orientation="horizontal" {...field}>
<TradingRadio
value={
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
}
id="expiryStrategy-submit"
label={'Submit'}
/>
<Radio
<TradingRadio
value={
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
}
id="expiryStrategy-cancel"
label={'Cancel'}
/>
</RadioGroup>
</TradingRadioGroup>
);
}}
/>
</FormGroup>
<div className="mb-4">
</TradingFormGroup>
<div className="mb-2">
<Controller
name="expiresAt"
control={control}
rules={{
required: t('You need provide a expiry time/date'),
validate: validateExpiration,
}}
render={({ field }) => {
@@ -7,6 +7,7 @@ import { DealTicket } from './deal-ticket';
import * as Schema from '@vegaprotocol/types';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { addDecimal } from '@vegaprotocol/utils';
import type { OrdersQuery } from '@vegaprotocol/orders';
import {
DealTicketType,
@@ -134,6 +135,20 @@ describe('DealTicket', () => {
);
});
it('should display last price for market type order', () => {
render(generateJsx());
act(() => {
screen.getByTestId('order-type-Market').click();
});
// Assert last price is shown
expect(screen.getByTestId('last-price')).toHaveTextContent(
// eslint-disable-next-line
`~${addDecimal(marketPrice, market.decimalPlaces)} ${
market.tradableInstrument.instrument.product.quoteName
}`
);
});
it('should use local storage state for initial values', () => {
const expectedOrder = {
marketId: market.id,
@@ -3,6 +3,7 @@ import * as Schema from '@vegaprotocol/types';
import type { FormEventHandler } from 'react';
import { memo, useCallback, useEffect, useRef, useMemo } from 'react';
import { Controller, useController, useForm } from 'react-hook-form';
import { DealTicketAmount } from './deal-ticket-amount';
import { DealTicketButton } from './deal-ticket-button';
import {
DealTicketFeeDetails,
@@ -16,10 +17,8 @@ import type { OrderSubmission } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { mapFormValuesToOrderSubmission } from '../../utils/map-form-values-to-submission';
import {
TradingInput as Input,
TradingCheckbox as Checkbox,
TradingFormGroup as FormGroup,
TradingInputError as InputError,
TradingCheckbox,
TradingInputError,
Intent,
Notification,
Tooltip,
@@ -29,13 +28,7 @@ import {
useEstimatePositionQuery,
useOpenVolume,
} from '@vegaprotocol/positions';
import {
toBigNum,
removeDecimal,
validateAmount,
toDecimal,
formatForInput,
} from '@vegaprotocol/utils';
import { toBigNum, removeDecimal } from '@vegaprotocol/utils';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import { getDerivedPrice } from '@vegaprotocol/markets';
import type { OrderInfo } from '@vegaprotocol/types';
@@ -175,7 +168,6 @@ export const DealTicket = ({
const rawPrice = watch('price');
const iceberg = watch('iceberg');
const peakSize = watch('peakSize');
const expiresAt = watch('expiresAt');
useEffect(() => {
const size = storedFormValues?.[dealTicketType]?.size;
@@ -340,10 +332,6 @@ export const DealTicket = ({
},
});
const priceStep = toDecimal(market?.decimalPlaces);
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const quoteName = market.tradableInstrument.instrument.product.quoteName;
return (
<form
onSubmit={
@@ -378,82 +366,15 @@ export const DealTicket = ({
<SideSelector value={field.value} onValueChange={field.onChange} />
)}
/>
<Controller
name="size"
<DealTicketAmount
type={type}
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, fieldState }) => (
<div className="mb-4">
<FormGroup
label={t('Size')}
labelFor="input-order-size-limit"
compact
>
<Input
id="input-order-size-limit"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
data-testid="order-size"
onWheel={(e) => e.currentTarget.blur()}
{...field}
/>
</FormGroup>
{fieldState.error && (
<InputError testId="deal-ticket-error-message-size">
{fieldState.error.message}
</InputError>
)}
</div>
)}
market={market}
marketData={marketData}
marketPrice={marketPrice || undefined}
sizeError={errors.size?.message}
priceError={errors.price?.message}
/>
{type === Schema.OrderType.TYPE_LIMIT && (
<Controller
name="price"
control={control}
rules={{
required: t('You need provide a price'),
min: {
value: priceStep,
message: t('Price cannot be lower than ' + priceStep),
},
validate: validateAmount(priceStep, 'Price'),
}}
render={({ field, fieldState }) => (
<div className="mb-4">
<FormGroup
labelFor="input-price-quote"
label={t(`Price (${quoteName})`)}
compact
>
<Input
id="input-price-quote"
className="w-full"
type="number"
step={priceStep}
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
{...field}
/>
</FormGroup>
{fieldState.error && (
<InputError testId="deal-ticket-error-message-price">
{fieldState.error.message}
</InputError>
)}
</div>
)}
/>
)}
<Controller
name="timeInForce"
control={control}
@@ -467,17 +388,7 @@ export const DealTicket = ({
<TimeInForceSelector
value={field.value}
orderType={type}
onSelect={(value) => {
if (
value === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT &&
(!expiresAt || new Date(expiresAt).getTime() < Date.now())
) {
setValue('expiresAt', formatForInput(new Date()), {
shouldValidate: true,
});
}
field.onChange(value);
}}
onSelect={field.onChange}
market={market}
marketData={marketData}
errorMessage={errors.timeInForce?.message}
@@ -490,7 +401,6 @@ export const DealTicket = ({
name="expiresAt"
control={control}
rules={{
required: t('You need provide a expiry time/date'),
validate: validateExpiration,
}}
render={({ field }) => (
@@ -507,7 +417,7 @@ export const DealTicket = ({
name="postOnly"
control={control}
render={({ field }) => (
<Checkbox
<TradingCheckbox
name="post-only"
checked={!disablePostOnlyCheckbox && field.value}
disabled={disablePostOnlyCheckbox}
@@ -539,7 +449,7 @@ export const DealTicket = ({
name="reduceOnly"
control={control}
render={({ field }) => (
<Checkbox
<TradingCheckbox
name="reduce-only"
checked={!disableReduceOnlyCheckbox && field.value}
disabled={disableReduceOnlyCheckbox}
@@ -573,7 +483,7 @@ export const DealTicket = ({
name="iceberg"
control={control}
render={({ field }) => (
<Checkbox
<TradingCheckbox
name="iceberg"
checked={field.value}
onCheckedChange={field.onChange}
@@ -662,11 +572,11 @@ export const NoWalletWarning = ({
if (isReadOnly) {
return (
<div className="mb-2">
<InputError testId="deal-ticket-error-message-summary">
<TradingInputError testId="deal-ticket-error-message-summary">
{
'You need to connect your own wallet to start trading on this market'
}
</InputError>
</TradingInputError>
</div>
);
}
@@ -703,9 +613,9 @@ const SummaryMessage = memo(
if (error?.message) {
return (
<div className="mb-2">
<InputError testId="deal-ticket-error-message-summary">
<TradingInputError testId="deal-ticket-error-message-summary">
{error?.message}
</InputError>
</TradingInputError>
</div>
);
}
@@ -18,28 +18,30 @@ export const ExpirySelector = ({
onSelect,
errorMessage,
}: ExpirySelectorProps) => {
const now = useRef(new Date());
const date = value ? new Date(value) : now.current;
const dateFormatted = formatForInput(date);
const minDate = formatForInput(date);
return (
<div className="mb-4">
<TradingFormGroup
label={t('Expiry time/date')}
labelFor="expiration"
compact
>
<TradingInput
data-testid="date-picker-field"
id="expiration"
type="datetime-local"
value={value && formatForInput(new Date(value))}
onChange={(e) => onSelect(e.target.value)}
min={formatForInput(useRef(new Date()).current)}
hasError={!!errorMessage}
/>
{errorMessage && (
<TradingInputError testId="deal-ticket-error-message-expiry">
{errorMessage}
</TradingInputError>
)}
</TradingFormGroup>
</div>
<TradingFormGroup
label={t('Expiry time/date')}
labelFor="expiration"
compact={true}
>
<TradingInput
data-testid="date-picker-field"
id="expiration"
type="datetime-local"
value={dateFormatted}
onChange={(e) => onSelect(e.target.value)}
min={minDate}
hasError={!!errorMessage}
/>
{errorMessage && (
<TradingInputError testId="deal-ticket-error-message-expiry">
{errorMessage}
</TradingInputError>
)}
</TradingFormGroup>
);
};
@@ -1,4 +1,7 @@
export * from './deal-ticket-amount';
export * from './deal-ticket-container';
export * from './deal-ticket-limit-amount';
export * from './deal-ticket-market-amount';
export * from './deal-ticket';
export * from './deal-ticket-stop-order';
export * from './deal-ticket-container';
@@ -90,34 +90,32 @@ export const TimeInForceSelector = ({
};
return (
<div className="mb-4">
<TradingFormGroup
label={t('Time in force')}
labelFor="select-time-in-force"
compact={true}
<TradingFormGroup
label={t('Time in force')}
labelFor="select-time-in-force"
compact={true}
>
<TradingSelect
id="select-time-in-force"
value={value}
onChange={(e) => {
onSelect(e.target.value as Schema.OrderTimeInForce);
}}
className="w-full"
data-testid="order-tif"
hasError={!!errorMessage}
>
<TradingSelect
id="select-time-in-force"
value={value}
onChange={(e) => {
onSelect(e.target.value as Schema.OrderTimeInForce);
}}
className="w-full"
data-testid="order-tif"
hasError={!!errorMessage}
>
{options.map(([key, value]) => (
<option key={key} value={value}>
{timeInForceLabel(value)}
</option>
))}
</TradingSelect>
{errorMessage && (
<TradingInputError testId="deal-ticket-error-message-tif">
{renderError(errorMessage)}
</TradingInputError>
)}
</TradingFormGroup>
</div>
{options.map(([key, value]) => (
<option key={key} value={value}>
{timeInForceLabel(value)}
</option>
))}
</TradingSelect>
{errorMessage && (
<TradingInputError testId="deal-ticket-error-message-tif">
{renderError(errorMessage)}
</TradingInputError>
)}
</TradingFormGroup>
);
};
@@ -76,7 +76,7 @@ export const TypeToggle = ({
<TradingDropdownTrigger
data-testid="order-type-Stop"
className={classNames(
'rounded px-2 flex flex-nowrap items-center justify-center',
'rounded px-3 flex flex-nowrap items-center justify-center',
{
'bg-vega-clight-500 dark:bg-vega-cdark-500': selectedOption,
}
@@ -28,17 +28,6 @@ export interface StopOrderFormValues {
expire: boolean;
expiryStrategy?: Schema.StopOrderExpiryStrategy;
expiresAt?: string;
oco?: boolean;
ocoTriggerType: 'price' | 'trailingPercentOffset';
ocoTriggerPrice?: string;
ocoTriggerTrailingPercentOffset?: string;
ocoType: OrderType;
ocoSize: string;
ocoTimeInForce: OrderTimeInForce;
ocoPrice?: string;
}
export type OrderFormValues = {
@@ -149,7 +138,6 @@ export const useDealTicketFormValues = create<Store>()(
})),
{
name: 'vega_deal_ticket_store',
version: 1,
}
)
)
@@ -59,22 +59,6 @@ export const mapFormValuesToOrderSubmission = (
: undefined,
});
const setTrigger = (
stopOrderSetup: StopOrderSetup,
triggerType: StopOrderFormValues['triggerPrice'],
triggerPrice: StopOrderFormValues['triggerPrice'],
triggerTrailingPercentOffset: StopOrderFormValues['triggerTrailingPercentOffset'],
decimalPlaces: number
) => {
if (triggerType === 'price') {
stopOrderSetup.price = removeDecimal(triggerPrice ?? '', decimalPlaces);
} else if (triggerType === 'trailingPercentOffset') {
stopOrderSetup.trailingPercentOffset = (
Number(triggerTrailingPercentOffset) / 100
).toFixed(3);
}
};
export const mapFormValuesToStopOrdersSubmission = (
data: StopOrderFormValues,
marketId: string,
@@ -97,46 +81,31 @@ export const mapFormValuesToStopOrdersSubmission = (
positionDecimalPlaces
),
};
setTrigger(
stopOrderSetup,
data.triggerType,
data.triggerPrice,
data.triggerTrailingPercentOffset,
decimalPlaces
);
let oppositeStopOrderSetup: StopOrderSetup | undefined = undefined;
if (data.oco) {
oppositeStopOrderSetup = {
orderSubmission: mapFormValuesToOrderSubmission(
{
type: data.ocoType,
side: data.side,
size: data.ocoSize,
timeInForce: data.ocoTimeInForce,
price: data.ocoPrice,
reduceOnly: true,
},
marketId,
decimalPlaces,
positionDecimalPlaces
),
};
setTrigger(
oppositeStopOrderSetup,
data.ocoTriggerType,
data.ocoTriggerPrice,
data.ocoTriggerTrailingPercentOffset,
if (data.triggerType === 'price') {
stopOrderSetup.price = removeDecimal(
data.triggerPrice ?? '',
decimalPlaces
);
} else if (data.triggerType === 'trailingPercentOffset') {
stopOrderSetup.trailingPercentOffset = (
Number(data.triggerTrailingPercentOffset) / 100
).toFixed(3);
}
if (data.expire) {
const expiresAt = data.expiresAt && toNanoSeconds(data.expiresAt);
stopOrderSetup.expiresAt = expiresAt;
stopOrderSetup.expiryStrategy = data.expiryStrategy;
if (oppositeStopOrderSetup) {
oppositeStopOrderSetup.expiresAt = expiresAt;
oppositeStopOrderSetup.expiryStrategy = data.expiryStrategy;
stopOrderSetup.expiresAt = data.expiresAt && toNanoSeconds(data.expiresAt);
if (
data.expiryStrategy ===
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
) {
stopOrderSetup.expiryStrategy =
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS;
} else if (
data.expiryStrategy ===
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
) {
stopOrderSetup.expiryStrategy =
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT;
}
}
@@ -145,14 +114,12 @@ export const mapFormValuesToStopOrdersSubmission = (
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
) {
submission.risesAbove = stopOrderSetup;
submission.fallsBelow = oppositeStopOrderSetup;
}
if (
data.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
) {
submission.fallsBelow = stopOrderSetup;
submission.risesAbove = oppositeStopOrderSetup;
}
return submission;
@@ -1,16 +1,8 @@
import {
addDecimalsFormatNumber,
formatNumberPercentage,
isNumeric,
priceChange,
priceChangePercentage,
} from '@vegaprotocol/utils';
import { PriceChangeCell, signedNumberCssClass } from '@vegaprotocol/datagrid';
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { isNumeric } from '@vegaprotocol/utils';
import { PriceChangeCell } from '@vegaprotocol/datagrid';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useCandles } from '../../hooks/use-candles';
import BigNumber from 'bignumber.js';
import classNames from 'classnames';
interface Props {
marketId?: string;
@@ -55,39 +47,10 @@ export const Last24hPriceChange = ({
if (error || !isNumeric(decimalPlaces)) {
return <span>-</span>;
}
const candles = oneDayCandles?.map((c) => c.close) || initialValue || [];
const change = priceChange(candles);
const changePercentage = priceChangePercentage(candles);
return (
<span
className={classNames(
'flex items-center gap-1',
signedNumberCssClass(change)
)}
>
<Arrow value={change} />
<span data-testid="price-change-percentage">
{formatNumberPercentage(new BigNumber(changePercentage.toString()), 2)}
</span>
<span data-testid="price-change">
{addDecimalsFormatNumber(change.toString(), decimalPlaces ?? 0, 3)}
</span>
</span>
<PriceChangeCell
candles={oneDayCandles?.map((c) => c.close) || initialValue || []}
decimalPlaces={decimalPlaces}
/>
);
};
const Arrow = ({ value }: { value: number | bigint }) => {
const size = 10;
if (value > 0) {
return <VegaIcon name={VegaIconNames.ARROW_UP} size={size} />;
}
if (value < 0) {
return <VegaIcon name={VegaIconNames.ARROW_DOWN} size={size} />;
}
return null;
};
@@ -98,7 +98,7 @@ export const Row = ({
<div style={{ wordBreak: 'break-word' }}>
{valueDiffersFromParentMarket ? (
<div className="flex items-center gap-3">
<span className="line-through dark:text-vega-dark-300">
<span className="line-through">
{getFormattedValue(parentValue)}
</span>
<span>{formattedValue}</span>
@@ -1,14 +1,13 @@
import isEqual from 'lodash/isEqual';
import type { ReactNode } from 'react';
import { Fragment, useMemo, useState } from 'react';
import { Fragment, useState } from 'react';
import { useMemo } from 'react';
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n';
import { marketDataProvider } from '../../market-data-provider';
import { totalFeesPercentage } from '../../market-utils';
import {
ExternalLink,
Intent,
Lozenge,
Splash,
Tooltip,
VegaIcon,
@@ -28,15 +27,9 @@ import type {
} from './market-info-data-provider';
import { Last24hVolume } from '../last-24h-volume';
import BigNumber from 'bignumber.js';
import type {
DataSourceDefinition,
MarketTradingMode,
SignerKind,
} from '@vegaprotocol/types';
import {
ConditionOperatorMapping,
MarketTradingModeMapping,
} from '@vegaprotocol/types';
import type { DataSourceDefinition, SignerKind } from '@vegaprotocol/types';
import { ConditionOperatorMapping } from '@vegaprotocol/types';
import { MarketTradingModeMapping } from '@vegaprotocol/types';
import {
DApp,
FLAGS,
@@ -55,6 +48,8 @@ import {
useSuccessorMarketQuery,
} from '../../__generated__';
import { useSuccessorMarketProposalDetailsQuery } from '@vegaprotocol/proposals';
import type { MarketTradingMode } from '@vegaprotocol/types';
import type { Signer } from '@vegaprotocol/types';
import classNames from 'classnames';
import compact from 'lodash/compact';
@@ -580,6 +575,7 @@ export const RiskFactorsInfoPanel = ({
export const PriceMonitoringBoundsInfoPanel = ({
market,
triggerIndex,
parentMarket,
}: MarketInfoProps & {
triggerIndex: number;
}) => {
@@ -588,13 +584,33 @@ export const PriceMonitoringBoundsInfoPanel = ({
variables: { marketId: market.id },
});
const { data: parentData } = useDataProvider({
dataProvider: marketDataProvider,
variables: { marketId: parentMarket?.id || '' },
skip:
!parentMarket ||
!parentMarket?.priceMonitoringSettings?.parameters?.triggers?.[
triggerIndex
],
});
const quoteUnit =
market?.tradableInstrument.instrument.product?.quoteName || '';
const parentQuoteUnit =
parentMarket?.tradableInstrument.instrument.product?.quoteName || '';
const isParentQuoteUnitEqual = quoteUnit === parentQuoteUnit;
const trigger =
market.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex];
const parentTrigger =
parentMarket?.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex];
const isParentTriggerEqual = isEqual(trigger, parentTrigger);
const bounds = data?.priceMonitoringBounds?.[triggerIndex];
const parentBounds = parentData?.priceMonitoringBounds?.[triggerIndex];
const shouldShowParentData =
isParentQuoteUnitEqual && isParentTriggerEqual && !!parentBounds;
if (!trigger) {
console.error(
@@ -622,6 +638,14 @@ export const PriceMonitoringBoundsInfoPanel = ({
highestPrice: bounds.maxValidPrice,
lowestPrice: bounds.minValidPrice,
}}
parentData={
shouldShowParentData
? {
highestPrice: parentBounds.maxValidPrice,
lowestPrice: parentBounds.minValidPrice,
}
: undefined
}
decimalPlaces={market.decimalPlaces}
assetSymbol={quoteUnit}
/>
@@ -815,76 +839,45 @@ export const OracleInfoPanel = ({
: (parentProduct?.dataSourceSpecForTradingTermination
?.data as DataSourceDefinition);
const shouldShowParentData =
parentMarket !== undefined &&
const isParentDataSourceSpecEqual =
parentDataSourceSpec !== undefined &&
dataSourceSpec === parentDataSourceSpec;
const isParentDataSourceSpecIdEqual =
parentDataSourceSpecId !== undefined &&
!isEqual(dataSourceSpec, parentDataSourceSpec);
const wrapperClasses = classNames('mb-4', {
'flex items-center gap-6': shouldShowParentData,
});
dataSourceSpecId === parentDataSourceSpecId;
// We'll only provide successor parent data (if it differs) to the
// DataSourceProof component. Having an old external link struck through
// is unlikely to be useful.
return (
<>
{shouldShowParentData && (
<Lozenge variant={Intent.Primary} className="text-sm">
{t('Updated')}
</Lozenge>
)}
<div className="flex flex-col gap-2">
<DataSourceProof
data-testid="oracle-proof-links"
data={dataSourceSpec}
providers={data}
type={type}
dataSourceSpecId={dataSourceSpecId}
parentData={
isParentDataSourceSpecEqual ? undefined : parentDataSourceSpec
}
parentDataSourceSpecId={
isParentDataSourceSpecIdEqual ? undefined : parentDataSourceSpecId
}
/>
<div className={wrapperClasses}>
{shouldShowParentData &&
parentDataSourceSpec &&
parentDataSourceSpecId &&
parentProduct && (
<div className="flex flex-col gap-2 text-vega-dark-300 line-through">
<DataSourceProof
data-testid="oracle-proof-links"
data={parentDataSourceSpec}
providers={data}
type={type}
dataSourceSpecId={parentDataSourceSpecId}
/>
<ExternalLink
data-testid="oracle-spec-links"
href={`${VEGA_EXPLORER_URL}/oracles/${
type === 'settlementData'
? parentProduct.dataSourceSpecForSettlementData.id
: parentProduct.dataSourceSpecForTradingTermination.id
}`}
>
{type === 'settlementData'
? t('View settlement data specification')
: t('View termination specification')}
</ExternalLink>
</div>
)}
<div className="flex flex-col gap-2">
<DataSourceProof
data-testid="oracle-proof-links"
data={dataSourceSpec}
providers={data}
type={type}
dataSourceSpecId={dataSourceSpecId}
/>
<ExternalLink
data-testid="oracle-spec-links"
href={`${VEGA_EXPLORER_URL}/oracles/${
type === 'settlementData'
? product.dataSourceSpecForSettlementData.id
: product.dataSourceSpecForTradingTermination.id
}`}
>
{type === 'settlementData'
? t('View settlement data specification')
: t('View termination specification')}
</ExternalLink>
</div>
</div>
</>
<ExternalLink
data-testid="oracle-spec-links"
href={`${VEGA_EXPLORER_URL}/oracles/${
type === 'settlementData'
? product.dataSourceSpecForSettlementData.id
: product.dataSourceSpecForTradingTermination.id
}`}
>
{type === 'settlementData'
? t('View settlement data specification')
: t('View termination specification')}
</ExternalLink>
</div>
);
};
@@ -893,14 +886,28 @@ export const DataSourceProof = ({
providers,
type,
dataSourceSpecId,
parentData,
parentDataSourceSpecId,
}: {
data: DataSourceDefinition;
providers: Provider[] | undefined;
type: 'settlementData' | 'termination';
dataSourceSpecId: string;
parentData?: DataSourceDefinition;
parentDataSourceSpecId?: string;
}) => {
// If this is a successor market, we'll only pass parent data to child
// components for comparison if the data differs from the parent market.
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
let parentSigners: Signer[];
if (
parentData &&
parentData.sourceType.__typename === 'DataSourceDefinitionExternal'
) {
parentSigners = parentData.sourceType.sourceType?.signers || [];
}
if (!providers?.length) {
return <NoOracleProof type={type} />;
@@ -908,15 +915,34 @@ export const DataSourceProof = ({
return (
<div className="flex flex-col gap-2">
{signers.map(({ signer }, i) => (
<OracleLink
key={i}
providers={providers}
signer={signer}
type={type}
dataSourceSpecId={dataSourceSpecId}
/>
))}
{signers.map(({ signer }, i) => {
const parentSigner = parentSigners?.find(
({ signer: ParentSigner }) =>
ParentSigner.__typename === signer.__typename
)?.signer;
const isParentSignerEqual = isEqual(signer, parentSigner);
return isParentSignerEqual ? (
<OracleLink
key={i}
providers={providers}
signer={signer}
type={type}
dataSourceSpecId={dataSourceSpecId}
/>
) : (
<OracleLink
key={i}
providers={providers}
signer={signer}
type={type}
dataSourceSpecId={dataSourceSpecId}
parentSigner={parentSigner}
parentDataSourceSpecId={parentDataSourceSpecId}
/>
);
})}
</div>
);
}
@@ -976,13 +1002,22 @@ const OracleLink = ({
signer,
type,
dataSourceSpecId,
parentSigner,
parentDataSourceSpecId,
}: {
providers: Provider[];
signer: SignerKind;
type: 'settlementData' | 'termination';
dataSourceSpecId: string;
parentSigner?: SignerKind;
parentDataSourceSpecId?: string;
}) => {
// If this is a successor market, the parent market data will only have been passed
// in if it differs from the current data.
const signerProviders = getSignerProviders(signer, providers);
const parentSignerProviders = parentSigner
? getSignerProviders(parentSigner, providers)
: [];
if (!signerProviders.length) {
return <NoOracleProof type={type} />;
@@ -990,13 +1025,34 @@ const OracleLink = ({
return (
<div className="mt-2">
{signerProviders.map((provider) => (
<OracleProfile
key={dataSourceSpecId}
provider={provider}
dataSourceSpecId={dataSourceSpecId}
/>
))}
{signerProviders.map((provider) => {
// Making the assumption here that if the provider name is the same,
// that it is the same provider that the parent market used.
const parentProvider = parentSignerProviders.find(
(p) => p.name === provider.name
);
const isParentProviderEqual =
parentProvider !== undefined && isEqual(provider, parentProvider);
// We only want to pass the parent data to the child component if the
// data differs from the parent market.
return isParentProviderEqual ? (
<OracleProfile
key={dataSourceSpecId}
provider={provider}
dataSourceSpecId={dataSourceSpecId}
/>
) : (
<OracleProfile
key={dataSourceSpecId}
provider={provider}
dataSourceSpecId={dataSourceSpecId}
parentProvider={parentProvider}
parentDataSourceSpecId={parentDataSourceSpecId}
/>
);
})}
</div>
);
};
@@ -1019,13 +1075,18 @@ const NoOracleProof = ({
const OracleProfile = (props: {
provider: Provider;
dataSourceSpecId: string;
parentProvider?: Provider;
parentDataSourceSpecId?: string;
}) => {
// If this is a successor market, the parent market data will only have been passed
// in if it differs from the current data.
const [open, onChange] = useState(false);
return (
<div key={props.provider.name}>
<OracleBasicProfile
provider={props.provider}
onClick={() => onChange(!open)}
parentProvider={props.parentProvider}
/>
<OracleDialog {...props} open={open} onChange={onChange} />
</div>
@@ -5,6 +5,7 @@ import {
ExternalLink,
Icon,
Intent,
Lozenge,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
@@ -59,10 +60,12 @@ export const OracleBasicProfile = ({
provider,
onClick,
markets: oracleMarkets,
parentProvider,
}: {
provider: Provider;
markets?: OracleMarketSpecFieldsFragment[] | undefined;
onClick?: (value?: boolean) => void;
parentProvider?: Provider;
}) => {
const { icon, message, intent } = getVerifiedStatusIcon(provider);
@@ -78,8 +81,14 @@ export const OracleBasicProfile = ({
icon: getLinkIcon(proof.type),
}));
// If this is a successor market and there's a different parent provider,
// we'll just show that there's been a change, rather than add old data
// in alongside the new provider.
return (
<>
{parentProvider && (
<Lozenge variant={Intent.Primary}>{t('Updated')}</Lozenge>
)}
<span className="flex gap-1">
{provider.url && (
<span className="flex align-items-bottom text-md gap-1">
+2 -55
View File
@@ -1,10 +1,6 @@
import * as Schema from '@vegaprotocol/types';
import type { Market, MarketMaybeWithDataAndCandles } from './markets-provider';
import {
calcTradedFactor,
filterAndSortMarkets,
totalFeesPercentage,
} from './market-utils';
import type { Market } from './markets-provider';
import { filterAndSortMarkets, totalFeesPercentage } from './market-utils';
const { MarketState, MarketTradingMode } = Schema;
const MARKET_A: Partial<Market> = {
@@ -81,52 +77,3 @@ describe('totalFees', () => {
expect(totalFeesPercentage(i)).toEqual(o);
});
});
describe('calcTradedFactor', () => {
const marketA = {
data: {
markPrice: '10',
},
candles: [
{
volume: '1000',
},
],
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
decimals: 18,
quantum: '1000000000000000000', // 1
},
},
},
},
};
const marketB = {
data: {
markPrice: '10',
},
candles: [
{
volume: '1000',
},
],
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
decimals: 18,
quantum: '1', // 0.0000000000000000001
},
},
},
},
};
it('a is "traded" more than b', () => {
const fa = calcTradedFactor(marketA as MarketMaybeWithDataAndCandles);
const fb = calcTradedFactor(marketB as MarketMaybeWithDataAndCandles);
// it should be true because market a's asset is "more valuable" than b's
expect(fa > fb).toBeTruthy();
});
});
+2 -22
View File
@@ -1,13 +1,8 @@
import { formatNumberPercentage, toBigNum } from '@vegaprotocol/utils';
import { formatNumberPercentage } from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import orderBy from 'lodash/orderBy';
import type {
Market,
Candle,
MarketMaybeWithData,
MarketMaybeWithDataAndCandles,
} from '../';
import type { Market, Candle, MarketMaybeWithData } from '../';
const { MarketState, MarketTradingMode } = Schema;
export const totalFees = (fees: Market['fees']['factors']) => {
@@ -91,18 +86,3 @@ export const calcCandleHigh = (candles: Candle[]): string | undefined => {
export const calcCandleVolume = (candles: Candle[]): string | undefined =>
candles &&
candles.reduce((acc, c) => new BigNumber(acc).plus(c.volume).toString(), '0');
export const calcTradedFactor = (m: MarketMaybeWithDataAndCandles) => {
const volume = Number(calcCandleVolume(m.candles || []) || 0);
const price = m.data?.markPrice ? Number(m.data.markPrice) : 0;
const quantum = Number(
m.tradableInstrument.instrument.product.settlementAsset.quantum
);
const decimals = Number(
m.tradableInstrument.instrument.product.settlementAsset.decimals
);
const fp = toBigNum(price, decimals);
const fq = toBigNum(quantum, decimals);
const factor = fq.multipliedBy(fp).multipliedBy(volume);
return factor.toNumber();
};
@@ -14,8 +14,8 @@ import {
VegaIconNames,
DropdownMenuItem,
TradingDropdownCopyItem,
Pill,
} from '@vegaprotocol/ui-toolkit';
import type { ForwardedRef } from 'react';
import { memo, useMemo } from 'react';
import {
AgGridLazy as AgGrid,
@@ -32,6 +32,7 @@ import type {
VegaValueFormatterParams,
VegaValueGetterParams,
} from '@vegaprotocol/datagrid';
import type { AgGridReact } from 'ag-grid-react';
import type { StopOrder } from '../order-data-provider/stop-orders-data-provider';
import type { ColDef } from 'ag-grid-community';
import type { Order } from '../order-data-provider';
@@ -49,249 +50,236 @@ export type StopOrdersTableProps = TypedDataAgGrid<StopOrder> & {
isReadOnly: boolean;
};
export const StopOrdersTable = memo(
({ onCancel, onMarketClick, onView, ...props }: StopOrdersTableProps) => {
const showAllActions = !props.isReadOnly;
const columnDefs: ColDef[] = useMemo(
() => [
{
headerName: t('Market'),
field: 'market.tradableInstrument.instrument.code',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'market.id', onMarketClick },
export const StopOrdersTable = memo<
StopOrdersTableProps & { ref?: ForwardedRef<AgGridReact> }
>(({ onCancel, onView, onMarketClick, ...props }: StopOrdersTableProps) => {
const showAllActions = !props.isReadOnly;
const columnDefs: ColDef[] = useMemo(
() => [
{
headerName: t('Market'),
field: 'market.tradableInstrument.instrument.code',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'market.id', onMarketClick },
},
{
headerName: t('Trigger'),
field: 'trigger',
cellClass: 'font-mono text-right',
type: 'rightAligned',
sortable: false,
valueFormatter: ({
data,
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string =>
data ? formatTrigger(data, data.market.decimalPlaces) : '',
},
{
field: 'expiresAt',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<StopOrder, 'expiresAt'>) => {
if (
data &&
value &&
data?.expiryStrategy !==
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_UNSPECIFIED
) {
const expiresAt = getDateTimeFormat().format(new Date(value));
const expiryStrategy =
data.expiryStrategy ===
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
? t('Submit')
: t('Cancels');
return `${expiryStrategy} ${expiresAt}`;
}
return '';
},
{
headerName: t('Trigger'),
field: 'trigger',
cellClass: 'font-mono text-right',
type: 'rightAligned',
sortable: false,
valueFormatter: ({
data,
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string =>
data ? formatTrigger(data, data.market.decimalPlaces) : '',
},
{
headerName: t('Size'),
field: 'submission.size',
cellClass: 'font-mono text-right',
type: 'rightAligned',
cellClassRules: {
[positiveClassNames]: ({ data }: { data: StopOrder }) =>
data?.submission.size === Schema.Side.SIDE_BUY,
[negativeClassNames]: ({ data }: { data: StopOrder }) =>
data?.submission.size === Schema.Side.SIDE_SELL,
},
{
field: 'expiresAt',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<StopOrder, 'expiresAt'>) => {
if (
data &&
value &&
data?.expiryStrategy !==
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_UNSPECIFIED
) {
const expiresAt = getDateTimeFormat().format(new Date(value));
const expiryStrategy =
data.expiryStrategy ===
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
? t('Submit')
: t('Cancels');
return `${expiryStrategy} ${expiresAt}`;
}
return '';
},
},
{
headerName: t('Size'),
field: 'submission.size',
cellClass: 'font-mono text-right',
type: 'rightAligned',
cellClassRules: {
[positiveClassNames]: ({ data }: { data: StopOrder }) =>
data?.submission.size === Schema.Side.SIDE_BUY,
[negativeClassNames]: ({ data }: { data: StopOrder }) =>
data?.submission.size === Schema.Side.SIDE_SELL,
},
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) => {
return data?.submission.size && data.market
? toBigNum(
data.submission.size,
data.market.positionDecimalPlaces ?? 0
)
.multipliedBy(
data.submission.side === Schema.Side.SIDE_SELL ? -1 : 1
)
.toNumber()
: undefined;
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<StopOrder, 'size'>) => {
if (!data) {
return '';
}
if (!data?.market || !isNumeric(data.submission.size)) {
return '-';
}
const prefix = data
? data.submission.side === Schema.Side.SIDE_BUY
? '+'
: '-'
: '';
return (
prefix +
addDecimalsFormatNumber(
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) => {
return data?.submission.size && data.market
? toBigNum(
data.submission.size,
data.market.positionDecimalPlaces
data.market.positionDecimalPlaces ?? 0
)
);
},
.multipliedBy(
data.submission.side === Schema.Side.SIDE_SELL ? -1 : 1
)
.toNumber()
: undefined;
},
{
field: 'submission.type',
filter: SetFilter,
filterParams: {
set: Schema.OrderTypeMapping,
},
cellRenderer: ({
value,
}: VegaICellRendererParams<StopOrder, 'submission.type'>) =>
value ? Schema.OrderTypeMapping[value] : '',
valueFormatter: ({
data,
}: VegaValueFormatterParams<StopOrder, 'size'>) => {
if (!data) {
return '';
}
if (!data?.market || !isNumeric(data.submission.size)) {
return '-';
}
const prefix = data
? data.submission.side === Schema.Side.SIDE_BUY
? '+'
: '-'
: '';
return (
prefix +
addDecimalsFormatNumber(
data.submission.size,
data.market.positionDecimalPlaces
)
);
},
{
field: 'status',
filter: SetFilter,
filterParams: {
set: Schema.StopOrderStatusMapping,
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<StopOrder, 'status'>) => {
return value ? Schema.StopOrderStatusMapping[value] : '';
},
cellRenderer: ({
valueFormatted,
data,
}: {
valueFormatted: string;
data: StopOrder;
}) => (
<>
<span data-testid={`order-status-${data?.id}`}>
{valueFormatted}
</span>
{data.ocoLinkId && (
<Pill
size="xxs"
className="uppercase ml-0.5"
title={t('One Cancels the Other')}
>
OCO
</Pill>
)}
</>
),
},
{
field: 'submission.type',
filter: SetFilter,
filterParams: {
set: Schema.OrderTypeMapping,
},
{
field: 'submission.price',
type: 'rightAligned',
cellClass: 'font-mono text-right',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<StopOrder, 'submission.price'>) => {
if (!data) {
return '';
}
if (
!data?.market ||
data.submission.type === Schema.OrderType.TYPE_MARKET ||
!isNumeric(value)
) {
return '-';
}
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
},
cellRenderer: ({
value,
}: VegaICellRendererParams<StopOrder, 'submission.type'>) =>
value ? Schema.OrderTypeMapping[value] : '',
},
{
field: 'status',
filter: SetFilter,
filterParams: {
set: Schema.StopOrderStatusMapping,
},
{
field: 'submission.timeInForce',
filter: SetFilter,
filterParams: {
set: Schema.OrderTimeInForceMapping,
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<StopOrder, 'submission.timeInForce'>) => {
return value ? Schema.OrderTimeInForceCode[value] : '';
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<StopOrder, 'status'>) => {
return value ? Schema.StopOrderStatusMapping[value] : '';
},
{
field: 'updatedAt',
filter: DateRangeFilter,
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) =>
data?.updatedAt || data?.createdAt,
cellRenderer: ({
data,
}: VegaICellRendererParams<StopOrder, 'createdAt'>) => {
if (!data) {
return undefined;
}
const value = data.updatedAt || data.createdAt;
return (
<span data-value={value}>
{value ? getDateTimeFormat().format(new Date(value)) : '-'}
</span>
);
},
cellRenderer: ({
valueFormatted,
data,
}: {
valueFormatted: string;
data: StopOrder;
}) => (
<span data-testid={`order-status-${data?.id}`}>{valueFormatted}</span>
),
},
{
field: 'submission.price',
type: 'rightAligned',
cellClass: 'font-mono text-right',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<StopOrder, 'submission.price'>) => {
if (!data) {
return '';
}
if (
!data?.market ||
data.submission.type === Schema.OrderType.TYPE_MARKET ||
!isNumeric(value)
) {
return '-';
}
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
},
{
colId: 'actions',
...COL_DEFS.actions,
minWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
maxWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
cellRenderer: ({ data }: { data?: StopOrder }) => {
if (!data) return null;
},
{
field: 'submission.timeInForce',
filter: SetFilter,
filterParams: {
set: Schema.OrderTimeInForceMapping,
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<StopOrder, 'submission.timeInForce'>) => {
return value ? Schema.OrderTimeInForceCode[value] : '';
},
},
{
field: 'updatedAt',
filter: DateRangeFilter,
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) =>
data?.updatedAt || data?.createdAt,
cellRenderer: ({
data,
}: VegaICellRendererParams<StopOrder, 'createdAt'>) => {
if (!data) {
return undefined;
}
const value = data.updatedAt || data.createdAt;
return (
<span data-value={value}>
{value ? getDateTimeFormat().format(new Date(value)) : '-'}
</span>
);
},
},
{
colId: 'actions',
...COL_DEFS.actions,
minWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
maxWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
cellRenderer: ({ data }: { data?: StopOrder }) => {
if (!data) return null;
return (
<div className="flex gap-2 items-center justify-end">
{data.status === Schema.StopOrderStatus.STATUS_PENDING &&
!props.isReadOnly && (
<ButtonLink
data-testid="cancel"
onClick={() => onCancel(data)}
return (
<div className="flex gap-2 items-center justify-end">
{data.status === Schema.StopOrderStatus.STATUS_PENDING &&
!props.isReadOnly && (
<ButtonLink
data-testid="cancel"
onClick={() => onCancel(data)}
>
{t('Cancel')}
</ButtonLink>
)}
{data.status === Schema.StopOrderStatus.STATUS_TRIGGERED &&
data.order && (
<ActionsDropdown data-testid="stop-order-actions-content">
<TradingDropdownCopyItem
value={data.order.id}
text={t('Copy order ID')}
/>
<DropdownMenuItem
key={'view-order'}
data-testid="view-order"
onClick={() =>
data.order &&
onView({ ...data.order, market: data.market })
}
>
{t('Cancel')}
</ButtonLink>
)}
{data.status === Schema.StopOrderStatus.STATUS_TRIGGERED &&
data.order && (
<ActionsDropdown data-testid="stop-order-actions-content">
<TradingDropdownCopyItem
value={data.order.id}
text={t('Copy order ID')}
/>
<DropdownMenuItem
key={'view-order'}
data-testid="view-order"
onClick={() =>
data.order &&
onView({ ...data.order, market: data.market })
}
>
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View order details')}
</DropdownMenuItem>
</ActionsDropdown>
)}
</div>
);
},
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View order details')}
</DropdownMenuItem>
</ActionsDropdown>
)}
</div>
);
},
],
[onCancel, onMarketClick, onView, props.isReadOnly, showAllActions]
);
},
],
[onCancel, onMarketClick, onView, props.isReadOnly, showAllActions]
);
return (
<AgGrid
defaultColDef={defaultColDef}
columnDefs={columnDefs}
getRowId={({ data }) => data.id}
components={{ MarketNameCell }}
{...props}
/>
);
}
);
return (
<AgGrid
defaultColDef={defaultColDef}
columnDefs={columnDefs}
getRowId={({ data }) => data.id}
components={{ MarketNameCell }}
{...props}
/>
);
});
@@ -181,7 +181,7 @@ const getSubscriptionVariables = (
): PositionsSubscriptionSubscriptionVariables[] =>
([] as string[]).concat(variables.partyIds).map((partyId) => ({ partyId }));
export const positionsDataProvider = makeDataProvider<
const positionsDataProvider = makeDataProvider<
PositionsQuery,
PositionFieldsFragment[],
PositionsSubscriptionSubscription,
+1 -15
View File
@@ -177,21 +177,7 @@ module.exports = {
success: '#00F780',
},
fontFamily: {
mono: [
'ui-monospace',
'Menlo',
'Monaco',
'Cascadia Mono',
'Segoe UI Mono',
'Roboto Mono',
'Oxygen Mono',
'Ubuntu Monospace',
'Source Code Pro',
'Fira Mono',
'Droid Sans Mono',
'Courier New',
'monospace',
],
mono: ['Roboto Mono', 'monospace'],
sans: [
'"Helvetica Neue", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
],
@@ -5,7 +5,7 @@ import { VegaIconNameMap } from './vega-icon-record';
export interface VegaIconProps {
name: VegaIconNames;
size?: 8 | 10 | 12 | 13 | 14 | 16 | 18 | 20 | 24 | 32;
size?: 8 | 10 | 12 | 13 | 14 | 16 | 20 | 24 | 32;
}
export const VegaIcon = ({ size = 16, name }: VegaIconProps) => {
-1
View File
@@ -34,7 +34,6 @@ export * from './progress-bar';
export * from './radio-group';
export * from './rounded-wrapper';
export * from './select';
export * from './show-more';
export * from './simple-grid';
export * from './slider';
export * from './sparkline';
@@ -1 +0,0 @@
export * from './show-more';
@@ -1,9 +0,0 @@
import { render } from '@testing-library/react';
import { ShowMore } from './show-more';
describe('Button', () => {
it('should render successfully', () => {
const { baseElement } = render(<ShowMore>test</ShowMore>);
expect(baseElement).toBeTruthy();
});
});
@@ -1,50 +0,0 @@
import type { Story, Meta } from '@storybook/react';
import { ShowMore } from './show-more';
export default {
component: ShowMore,
title: 'ShowMore',
} as Meta;
const Template: Story = (args) => (
<ShowMore {...args}>
<p>
Spaceflight will never tolerate carelessness, incapacity, and neglect.
Somewhere, somehow, we screwed up. It could have been in design, build, or
test. Whatever it was, we should have caught it. We were too gung ho about
the schedule and we locked out all of the problems we saw each day in our
work. Every element of the program was in trouble and so were we. The
simulators were not working, Mission Control was behind in virtually every
area, and the flight and test procedures changed daily. Nothing we did had
any shelf life. Not one of us stood up and said, Dammit, stop! I dont
know what Thompsons committee will find as the cause, but I know what I
find. We are the cause! We were not ready! We did not do our job. We were
rolling the dice, hoping that things would come together by launch day,
when in our hearts we knew it would take a miracle. We were pushing the
schedule and betting that the Cape would slip before we did. From this
day forward, Flight Control will be known by two words: Tough and
Competent. Tough means we are forever accountable for what we do or what
we fail to do. We will never again compromise our responsibilities. Every
time we walk into Mission Control we will know what we stand for.
Competent means we will never take anything for granted. We will never be
found short in our knowledge and in our skills. Mission Control will be
perfect. When you leave this meeting today you will go to your office and
the first thing you will do there is to write Tough and Competent on
your blackboards. It will never be erased. Each day when you enter the
room these words will remind you of the price paid by Grissom, White, and
Chaffee. These words are the price of admission to the ranks of Mission
Control.
</p>
</ShowMore>
);
export const Default = Template.bind({});
export const CustomMaxHeight = Template.bind({});
CustomMaxHeight.args = {
closedMaxHeightPx: 50,
};
export const CustomOverlayColour = Template.bind({});
CustomOverlayColour.args = {
overlayColourOverrides: 'to-yellow-400',
};
@@ -1,78 +0,0 @@
import classNames from 'classnames';
import { useRef, useState, useEffect } from 'react';
import { t } from '@vegaprotocol/i18n';
import { Button } from '../button';
import type { ReactNode } from 'react';
type ShowMoreProps = {
children: ReactNode;
closedMaxHeightPx?: number;
overlayColourOverrides?: string;
};
export const ShowMore = ({
children,
closedMaxHeightPx = 125,
overlayColourOverrides,
}: ShowMoreProps) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const checkHeight = () => {
const container = containerRef.current;
if (container) {
container.scrollHeight < closedMaxHeightPx
? setExpanded(true)
: setExpanded(false);
}
};
checkHeight();
window.addEventListener('resize', checkHeight);
return () => {
window.removeEventListener('resize', checkHeight);
};
}, [closedMaxHeightPx]);
const containerClasses = classNames(
'overflow-hidden transition-all ease-in-out duration-300',
{
'max-h-none': expanded,
}
);
const overlayClasses = classNames(
`absolute w-full h-16 bottom-0 left-0 transition-opacity duration-300 bg-gradient-to-b from-transparent ${
overlayColourOverrides ? overlayColourOverrides : 'to-white dark:to-black'
}`,
{
hidden: expanded,
}
);
return (
<>
<div className="relative">
<div
ref={containerRef}
className={containerClasses}
style={{ maxHeight: expanded ? 'none' : `${closedMaxHeightPx}px` }}
>
{children}
</div>
<div className={overlayClasses}></div>
</div>
{!expanded && (
<div className="mt-1 text-center">
<Button size={'sm'} onClick={() => setExpanded(true)}>
{t('Show more')}
</Button>
</div>
)}
</>
);
};
@@ -204,7 +204,6 @@ const ConnectorList = ({
setWalletUrl: (value: string) => void;
isDesktopWalletRunning: boolean | null;
}) => {
const { pubKey } = useVegaWallet();
const title = isBrowserWalletInstalled()
? t('Connect Vega wallet')
: t('Get a Vega wallet');
@@ -235,7 +234,7 @@ const ConnectorList = ({
onClick={() => onSelect('injected')}
/>
) : (
<GetWalletButton />
<GetWallet />
)}
</div>
<div>
@@ -243,7 +242,6 @@ const ConnectorList = ({
type="view"
text={t('View as party')}
onClick={() => onSelect('view')}
disabled={Boolean(pubKey)}
/>
</div>
<div className="last:mb-0">
@@ -320,7 +318,7 @@ const SelectedForm = ({
throw new Error('No connector selected');
};
export const GetWalletButton = ({ className }: { className?: string }) => {
const GetWallet = () => {
const { MOZILLA_EXTENSION_URL, CHROME_EXTENSION_URL } = useEnvironment();
const isItChrome = window.navigator.userAgent.includes('Chrome');
const isItMozilla =
@@ -349,13 +347,10 @@ export const GetWalletButton = ({ className }: { className?: string }) => {
return !isItChrome && !isItMozilla ? (
<div
className={classNames(
[
'bg-vega-blue-350 hover:bg-vega-blue-400 dark:bg-vega-blue-650 dark:hover:bg-vega-blue-600',
'flex gap-2 items-center justify-center rounded h-8 px-3 relative',
],
className
)}
className={classNames([
'bg-vega-blue-350 hover:bg-vega-blue-400 dark:bg-vega-blue-650 dark:hover:bg-vega-blue-600',
'flex gap-2 items-center justify-center rounded h-8 px-3 relative',
])}
data-testid="get-wallet-button"
>
{buttonContent}
@@ -365,7 +360,7 @@ export const GetWalletButton = ({ className }: { className?: string }) => {
onClick={onClick}
intent={Intent.Info}
data-testid="get-wallet-button"
className={classNames('relative', className)}
className="relative"
size="small"
fill
>
@@ -414,7 +409,6 @@ const CustomUrlInput = ({
isDesktopWalletRunning: boolean | null;
onSelect: (type: WalletType) => void;
}) => {
const { pubKey } = useVegaWallet();
const [urlInputExpanded, setUrlInputExpanded] = useState(false);
return urlInputExpanded ? (
<>
@@ -439,7 +433,7 @@ const CustomUrlInput = ({
/>
</TradingFormGroup>
<ConnectionOption
disabled={!isDesktopWalletRunning || Boolean(pubKey)}
disabled={!isDesktopWalletRunning}
type="jsonRpc"
text={t('Connect the App/CLI')}
onClick={() => onSelect('jsonRpc')}
@@ -448,7 +442,7 @@ const CustomUrlInput = ({
) : (
<>
<ConnectionOption
disabled={!isDesktopWalletRunning || Boolean(pubKey)}
disabled={!isDesktopWalletRunning}
type="jsonRpc"
text={t('Use the Desktop App/CLI')}
onClick={() => onSelect('jsonRpc')}
@@ -459,7 +453,6 @@ const CustomUrlInput = ({
<button
className="underline text-default"
onClick={() => setUrlInputExpanded(true)}
disabled={Boolean(pubKey)}
>
{t('Enter a custom wallet location')}{' '}
<VegaIcon name={VegaIconNames.ARROW_RIGHT} />
@@ -474,7 +467,6 @@ const CustomUrlInput = ({
<button
className="underline"
onClick={() => setUrlInputExpanded(true)}
disabled={Boolean(pubKey)}
>
{t('custom wallet location')}
</button>
@@ -12,9 +12,7 @@ export const useIsWalletServiceRunning = (
const checkState = useCallback(async () => {
const connector = connectors['jsonRpc'] as JsonRpcConnector;
if (url && url !== connector.url) {
connector.url = url;
}
connector.url = url;
try {
await connector.checkCompat();
const chainIdResult = await connector.getChainId();
@@ -9,7 +9,6 @@ import type {
VegaStoredTxState,
WithdrawalBusEventFieldsFragment,
StopOrdersSubmission,
StopOrderSetup,
} from '@vegaprotocol/wallet';
import {
isTransferTransaction,
@@ -50,7 +49,6 @@ import {
useOrderByIdQuery,
useStopOrderByIdQuery,
} from '@vegaprotocol/orders';
import type { Market } from '@vegaprotocol/markets';
import { useMarketsMapProvider } from '@vegaprotocol/markets';
import type { Side } from '@vegaprotocol/types';
import { OrderStatusMapping } from '@vegaprotocol/types';
@@ -176,15 +174,11 @@ const SubmitOrderDetails = ({
);
};
const SubmitStopOrderSetup = ({
stopOrderSetup,
triggerDirection,
market,
}: {
stopOrderSetup: StopOrderSetup;
triggerDirection: Schema.StopOrderTriggerDirection;
market: Market;
}) => {
const SubmitStopOrderDetails = ({ data }: { data: StopOrdersSubmission }) => {
const { data: markets } = useMarketsMapProvider();
const stopOrderSetup = data.risesAbove || data.fallsBelow;
if (!stopOrderSetup) return null;
const market = markets?.[stopOrderSetup?.orderSubmission.marketId];
if (!market || !stopOrderSetup) return null;
const { price, size, side } = stopOrderSetup.orderSubmission;
@@ -197,64 +191,37 @@ const SubmitStopOrderSetup = ({
__typename: 'StopOrderTrailingPercentOffset',
};
}
return (
<p>
<SizeAtPrice
meta={{
positionDecimalPlaces: market.positionDecimalPlaces,
decimalPlaces: market.decimalPlaces,
asset:
market.tradableInstrument.instrument.product.settlementAsset.symbol,
}}
side={side}
size={size}
price={price}
/>
<br />
{trigger &&
formatTrigger(
{
triggerDirection,
trigger,
},
market.decimalPlaces,
''
)}
</p>
);
};
const SubmitStopOrderDetails = ({ data }: { data: StopOrdersSubmission }) => {
const { data: markets } = useMarketsMapProvider();
const marketId =
data.fallsBelow?.orderSubmission.marketId ||
data.risesAbove?.orderSubmission.marketId;
const market = marketId && markets?.[marketId];
if (!market) {
return null;
}
const triggerDirection = data.risesAbove
? Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
: Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW;
return (
<Panel>
<h4>{t('Submit stop order')}</h4>
<p>{market?.tradableInstrument.instrument.code}</p>
{data.fallsBelow && (
<SubmitStopOrderSetup
stopOrderSetup={data.fallsBelow}
triggerDirection={
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
}
market={market}
<p>
<SizeAtPrice
meta={{
positionDecimalPlaces: market.positionDecimalPlaces,
decimalPlaces: market.decimalPlaces,
asset:
market.tradableInstrument.instrument.product.settlementAsset
.symbol,
}}
side={side}
size={size}
price={price}
/>
)}
{data.risesAbove && (
<SubmitStopOrderSetup
stopOrderSetup={data.risesAbove}
triggerDirection={
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
}
market={market}
/>
)}
<br />
{trigger &&
formatTrigger(
{
triggerDirection,
trigger,
},
market.decimalPlaces,
''
)}
</p>
</Panel>
);
};
+2 -1
View File
@@ -21,7 +21,8 @@
"echo $NX_TENDERMINT_URL",
"echo $NX_TENDERMINT_WEBSOCKET_URL",
"echo $NX_ETHEREUM_PROVIDER_URL"
]
],
"url": "https://cloud.nx.app"
}
}
},
+6 -18
View File
@@ -1,34 +1,22 @@
# First use & get started steps
## "Onboarding" state is gradable and has following steps: (<a name="0007-FUGS-002" href="#0007-FUGS-002">0007-FUGS-003</a>)
- New visitor - has no wallet nor any dapps running.
- I **must** see CTA button "Get started", which clicking launches the get wallet flow (wallet connection window with links to the Chrome and FF stores)
- Has wallet - Once wallet detected set to this.
- I **must** see CTA button "Connect", which clicking launches the wallet connection
- Has connected - Once user has connected set to this.
- I **must** see CTA button "Deposit", which clicking launches the deposit ticket.
- Has deposited - Once user has made AT LEAST one deposit of ANY settlement asset set to this.
- I **must** see CTA button "Dismiss", which clicking updates the state to Ready to trade - Get started box should now disappear forever.
- Ready to trade - Once user has made at least one deposit AND has dismissed the "Get Started" box in the ticket.
- Onboarding window nor contextual "Get started" banner should be not displayed anymore.
## When first enter the app or next times, but I didn't accomplish all onboarding steps.
## When first enter the app
- **Must** When I open Console for the first time I can see what it is i.e. a short description and key features in auto opened dialog window (first use popup) (<a name="0007-FUGS-001" href="#0007-FUGS-001">0007-FUGS-001</a>)
- If full "onboarding" hasn't been accomplished yet, I **must** see the popup with my progress marked. (<a name="0007-FUGS-002" href="#0007-FUGS-002">0007-FUGS-002</a>)
- **Must** If my wallet is already connected I don't see the first use popup (<a name="0007-FUGS-002" href="#0007-FUGS-002">0007-FUGS-002</a>)
- - **Must** If window.vega is detected (browser wallet is installed), don't open first use popup (<a name="0007-FUGS-003" href="#0007-FUGS-003">0007-FUGS-003</a>)
- - **Must** If we detect previous connection using localStorage for desktop/cli wallet, don't open first use popup (<a name="0007-FUGS-004" href="#0007-FUGS-004">0007-FUGS-004</a>)
- **Must** There is a call to action to browse markets, linking to the market view market/all (<a name="0007-FUGS-005" href="#0007-FUGS-005">0007-FUGS-005</a>)
- **Must** I can see the steps I need to take to get started trading (<a name="0007-FUGS-007" href="#0007-FUGS-006">0007-FUGS-006</a>)
- **Must** There is a call to action to get started, triggering the connect modal (<a name="0007-FUGS-007" href="#0007-FUGS-007">0007-FUGS-007</a>)
- **Must** There is a link to try out trading on Fairground when I'm on Mainnet (<a name="0007-FUGS-008" href="#0007-FUGS-008">0007-FUGS-008</a>)
- **Must** There is a link to trade with real funds on Mainnet when I am on Fairground (<a name="0007-FUGS-010" href="#0007-FUGS-010">0007-FUGS-010</a>)
- **Must** When I am on the Fairground version, I can see a warning / call out that this is Fairground meaning I can try out with virtual assets at no risk (<a name="0007-FUGS-011" href="#0007-FUGS-011">0007-FUGS-011</a>)
- If I dismiss the popup, I **must** not see it unless I NOT accomplish full "onboarding"
- If I dismiss the popup, I land on the default market (<a name="0007-FUGS-012" href="#0007-FUGS-012">0007-FUGS-012</a>)
## When the popup has been dismissed:
## When first use popup has been seen, but no browser wallet is installed
- **Must** I can see the steps to get started with a visible call to action (according to my progress) in the context of the deal ticket, deposit, withdraw, transfer components in the sidebar (<a name="0007-FUGS-013" href="#0007-FUGS-013">0007-FUGS-013</a>)
- **Must** I can see the steps to get started with a visible call to action to "get started" in the context of the deal ticket, deposit, withdraw, transfer components in the sidebar (<a name="0007-FUGS-013" href="#0007-FUGS-013">0007-FUGS-013</a>)
- **Must** Remove buttons from pane containers that prompt to connect wallet (<a name="0007-FUGS-014" href="#0007-FUGS-014">0007-FUGS-014</a>)
- **Must** We've replaced "connect wallet" in the top right with "get started" (<a name="0007-FUGS-015" href="#0007-FUGS-015">0007-FUGS-015</a>)
- **Must** When I press the get started CTA, I see the wallet connect popup (<a name="0007-FUGS-016" href="#0007-FUGS-016">0007-FUGS-016</a>)