Compare commits

..
Author SHA1 Message Date
asiaznik d28dad2777 fix(proposals): error policy guard for proposal data provider 2023-08-17 17:41:16 +02:00
208 changed files with 3750 additions and 5514 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
- 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,
},
},
};
@@ -41,7 +41,7 @@ export const TxDetailsIssueSignatures = ({
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const cmd: Command = txData.command.issueSignatures;
const cmd: Command = txData.command;
const k = cmd.kind ? kind[cmd.kind] : null;
return (
@@ -48,11 +48,9 @@ query ExplorerPartyAssets($partyId: ID!) {
}
stakingSummary {
currentStakeAvailable
linkings(pagination: { last: 100 }) {
linkings(pagination: { first: 100 }) {
edges {
node {
type
status
amount
}
}
@@ -10,7 +10,7 @@ export type ExplorerPartyAssetsQueryVariables = Types.Exact<{
}>;
export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string, linkings: { __typename?: 'StakesConnection', edges?: Array<{ __typename?: 'StakeLinkingEdge', node: { __typename?: 'StakeLinking', type: Types.StakeLinkingType, status: Types.StakeLinkingStatus, amount: string } } | null> | null } }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } | null } } | null> | null } | null } }> } | null };
export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string, linkings: { __typename?: 'StakesConnection', edges?: Array<{ __typename?: 'StakeLinkingEdge', node: { __typename?: 'StakeLinking', amount: string } } | null> | null } }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } | null } } | null> | null } | null } }> } | null };
export const ExplorerPartyAssetsAccountsFragmentDoc = gql`
fragment ExplorerPartyAssetsAccounts on AccountBalance {
@@ -64,11 +64,9 @@ export const ExplorerPartyAssetsDocument = gql`
}
stakingSummary {
currentStakeAvailable
linkings(pagination: {last: 100}) {
linkings(pagination: {first: 100}) {
edges {
node {
type
status
amount
}
}
@@ -42,15 +42,9 @@ export const PartyBlockStake = ({
linkedLength && linkedLength > 0
? p?.stakingSummary?.linkings?.edges
?.reduce((total, e) => {
const accumulator = new BigNumber(total);
const diff = new BigNumber(e?.node.amount || 0);
if (e?.node.type === 'TYPE_LINK') {
return accumulator.plus(diff);
} else if (e?.node.type === 'TYPE_UNLINK') {
return accumulator.minus(diff);
} else {
return accumulator;
}
return new BigNumber(total).plus(
new BigNumber(e?.node.amount || 0)
);
}, new BigNumber(0))
.toString()
: '0';
@@ -220,7 +220,6 @@ describe(
cy.getByTestId(changeVoteButton).should('be.visible').click();
voteForProposal('for');
// 3001-VOTE-064
cy.getByTestId('user-voted-yes').should('exist');
getProposalInformationFromTable('Tokens for proposal')
.should('have.text', (1).toFixed(2))
.and('be.visible');
@@ -361,34 +360,6 @@ describe(
stakingPageDisassociateAllTokens();
});
it('Error message should be displayed if error returned from wallet when voting', function () {
const errorMsg =
'Application error: party has already submitted the maximum number of transactions of this type per epoch (3)';
createRawProposal();
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getProposalFromTitle(rawProposal.rationale.title).within(() =>
cy.getByTestId(viewProposalButton).click()
);
cy.intercept('POST', '/api/v2/requests', {
jsonrpc: '2.0',
error: {
code: 2001,
message: 'Application error',
data: 'party has already submitted the maximum number of transactions of this type per epoch (3)',
},
id: '-PK5EGmErnjLhAmzMeclC',
});
cy.contains('Vote breakdown').should('be.visible', { timeout: 10000 });
cy.getByTestId('vote-buttons').contains('for').click();
cy.getByTestId('dialog-title').should(
'have.text',
'Transaction failed'
);
cy.getByTestId('Error').should('have.text', errorMsg);
});
});
it('Able to see successor market details with new and updated values', function () {
cy.createMarket();
cy.reload();
@@ -119,7 +119,6 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
// 0006-NETW-001 0006-NETW-002
it('should display network data', function () {
cy.getByTestId('git-network-data')
.should('contain.text', 'Reading network data from')
@@ -131,37 +130,6 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
// 0006-NETW-003 0006-NETW-008 0006-NETW-009 0006-NETW-010 0006-NETW-012 0006-NETW-013 0006-NETW-017 0006-NETW-018 0006-NETW-019 0006-NETW-020
it('should have option to switch to different network node', function () {
cy.getByTestId('git-network-data').within(() => {
cy.getByTestId('link').click();
});
cy.getByTestId('node-row').within(() => {
cy.getByTestId('node-url-0')
.parent()
.should('have.text', 'http://localhost:3008/graphql');
cy.getByTestId('response-time-cell')
.invoke('text')
.should('not.be.empty')
.and('not.eq', 'Checking');
cy.getByTestId('block-height-cell')
.invoke('text')
.should('not.be.empty')
.then((currentBlockHeight) => {
// Check that block height updates automatically
cy.getByTestId('block-height-cell')
.invoke('text')
.should('not.eq', currentBlockHeight);
});
cy.getByTestId('subscription-cell').should('have.text', 'Yes');
});
cy.getByTestId('connect').should('be.disabled');
cy.getByTestId('node-url-custom').click();
cy.get('input').should('exist');
cy.getByTestId('connect').should('be.disabled');
cy.getByTestId('icon-cross').click();
});
it('should display eth data', function () {
cy.getByTestId('git-eth-data')
.should('contain.text', 'Reading Ethereum data from')
@@ -170,7 +138,6 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
// 0006-NETW-011
it('should contain link for known issues on Github', function () {
cy.getByTestId('git-info').within(() => {
cy.contains('Known issues and feedback on')
@@ -182,7 +182,7 @@ export function clickOnValidatorFromList(
} else {
cy.get(`[row-id="${validatorNumber}"]`)
.should('be.visible')
.first()
.find(stakeValidatorListName)
.as('validatorOnList');
cy.get('@validatorOnList').click();
}
+2 -1
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
@@ -29,4 +31,3 @@ LC_ALL="en_US.UTF-8"
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
+1 -5
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,5 +26,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
CYPRESS_FAIRGROUND=false
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
NX_SUCCESSOR_MARKETS=false
+1 -5
View File
@@ -14,12 +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_METAMASK_SNAPS=true
NX_SUCCESSOR_MARKETS=true
+1 -4
View File
@@ -14,12 +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_METAMASK_SNAPS=false
NX_SUCCESSOR_MARKETS=false
+1 -4
View File
@@ -13,12 +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_METAMASK_SNAPS=false
NX_SUCCESSOR_MARKETS=false
+1 -5
View File
@@ -10,12 +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_METAMASK_SNAPS=true
NX_SUCCESSOR_MARKETS=true
+1 -5
View File
@@ -15,12 +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_METAMASK_SNAPS=true
NX_SUCCESSOR_MARKETS=true
+1 -5
View File
@@ -12,12 +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_METAMASK_SNAPS=false
NX_SUCCESSOR_MARKETS=false
+1 -10
View File
@@ -1,26 +1,17 @@
import { ENV } from '@vegaprotocol/environment';
import {
JsonRpcConnector,
ViewConnector,
InjectedConnector,
SnapConnector,
DEFAULT_SNAP_ID,
} from '@vegaprotocol/wallet';
const urlParams = new URLSearchParams(window.location.search);
export const jsonRpc = new JsonRpcConnector();
export const injected = new InjectedConnector();
export const jsonRpc = new JsonRpcConnector();
export const view = new ViewConnector(urlParams.get('address'));
export const snap = new SnapConnector(
ENV.VEGA_URL ? new URL(ENV.VEGA_URL).origin : undefined,
DEFAULT_SNAP_ID
);
export const Connectors = {
injected,
jsonRpc,
view,
snap,
};
@@ -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>
</>
)}
@@ -55,7 +55,7 @@ export const Proposal = ({
mostRecentlyEnactedAssociatedMarketProposal,
}: ProposalProps) => {
const { t } = useTranslation();
const { submit, Dialog, finalizedVote, transaction } = useVoteSubmit();
const { submit, Dialog, finalizedVote } = useVoteSubmit();
const { voteState, voteDatetime } = useUserVote(proposal?.id, finalizedVote);
if (!proposal) {
@@ -215,7 +215,6 @@ export const Proposal = ({
}
submit={submit}
dialog={Dialog}
transaction={transaction}
voteState={voteState}
voteDatetime={voteDatetime}
/>
@@ -1,110 +0,0 @@
import { render, screen } from '@testing-library/react';
import { VoteTransactionDialog } from './vote-transaction-dialog';
import { VoteState } from './use-user-vote';
import { VegaTxStatus } from '@vegaprotocol/wallet';
describe('VoteTransactionDialog', () => {
const mockTransactionDialog = jest.fn(({ title, content }) => (
<div>
<div>{title}</div>
<div>{content?.Complete}</div>
</div>
));
it('renders without crashing', () => {
render(
<VoteTransactionDialog
voteState={VoteState.Yes}
transaction={null}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.getByTestId('vote-transaction-dialog')).toBeInTheDocument();
});
it('renders with txRequested title when voteState is Requested', () => {
render(
<VoteTransactionDialog
voteState={VoteState.Requested}
transaction={null}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.getByText('txRequested')).toBeInTheDocument();
});
it('renders with votePending title when voteState is Pending', () => {
render(
<VoteTransactionDialog
voteState={VoteState.Pending}
transaction={null}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.getByText('votePending')).toBeInTheDocument();
});
it('renders with no title when voteState is neither Requested nor Pending', () => {
render(
<VoteTransactionDialog
voteState={VoteState.Yes} // or any other state other than Requested or Pending
transaction={null}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.queryByText('txRequested')).not.toBeInTheDocument();
expect(screen.queryByText('votePending')).not.toBeInTheDocument();
});
it('renders custom error message when voteState is Failed and error message exists', () => {
render(
<VoteTransactionDialog
voteState={VoteState.Failed}
transaction={{
error: { message: 'Custom error test message', name: 'blah' },
txHash: null,
signature: null,
status: VegaTxStatus.Error,
dialogOpen: false,
}}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.getByText('Custom error test message')).toBeInTheDocument();
});
it('renders default error message when voteState is failed and no error message exists on the tx', () => {
render(
<VoteTransactionDialog
voteState={VoteState.Failed}
transaction={{
error: null,
txHash: null,
signature: null,
status: VegaTxStatus.Error,
dialogOpen: false,
}}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.getByText('voteError')).toBeInTheDocument();
});
it('renders default ui (i.e. not error) when not in a failed state', () => {
render(
<VoteTransactionDialog
voteState={VoteState.Yes}
transaction={null}
TransactionDialog={mockTransactionDialog}
/>
);
expect(screen.queryByText('voteError')).not.toBeInTheDocument();
});
});
@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { render, screen, fireEvent } from '@testing-library/react';
import BigNumber from 'bignumber.js';
import { VoteButtons } from './vote-buttons';
import { VoteState } from './use-user-vote';
@@ -24,7 +24,6 @@ describe('Vote buttons', () => {
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -48,7 +47,6 @@ describe('Vote buttons', () => {
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -83,7 +81,6 @@ describe('Vote buttons', () => {
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -108,7 +105,6 @@ describe('Vote buttons', () => {
currentStakeAvailable={new BigNumber(0)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -136,7 +132,6 @@ describe('Vote buttons', () => {
currentStakeAvailable={new BigNumber(1)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -164,7 +159,6 @@ describe('Vote buttons', () => {
currentStakeAvailable={new BigNumber(10)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -189,7 +183,6 @@ describe('Vote buttons', () => {
currentStakeAvailable={new BigNumber(10)}
dialog={() => <div>Blah</div>}
submit={() => Promise.resolve()}
transaction={null}
/>
</VegaWalletContext.Provider>
</MockedProvider>
@@ -17,7 +17,7 @@ import { VoteState } from './use-user-vote';
import { ProposalMinRequirements, ProposalUserAction } from '../shared';
import { VoteTransactionDialog } from './vote-transaction-dialog';
import { useVoteButtonsQuery } from './__generated__/Stake';
import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet';
import type { DialogProps } from '@vegaprotocol/wallet';
interface VoteButtonsContainerProps {
voteState: VoteState | null;
@@ -27,7 +27,6 @@ interface VoteButtonsContainerProps {
minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
transaction: VegaTxState | null;
dialog: (props: DialogProps) => JSX.Element;
className?: string;
}
@@ -68,7 +67,6 @@ export const VoteButtons = ({
minVoterBalance,
spamProtectionMinTokens,
submit,
transaction,
dialog: Dialog,
}: VoteButtonsProps) => {
const { t } = useTranslation();
@@ -210,11 +208,7 @@ export const VoteButtons = ({
</p>
)
)}
<VoteTransactionDialog
voteState={voteState}
transaction={transaction}
TransactionDialog={Dialog}
/>
<VoteTransactionDialog voteState={voteState} TransactionDialog={Dialog} />
</>
);
};
@@ -12,7 +12,7 @@ import { VoteButtonsContainer } from './vote-buttons';
import { SubHeading } from '../../../../components/heading';
import { ProposalType } from '../proposal/proposal';
import type { VoteValue } from '@vegaprotocol/types';
import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet';
import type { DialogProps } from '@vegaprotocol/wallet';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { VoteState } from './use-user-vote';
@@ -22,7 +22,6 @@ interface VoteDetailsProps {
minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined;
proposalType: ProposalType | null;
transaction: VegaTxState | null;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
dialog: (props: DialogProps) => JSX.Element;
voteState: VoteState | null;
@@ -35,7 +34,6 @@ export const VoteDetails = ({
spamProtectionMinTokens,
proposalType,
submit,
transaction,
dialog,
voteState,
voteDatetime,
@@ -230,7 +228,6 @@ export const VoteDetails = ({
spamProtectionMinTokens={spamProtectionMinTokens}
className="flex"
submit={submit}
transaction={transaction}
dialog={dialog}
/>
)
@@ -1,10 +1,9 @@
import { t } from '@vegaprotocol/i18n';
import { VoteState } from './use-user-vote';
import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet';
import type { DialogProps } from '@vegaprotocol/wallet';
interface VoteTransactionDialogProps {
voteState: VoteState;
transaction: VegaTxState | null;
TransactionDialog: (props: DialogProps) => JSX.Element;
}
@@ -21,15 +20,12 @@ const dialogTitle = (voteState: VoteState): string | undefined => {
export const VoteTransactionDialog = ({
voteState,
transaction,
TransactionDialog,
}: VoteTransactionDialogProps) => {
// Render a custom message if the voting fails otherwise
// pass undefined so that the default vega transaction dialog UI gets used
const customMessage =
voteState === VoteState.Failed ? (
<p>{transaction?.error?.message || t('voteError')}</p>
) : undefined;
voteState === VoteState.Failed ? <p>{t('voteError')}</p> : undefined;
return (
<div data-testid="vote-transaction-dialog">
+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
@@ -259,12 +259,6 @@ describe('Closed markets', { tags: '@smoke' }, () => {
.find('[data-testid="market-code"]')
.should('have.text', settledMarket.tradableInstrument.instrument.code);
// 6001-MARK-071
cy.get(rowSelector)
.first()
.find('[title="Future"]')
.should('have.text', 'Futr');
// 6001-MARK-002
cy.get(rowSelector)
.first()
@@ -0,0 +1,212 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import type { MarketsQuery } from '@vegaprotocol/markets';
import * as Schema from '@vegaprotocol/types';
const rowSelector =
'[data-testid="tab-open-markets"] .ag-center-cols-container .ag-row';
const colInstrumentCode =
'[col-id="tradableInstrument.instrument.code"] [data-testid="market-code"]';
describe('markets all table', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.clearLocalStorage().then(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.mockSubscription();
cy.visit('/#/markets/all');
});
});
it('can see table headers', () => {
cy.wait('@Markets');
cy.wait('@MarketsData');
const headers = [
'Market',
'Description',
'Trading mode',
'Status',
'Successor market',
'Best bid',
'Best offer',
'Mark price',
'Settlement asset',
'',
];
cy.getByTestId('tab-open-markets').within(($headers) => {
cy.wrap($headers)
.get('.ag-header-cell-text')
.each(($header, i) => {
cy.wrap($header).should('have.text', headers[i]);
});
});
});
it('markets tab should be rendered properly', () => {
cy.get('[data-testid="Open markets"]').should(
'have.attr',
'data-state',
'active'
);
cy.get('[data-testid="Proposed markets"]').should(
'have.attr',
'data-state',
'inactive'
);
cy.get('[data-testid="Closed markets"]').should(
'have.attr',
'data-state',
'inactive'
);
});
it('renders markets correctly', () => {
// 6001-MARK-035
cy.get(rowSelector)
.first()
.find(colInstrumentCode)
.should('have.text', 'SOLUSD');
// 6001-MARK-036
cy.get(rowSelector)
.first()
.find('[col-id="tradableInstrument.instrument.name"]')
.should('have.text', 'SUSPENDED MARKET');
// 6001-MARK-037
cy.get(rowSelector)
.first()
.find('[col-id="tradingMode"]')
.should('have.text', 'Continuous');
// 6001-MARK-038
cy.get(rowSelector)
.first()
.find('[col-id="state"]')
.should('have.text', 'Active');
// 6001-MARK-039
cy.get(rowSelector)
.first()
.find('[col-id="data.bestBidPrice"]')
.should('have.text', '0.00');
// 6001-MARK-040
cy.get(rowSelector)
.first()
.find('[col-id="data.bestOfferPrice"]')
.should('have.text', '0.00');
// 6001-MARK-041
cy.get(rowSelector)
.first()
.find('[col-id="data.markPrice"]')
.should('have.text', '84.41');
// 6001-MARK-042
cy.get(rowSelector)
.first()
.find(
'[col-id="tradableInstrument.instrument.product.settlementAsset.symbol"]'
)
.should('have.text', 'XYZalpha');
// 6001-MARK-043
cy.get(rowSelector)
.first()
.find(
'[col-id="tradableInstrument.instrument.product.settlementAsset.symbol"] button'
)
.click();
cy.getByTestId('dialog-title').should('have.text', 'Asset details - tEURO');
cy.getByTestId('close-asset-details-dialog').click();
});
it('can open row actions', () => {
// 6001-MARK-044
cy.get('.ag-pinned-right-cols-container')
.find('[col-id="market-actions"]')
.first()
.find('button')
.click();
// 6001-MARK-045
const dropdownContent = '[data-testid="market-actions-content"]';
const dropdownContentItem = '[role="menuitem"]';
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(0)
// Cannot click the copy button as it falls back to window.prompt, blocking the test.
.should('have.text', 'Copy Market ID');
// 6001-MARK-046
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(1)
.find('a')
.then(($el) => {
const href = $el.attr('href');
expect(/\/markets\/market-1/.test(href || '')).to.equal(true);
})
.should('have.text', 'View on Explorer');
// 6001-MARK-047
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(2)
.should('have.text', 'View settlement asset details');
cy.getByTestId('market-actions-content').click();
});
it('able to open and sort full market list - market page', () => {
// 6001-MARK-064
const ExpectedSortedMarkets = [
'AAPL.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'SOLUSD',
];
cy.get('[data-testid="Open markets"]').click({ force: true });
cy.url().should('eq', Cypress.config('baseUrl') + '/#/markets/all');
cy.contains('AAPL.MF21').should('be.visible');
cy.get('.ag-header-cell-label').contains('Market').click(); // sort by market name
for (let i = 0; i < ExpectedSortedMarkets.length; i++) {
cy.get(`[row-index=${i}]`)
.find(colInstrumentCode)
.should('have.text', ExpectedSortedMarkets[i]);
}
});
it('can drag and drop columns', () => {
// 6001-MARK-065
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
cy.get(colInstrumentCode)
.realMouseDown()
.realMouseMove(700, 15)
.realMouseUp();
cy.get(colInstrumentCode).should(($element) => {
const attributeValue = $element.attr('aria-colindex');
expect(attributeValue).not.to.equal('1');
});
});
});
describe('no open markets', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
const markets: MarketsQuery = {};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Markets', markets);
});
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all');
});
it.skip('can see no markets message', () => {
// 6001-MARK-048
cy.getByTestId('tab-open-markets').should('contain.text', 'No markets');
});
});
@@ -0,0 +1,136 @@
import {
AuctionTrigger,
MarketState,
MarketTradingMode,
} from '@vegaprotocol/types';
describe('markets selector', { tags: '@smoke' }, () => {
const list = 'market-selector-list';
const searchInput = 'search-term';
beforeEach(() => {
cy.window().then((window) => {
window.localStorage.setItem('marketId', 'market-1');
});
cy.setOnBoardingViewed();
cy.mockTradingPage(
MarketState.STATE_ACTIVE,
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.mockSubscription();
cy.visit('/');
cy.wait('@Markets');
cy.wait('@MarketsData');
});
// 6001-MARK-066
it('can open popover to view markets', () => {
cy.getByTestId('market-selector').should('not.exist');
cy.getByTestId('header-title').should('be.visible').click();
cy.getByTestId('market-selector').should('be.visible');
});
// need function keyword as we need 'this' to access market data
it('displays data as expected', () => {
// TODO: load data from mocks in. Using alias and wrap intermittently fails
const data = [
{
code: 'SOLUSD',
markPrice: '84.41',
vol: '0.00',
productType: 'Futr',
},
{
code: 'ETHBTC.QM21',
markPrice: '46,126.90058',
vol: '0.00',
productType: 'Futr',
},
{
code: 'BTCUSD.MF21',
markPrice: '46,126.90058',
vol: '0.00',
productType: 'Futr',
},
{
code: 'AAPL.MF21',
markPrice: '46,126.90058',
vol: '0.00',
productType: 'Futr',
},
];
cy.getByTestId('header-title').should('be.visible').click();
cy.getByTestId(list)
.find('a')
.each((item, i) => {
const market = data[i];
// 6001-MARK-021
// 6001-MARK-022
expect(item.find('h3').text()).equals(
`${market.code} ${market.productType}`
);
expect(
item.find('[data-testid="market-selector-volume"]').text()
).contains(market.vol);
// 6001-MARK-024
expect(
item.find('[data-testid="market-selector-price"]').text()
).contains(market.markPrice);
// 6001-MARK-025
expect(item.find('[data-testid="sparkline-svg"]')).to.not.exist;
});
});
it('can use the filter options', () => {
cy.getByTestId('header-title').should('be.visible').click();
// 6001-MARK-027
// product type
cy.getByTestId('product-Spot').click();
cy.getByTestId(list).contains('Spot markets coming soon.');
cy.getByTestId('product-Perpetual').click();
cy.getByTestId(list).contains('Perpetual markets coming soon.');
cy.getByTestId('product-Future').click();
cy.getByTestId(list).find('a').should('have.length', 4);
// 6001-MARK-029
cy.getByTestId(searchInput).clear().type('btc');
cy.getByTestId(list).find('a').should('have.length', 2);
cy.getByTestId(list).find('a').eq(1).contains('BTCUSD.MF21');
cy.getByTestId(list).find('a').eq(0).contains('ETHBTC.QM21');
cy.getByTestId(searchInput).clear();
cy.getByTestId(list).find('a').should('have.length', 4);
});
it('can sort by by top gaining and top losing market', () => {
cy.getByTestId('header-title').should('be.visible').click();
// 6001-MARK-030
// 6001-MARK-031
// 6001-MARK-032
// 6001-MARK-033
cy.getByTestId(' sort-trigger').click();
cy.getByTestId('sort-item-Gained')
.contains('Top gaining')
.should('be.visible');
cy.getByTestId('sort-item-Lost')
.contains('Top losing')
.should('be.visible');
cy.getByTestId('sort-item-New')
.contains('New markets')
.should('be.visible');
});
it('can filter by settlement asset', () => {
cy.getByTestId('header-title').should('be.visible').click();
// 6001-MARK-028
cy.getByTestId('asset-trigger').click();
cy.getByTestId('asset-id-asset-3').contains('tBTC').click();
cy.getByTestId(list).find('a').should('have.length', 1);
cy.getByTestId(list).find('a').eq(0).contains('ETHBTC.QM21');
});
});
@@ -0,0 +1,233 @@
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-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,30 @@
describe('Settings page', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/');
// Only click if not already active otherwise sidebar will close
cy.get('[data-testid="sidebar-content"]').then(($sidebarContent) => {
if ($sidebarContent.find('h2').text() !== 'Settings') {
cy.get('[data-testid="sidebar"] [data-testid="Settings"]').click();
}
});
});
it('telemetry checkbox should work well', () => {
const telemetrySwitch = '#switch-settings-telemetry-switch';
cy.get(telemetrySwitch).should('have.attr', 'data-state', 'unchecked');
cy.get(telemetrySwitch).click();
cy.get(telemetrySwitch).should('have.attr', 'data-state', 'checked');
cy.reload();
cy.get(telemetrySwitch).should('have.attr', 'data-state', 'checked');
cy.get(telemetrySwitch).click();
cy.get(telemetrySwitch).should('have.attr', 'data-state', 'unchecked');
cy.reload();
cy.get(telemetrySwitch).should('have.attr', 'data-state', 'unchecked');
});
});
@@ -0,0 +1,186 @@
interface ItemInfoType {
name: string;
infoText: string;
}
type CheckMenuItemsFnType = (
triggerSelector: string,
validTexts: string[],
clickItem?: string
) => void;
type CheckMenuItemCheckboxFnType = (
buttonText: string,
items: ItemInfoType[]
) => void;
const menuItemRadio = 'div[role="menuitemradio"]';
const menuItemCheckbox = 'div[role="menuitemcheckbox"]';
const button = 'button';
const indicatorInfo = '.indicator-info-wrapper';
const checkMenuItems: CheckMenuItemsFnType = (
triggerSelector,
validTexts,
clickItem
) => {
cy.get(triggerSelector).click();
cy.get(menuItemRadio)
.should('have.length', validTexts.length)
.each(($el, index) => {
const text = $el.text().trim();
expect(text).to.equal(validTexts[index]);
});
if (clickItem) {
cy.contains(menuItemRadio, clickItem).click();
cy.get(triggerSelector).click();
cy.get(`${menuItemRadio}[data-state="checked"]`)
.invoke('text')
.then((text: string) => {
expect(text.trim()).to.equal(clickItem);
});
}
};
const checkMenuItemCheckbox: CheckMenuItemCheckboxFnType = (
buttonText,
items
) => {
items.forEach((item) => {
cy.contains(button, buttonText).click();
cy.contains(menuItemCheckbox, item.name).click();
});
cy.contains(button, buttonText).click();
cy.get(menuItemCheckbox)
.should('have.length', items.length)
.each(($el, index) => {
const text = $el.text();
expect(text).to.equal(items[index].name);
});
items.forEach((item, index) => {
cy.get(indicatorInfo)
.eq(index + 1)
.invoke('text')
.should('eq', item.infoText);
});
cy.contains(button, buttonText).click({ force: true });
};
function getButtonSelectorByText(text: string): string {
return `${button}[aria-haspopup="menu"]:contains(${text})`;
}
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
describe(
'chart display options',
{ tags: '@smoke', testIsolation: true },
() => {
it('change time interval', () => {
// 6004-CHAR-001
checkMenuItems(
getButtonSelectorByText('Interval:'),
['1m', '5m', '15m', '1H', '6H', '1D'],
'1m'
);
});
it('change display type', () => {
// 6004-CHAR-002
// 6004-CHAR-003
checkMenuItems(
'[aria-label$="chart icon"]',
['Mountain', 'Candlestick', 'Line', 'OHLC'],
'Mountain'
);
});
it('Overlays', () => {
// 6004-CHAR-004
// 6004-CHAR-008
// 6004-CHAR-009
// 6004-CHAR-034
// 6004-CHAR-037
// 6004-CHAR-039
// 6004-CHAR-041
const overlayInfo: ItemInfoType[] = [
{
name: 'Bollinger bands',
infoText: 'Bollinger: Upper 174.78590Lower 173.38014',
},
{
name: 'Envelope',
infoText: 'Envelope: Upper 191.29000Lower 156.51000',
},
{ name: 'EMA', infoText: 'EMA: 174.06793' },
{ name: 'Moving average', infoText: 'Moving average: 174.08302' },
{
name: 'Price monitoring bounds',
infoText:
'Price Monitoring Bounds 1: Min 162.56291Max 182.96869Reference 172.47489',
},
];
checkMenuItemCheckbox('Overlays', overlayInfo);
});
it('Studies', () => {
// 6004-CHAR-005
// 6004-CHAR-006
// 6004-CHAR-007
// 6004-CHAR-042
// 6004-CHAR-045
// 6004-CHAR-047
// 6004-CHAR-049
// 6004-CHAR-051
const studyInfo: ItemInfoType[] = [
{
name: 'Eldar-ray',
infoText: 'Eldar-ray: Bull -0.08376Bear -0.58376',
},
{ name: 'Force index', infoText: 'Force index: 987.48858' },
{ name: 'MACD', infoText: 'MACD: S -0.06420D 0.00359MACD -0.06062' },
{ name: 'RSI', infoText: 'RSI: 47.08648' },
{ name: 'Volume', infoText: 'Volume: 55,000' },
];
cy.get(indicatorInfo).eq(1).realHover();
cy.get('.chart__wrapper [data-testid="split-view-view"]')
.last()
.find('[role="button"][title="Close"]')
.click({ force: true });
cy.get(indicatorInfo).should('have.length', 1);
checkMenuItemCheckbox('Studies', studyInfo);
});
it('price details', () => {
// 6004-CHAR-010
const expectedDateRegex = new RegExp(
/^\d{2}:\d{2} \d{2} [A-Za-z]{3} \d{4}$/
);
const expectedOhlc = `O 173.60000H 174.00000L 173.50000C 173.90000Change 0.60000(0.34%)`;
cy.get(indicatorInfo)
.eq(0)
.invoke('text')
.then((text) => {
const actualDate = text.slice(0, -67);
// eslint-disable-next-line no-console
console.log(actualDate);
const actualOhlc = text.slice(-67);
assert.isTrue(expectedDateRegex.test(actualDate));
assert.strictEqual(actualOhlc, expectedOhlc);
});
});
}
);
@@ -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('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"]')
@@ -54,15 +54,6 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
cy.getByTestId(toggleLimit).next('input').should('be.checked');
cy.getByTestId(orderPriceField).should('have.value', '101');
});
it('sidebar should be open after reload', () => {
cy.mockTradingPage();
cy.getByTestId('deal-ticket-form').should('be.visible');
cy.getByTestId('Order').click();
cy.getByTestId('deal-ticket-form').should('not.exist');
cy.reload();
cy.getByTestId('deal-ticket-form').should('be.visible');
});
});
describe(
@@ -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'
);
@@ -1,87 +0,0 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import {
accountsQuery,
amendGeneralAccountBalance,
amendMarginAccountBalance,
} from '@vegaprotocol/mock';
describe.skip(
'account validation',
{ tags: '@regression', testIsolation: true },
() => {
describe('zero balance error', () => {
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
let accounts = accountsQuery();
accounts = amendMarginAccountBalance(accounts, 'market-0', '1000');
accounts = amendGeneralAccountBalance(accounts, 'market-0', '0');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('should show an error if your balance is zero', () => {
const accounts = accountsQuery();
amendMarginAccountBalance(accounts, 'market-0', '0');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
// 7002-SORD-060
cy.getByTestId('place-order').should('be.enabled');
// 7002-SORD-003
cy.getByTestId('deal-ticket-error-message-zero-balance').should(
'have.text',
'You need ' +
'tDAI' +
' in your wallet to trade in this market. See all your collateral.Make a deposit'
);
cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist');
});
});
describe('not enough balance warning', () => {
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
let accounts = accountsQuery();
accounts = amendMarginAccountBalance(accounts, 'market-0', '1000');
accounts = amendGeneralAccountBalance(accounts, 'market-0', '1');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.get('[data-testid="deal-ticket-form"]').then(($form) => {
if (!$form.length) {
cy.getByTestId('Order').click();
}
});
});
it('should display info and button for deposit', () => {
// 7002-SORD-003
// warning should show immediately
cy.getByTestId('deal-ticket-warning-margin').should(
'contain.text',
'You may not have enough margin available to open this position'
);
cy.getByTestId('deal-ticket-warning-margin').should(
'contain.text',
'You may not have enough margin available to open this position. 5.00 tDAI is currently required. You have only 0.01001 tDAI available.'
);
cy.getByTestId('deal-ticket-deposit-dialog-button').click();
cy.getByTestId('sidebar-content')
.find('h2')
.eq(0)
.should('have.text', 'Deposit');
});
});
}
);
@@ -8,7 +8,7 @@ import {
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { createOrder } from '../support/create-order';
describe.skip(
describe(
'account validation',
{ tags: '@regression', testIsolation: true },
() => {
@@ -222,17 +222,12 @@ describe('subscribe orders', { tags: '@smoke' }, () => {
it('must see a filled order', () => {
// 7002-SORD-046
// 7003-MORD-020
// NOT COVERED: Must be able to see/link to all trades that were created from this order
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_FILLED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Filled');
cy.get('[col-id="market.tradableInstrument.instrument.code"]').contains(
'[title="Future"]',
'Futr'
);
});
it('must see a rejected order', () => {
@@ -1,25 +1,59 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { partyAssetsQuery } from '@vegaprotocol/mock';
import { ledgerEntriesQuery } from '@vegaprotocol/mock';
describe('Portfolio page', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'PartyAssets', partyAssetsQuery());
});
cy.mockTradingPage();
cy.mockGQL((req) => {
aliasGQLQuery(req, 'LedgerEntries', ledgerEntriesQuery());
});
cy.mockSubscription();
cy.setVegaWallet();
});
describe('Ledger entries', () => {
it('Download form should be properly rendered', () => {
// 7007-LEEN-001
it('List should be properly rendered', () => {
cy.visit('/#/portfolio');
cy.getByTestId('"Ledger entries"').click();
const headers = [
'Sender',
'Account type',
'Market',
'Receiver',
'Account type',
'Market',
'Transfer type',
'Quantity',
'Asset',
'Sender account balance',
'Receiver account balance',
'Vega time',
];
cy.getByTestId('tab-ledger-entries').within(($headers) => {
cy.wrap($headers)
.getByTestId('ledger-download-button')
.should('be.visible');
.get('.ag-header-cell-text')
.each(($header, i) => {
cy.wrap($header).should('have.text', headers[i]);
});
});
cy.get(
'[data-testid="tab-ledger-entries"] .ag-center-cols-container .ag-row'
).should('have.length', ledgerEntriesQuery().ledgerEntries.edges.length);
});
it('account filters should be callable', () => {
cy.visit('/#/portfolio');
cy.getByTestId('"Ledger entries"').click();
cy.get('[role="columnheader"][col-id="fromAccountType"]').realHover();
cy.get(
'[role="columnheader"][col-id="fromAccountType"] .ag-header-cell-menu-button'
).click();
cy.get('fieldset.ag-simple-filter-body-wrapper')
.should('be.visible')
.within((fields) => {
cy.wrap(fields).find('label').should('have.length', 18);
});
cy.getByTestId('"Ledger entries"').click();
cy.get('fieldset.ag-simple-filter-body-wrapper').should('not.exist');
});
});
});
+2 -2
View File
@@ -12,14 +12,14 @@ 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
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
-3
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"
@@ -23,7 +21,6 @@ NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
-3
View File
@@ -13,15 +13,12 @@ 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
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
-3
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
@@ -23,7 +21,6 @@ NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
-3
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
@@ -23,7 +21,6 @@ NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
+1 -4
View File
@@ -13,12 +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_METAMASK_SNAPS=true
# NX_PRODUCT_PERPETUALS
+1 -4
View File
@@ -14,15 +14,12 @@ 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
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
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
-4
View File
@@ -15,15 +15,11 @@ 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
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
@@ -94,11 +94,7 @@ const MainGrid = memo(
>
<TradeGridChild>
<Tabs storageKey="console-trade-grid-bottom">
<Tab
id="positions"
name={t('Positions')}
menu={<TradingViews.positions.menu />}
>
<Tab id="positions" name={t('Positions')}>
<TradingViews.positions.component />
</Tab>
<Tab
@@ -17,7 +17,6 @@ import type { OrderContainerProps } from '../../components/orders-container';
import { OrdersContainer } from '../../components/orders-container';
import { StopOrdersContainer } from '../../components/stop-orders-container';
import { AccountsMenu } from '../../components/accounts-menu';
import { PositionsMenu } from '../../components/positions-menu';
type MarketDependantView =
| typeof CandlesChartContainer
@@ -58,11 +57,7 @@ export const TradingViews = {
label: 'Trades',
component: requiresMarket(TradesContainer),
},
positions: {
label: 'Positions',
component: PositionsContainer,
menu: PositionsMenu,
},
positions: { label: 'Positions', component: PositionsContainer },
activeOrders: {
label: 'Active',
component: (props: OrderContainerProps) => (
+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,29 +1,22 @@
import { useDataGridEvents } from '@vegaprotocol/datagrid';
import { t } from '@vegaprotocol/i18n';
import { LedgerExportForm } from '@vegaprotocol/ledger';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { LedgerManager } from '@vegaprotocol/ledger';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useEnvironment } from '@vegaprotocol/environment';
import type { PartyAssetFieldsFragment } from '@vegaprotocol/assets';
import { usePartyAssetsQuery } from '@vegaprotocol/assets';
import type { DataGridSlice } from '../../stores/datagrid-store-slice';
import { createDataGridSlice } from '../../stores/datagrid-store-slice';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export const LedgerContainer = () => {
const VEGA_URL = useEnvironment((store) => store.VEGA_URL);
const { pubKey } = useVegaWallet();
const { data, loading } = usePartyAssetsQuery({
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const assets = (data?.party?.accountsConnection?.edges ?? [])
.map<PartyAssetFieldsFragment>(
(item) => item?.node?.asset ?? ({} as PartyAssetFieldsFragment)
)
.reduce((aggr, item) => {
if ('id' in item && 'symbol' in item) {
aggr[item.id as string] = item.symbol as string;
}
return aggr;
}, {} as Record<string, string>);
const gridStore = useLedgerStore((store) => store.gridStore);
const updateGridStore = useLedgerStore((store) => store.updateGridStore);
const gridStoreCallbacks = useDataGridEvents(gridStore, (colState) => {
updateGridStore(colState);
});
if (!pubKey) {
return (
@@ -33,31 +26,11 @@ export const LedgerContainer = () => {
);
}
if (!VEGA_URL) {
return (
<Splash>
<p>{t('Environment not configured')}</p>
</Splash>
);
}
if (loading) {
return (
<div className="relative flex items-center justify-center w-full h-full">
<Loader />
</div>
);
}
if (!Object.keys(assets).length) {
return (
<Splash>
<p>{t('No ledger entries to export')}</p>
</Splash>
);
}
return (
<LedgerExportForm partyId={pubKey} vegaUrl={VEGA_URL} assets={assets} />
);
return <LedgerManager partyId={pubKey} gridProps={gridStoreCallbacks} />;
};
const useLedgerStore = create<DataGridSlice>()(
persist(createDataGridSlice, {
name: 'vega_ledger_store',
})
);
@@ -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,14 +1,14 @@
import { t } from '@vegaprotocol/i18n';
import uniqBy from 'lodash/uniqBy';
import { type MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
import {
TradingInput,
Input,
TinyScroll,
VegaIcon,
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">
@@ -62,7 +57,7 @@ export const MarketSelector = ({
/>
<div className="text-sm grid grid-cols-[2fr_1fr_1fr] gap-1 ">
<div className="flex-1">
<TradingInput
<Input
onChange={(e) =>
setFilter((curr) => ({ ...curr, searchTerm: e.target.value }))
}
@@ -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 (
@@ -5,7 +5,6 @@ import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { DataGridSlice } from '../../stores/datagrid-store-slice';
import { createDataGridSlice } from '../../stores/datagrid-store-slice';
import type { StateCreator } from 'zustand';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
@@ -14,7 +13,6 @@ export const PositionsContainer = ({ allKeys }: { allKeys?: boolean }) => {
const onMarketClick = useMarketClickHandler(true);
const { pubKey, pubKeys, isReadOnly } = useVegaWallet();
const showClosed = usePositionsStore((store) => store.showClosedMarkets);
const gridStore = usePositionsStore((store) => store.gridStore);
const updateGridStore = usePositionsStore((store) => store.updateGridStore);
const gridStoreCallbacks = useDataGridEvents(gridStore, updateGridStore);
@@ -42,35 +40,12 @@ export const PositionsContainer = ({ allKeys }: { allKeys?: boolean }) => {
onMarketClick={onMarketClick}
isReadOnly={isReadOnly}
gridProps={gridStoreCallbacks}
showClosed={showClosed}
/>
);
};
type PositionsStoreSlice = {
showClosedMarkets: boolean;
toggleClosedMarkets: () => void;
};
const createPositionStoreSlice: StateCreator<PositionsStoreSlice> = (set) => ({
showClosedMarkets: false,
toggleClosedMarkets: () => {
set((curr) => {
return {
showClosedMarkets: !curr.showClosedMarkets,
};
});
},
});
export const usePositionsStore = create<PositionsStoreSlice & DataGridSlice>()(
persist(
(...args) => ({
...createPositionStoreSlice(...args),
...createDataGridSlice(...args),
}),
{
name: 'vega_positions_store',
}
)
const usePositionsStore = create<DataGridSlice>()(
persist(createDataGridSlice, {
name: 'vega_positions_store',
})
);
@@ -1 +0,0 @@
export * from './positions-menu';
@@ -1,18 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
import { usePositionsStore } from '../positions-container';
export const PositionsMenu = () => {
const showClosed = usePositionsStore((store) => store.showClosedMarkets);
const toggle = usePositionsStore((store) => store.toggleClosedMarkets);
return (
<TradingButton
intent={Intent.Primary}
size="extra-small"
data-testid="open-transfer"
onClick={toggle}
>
{showClosed ? t('Hide closed markets') : t('Show closed markets')}
</TradingButton>
);
};
+21 -10
View File
@@ -14,9 +14,12 @@ import { Settings } from '../settings';
import { Tooltip } from '../../components/tooltip';
import { WithdrawContainer } from '../withdraw-container';
import { Routes as AppRoutes } from '../../pages/client-router';
import { persist } from 'zustand/middleware';
import { GetStarted } from '../welcome-dialog';
import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
const STORAGE_KEY = 'vega_sidebar_store';
export enum ViewType {
Order = 'Order',
Info = 'Info',
@@ -299,14 +302,22 @@ export const useSidebar = create<{
init: boolean;
view: SidebarView | null;
setView: (view: SidebarView | null) => void;
}>()((set) => ({
init: true,
view: null,
setView: (x) =>
set(() => {
if (x == null) {
return { view: null, init: false };
}
return { view: x, init: false };
}>()(
persist(
(set) => ({
init: true,
view: null,
setView: (x) =>
set(() => {
if (x == null) {
return { view: null, init: false };
}
return { view: x, init: false };
}),
}),
}));
{
name: STORAGE_KEY,
}
)
);
@@ -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,4 +1,4 @@
import { TradingCheckbox } from '@vegaprotocol/ui-toolkit';
import { Checkbox } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
@@ -7,7 +7,7 @@ export const TelemetryApproval = ({ helpText }: { helpText: string }) => {
return (
<div className="flex flex-col py-3">
<div className="mr-4" role="form">
<TradingCheckbox
<Checkbox
label={<span className="text-lg pl-1">{t('Share usage data')}</span>}
checked={isApproved}
name="telemetry-approval"
@@ -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">
-9
View File
@@ -1,10 +1,7 @@
import { ENV } from '@vegaprotocol/environment';
import {
JsonRpcConnector,
ViewConnector,
InjectedConnector,
SnapConnector,
DEFAULT_SNAP_ID,
} from '@vegaprotocol/wallet';
export const jsonRpc = new JsonRpcConnector();
@@ -18,14 +15,8 @@ if (typeof window !== 'undefined') {
view = new ViewConnector();
}
export const snap = new SnapConnector(
ENV.VEGA_URL ? new URL(ENV.VEGA_URL).origin : undefined,
DEFAULT_SNAP_ID
);
export const Connectors = {
injected,
jsonRpc,
view,
snap,
};
+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;
};
+39 -9
View File
@@ -1,10 +1,40 @@
import { Head, Html, Main, NextScript } from 'next/document';
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<>
<Html>
<Head>
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charSet="utf-8" />
<link
rel="icon"
type="image/x-icon"
href="https://static.vega.xyz/favicon.ico"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Vega Protocol - VEGA Console" />
<meta name="og:type" content="website" />
<meta name="og:url" content="https://console.vega.xyz/" />
<meta name="og:title" content="Vega Protocol - Console" />
<meta name="og:site_name" content="Vega Protocol - Console" />
<meta name="og:image" content="https://static.vega.xyz/favicon.ico" />
<meta
name="twitter:card"
content="https://static.vega.xyz/favicon.ico"
/>
<meta name="twitter:title" content="Vega Protocol - Console" />
<meta name="twitter:description" content="Vega Protocol - Console" />
<meta
name="twitter:image"
content="https://static.vega.xyz/favicon.ico"
/>
<meta name="twitter:image:alt" content="VEGA logo" />
<meta name="twitter:site" content="@vegaprotocol" />
<meta name="description" content="Vega Protocol - Console" />
<link
rel="apple-touch-icon"
content="https://static.vega.xyz/favicon.ico"
@@ -15,6 +45,8 @@ export default function Document() {
as="font"
type="font/woff2"
/>
{/* eslint-disable-next-line @next/next/no-css-tags */}
<link rel="stylesheet" href="/preloader.css" media="all" />
<link
rel="icon"
type="image/x-icon"
@@ -22,12 +54,10 @@ export default function Document() {
/>
<script src="/theme-setter.js" type="text/javascript" async />
</Head>
<Html>
<body className="bg-white dark:bg-vega-cdark-900 text-default font-alpha">
<Main />
<NextScript />
</body>
</Html>
</>
<body className="bg-white dark:bg-vega-cdark-900 text-default font-alpha">
<Main />
<NextScript />
</body>
</Html>
);
}
+1 -57
View File
@@ -1,4 +1,3 @@
import Head from 'next/head';
import { ClientRouter } from './client-router';
/**
@@ -7,60 +6,5 @@ import { ClientRouter } from './client-router';
* have to serve a static site via next export
*/
export default function Index() {
return (
<>
<Head>
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charSet="utf-8" />
<link
rel="icon"
type="image/x-icon"
href="https://static.vega.xyz/favicon.ico"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Vega Protocol - VEGA Console" />
<meta name="og:type" content="website" />
<meta name="og:url" content="https://console.vega.xyz/" />
<meta name="og:title" content="Vega Protocol - Console" />
<meta name="og:site_name" content="Vega Protocol - Console" />
<meta name="og:image" content="https://static.vega.xyz/favicon.ico" />
<meta
name="twitter:card"
content="https://static.vega.xyz/favicon.ico"
/>
<meta name="twitter:title" content="Vega Protocol - Console" />
<meta name="twitter:description" content="Vega Protocol - Console" />
<meta
name="twitter:image"
content="https://static.vega.xyz/favicon.ico"
/>
<meta name="twitter:image:alt" content="VEGA logo" />
<meta name="twitter:site" content="@vegaprotocol" />
<meta name="description" content="Vega Protocol - Console" />
<link
rel="apple-touch-icon"
content="https://static.vega.xyz/favicon.ico"
/>
<link
rel="preload"
href="https://static.vega.xyz/AlphaLyrae-Medium.woff2"
as="font"
type="font/woff2"
/>
{/* eslint-disable-next-line @next/next/no-css-tags */}
<link rel="stylesheet" href="/preloader.css" media="all" />
<link
rel="icon"
type="image/x-icon"
href="https://static.vega.xyz/favicon.ico"
/>
<script src="/theme-setter.js" type="text/javascript" async />
</Head>
<ClientRouter />
</>
);
return <ClientRouter />;
}
+1 -21
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;
}
@@ -64,10 +60,6 @@ html.dark {
html [data-theme='dark'],
html [data-theme='light'] {
/* fonts */
--pennant-font-family-base: theme(fontFamily.alpha);
--pennant-font-family-monospace: theme(fontFamily.mono);
/* sell candles only use stroke as the candle is solid (without border) */
--pennant-color-sell-stroke: theme(colors.market.red.DEFAULT);
@@ -155,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 */
@@ -217,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) => {
+23 -31
View File
@@ -9,13 +9,13 @@ import {
import { t } from '@vegaprotocol/i18n';
import {
Button,
TradingFormGroup,
TradingInput,
TradingInputError,
TradingRichSelect,
TradingSelect,
FormGroup,
Input,
InputError,
RichSelect,
Select,
Tooltip,
TradingCheckbox,
Checkbox,
} from '@vegaprotocol/ui-toolkit';
import type { Transfer } from '@vegaprotocol/wallet';
import { normalizeTransfer } from '@vegaprotocol/wallet';
@@ -130,16 +130,12 @@ export const TransferForm = ({
className="text-sm"
data-testid="transfer-form"
>
<TradingFormGroup label="Vega key" labelFor="to-address">
<FormGroup label="Vega key" labelFor="to-address">
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('toAddress', '')}
select={
<TradingSelect
{...register('toAddress')}
id="to-address"
defaultValue=""
>
<Select {...register('toAddress')} id="to-address" defaultValue="">
<option value="" disabled={true}>
{t('Please select')}
</option>
@@ -151,10 +147,10 @@ export const TransferForm = ({
{pk}
</option>
))}
</TradingSelect>
</Select>
}
input={
<TradingInput
<Input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to-address"
@@ -175,12 +171,12 @@ export const TransferForm = ({
}
/>
{errors.toAddress?.message && (
<TradingInputError forInput="to-address">
<InputError forInput="to-address">
{errors.toAddress.message}
</TradingInputError>
</InputError>
)}
</TradingFormGroup>
<TradingFormGroup label="Asset" labelFor="asset">
</FormGroup>
<FormGroup label="Asset" labelFor="asset">
<Controller
control={control}
name="asset"
@@ -190,7 +186,7 @@ export const TransferForm = ({
},
}}
render={({ field }) => (
<TradingRichSelect
<RichSelect
data-testid="select-asset"
id={field.name}
name={field.name}
@@ -212,17 +208,15 @@ export const TransferForm = ({
}
/>
))}
</TradingRichSelect>
</RichSelect>
)}
/>
{errors.asset?.message && (
<TradingInputError forInput="asset">
{errors.asset.message}
</TradingInputError>
<InputError forInput="asset">{errors.asset.message}</InputError>
)}
</TradingFormGroup>
<TradingFormGroup label="Amount" labelFor="amount">
<TradingInput
</FormGroup>
<FormGroup label="Amount" labelFor="amount">
<Input
id="amount"
autoComplete="off"
appendElement={
@@ -245,13 +239,11 @@ export const TransferForm = ({
})}
/>
{errors.amount?.message && (
<TradingInputError forInput="amount">
{errors.amount.message}
</TradingInputError>
<InputError forInput="amount">{errors.amount.message}</InputError>
)}
</TradingFormGroup>
</FormGroup>
<div className="mb-4">
<TradingCheckbox
<Checkbox
name="include-transfer-fee"
disabled={!transferAmount}
label={
+1 -5
View File
@@ -1,8 +1,4 @@
{
"name": "@vegaprotocol/announcements",
"version": "0.0.2",
"peerDependencies": {
"react": "18.2.0",
"react-dom": "18.2.0"
}
"version": "0.0.2"
}
-23
View File
@@ -24,26 +24,3 @@ query Assets {
}
}
}
fragment PartyAssetFields on Asset {
id
name
symbol
status
}
query PartyAssets($partyId: ID!) {
party(id: $partyId) {
id
accountsConnection {
edges {
node {
type
asset {
...PartyAssetFields
}
}
}
}
}
}
+1 -63
View File
@@ -10,15 +10,6 @@ export type AssetsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type AssetsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, quantum: string, status: Types.AssetStatus, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string, lifetimeLimit: string, withdrawThreshold: string } } } | null> | null } | null };
export type PartyAssetFieldsFragment = { __typename?: 'Asset', id: string, name: string, symbol: string, status: Types.AssetStatus };
export type PartyAssetsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type PartyAssetsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, asset: { __typename?: 'Asset', id: string, name: string, symbol: string, status: Types.AssetStatus } } } | null> | null } | null } | null };
export const AssetListFieldsFragmentDoc = gql`
fragment AssetListFields on Asset {
id
@@ -37,14 +28,6 @@ export const AssetListFieldsFragmentDoc = gql`
status
}
`;
export const PartyAssetFieldsFragmentDoc = gql`
fragment PartyAssetFields on Asset {
id
name
symbol
status
}
`;
export const AssetsDocument = gql`
query Assets {
assetsConnection {
@@ -82,49 +65,4 @@ export function useAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<Ass
}
export type AssetsQueryHookResult = ReturnType<typeof useAssetsQuery>;
export type AssetsLazyQueryHookResult = ReturnType<typeof useAssetsLazyQuery>;
export type AssetsQueryResult = Apollo.QueryResult<AssetsQuery, AssetsQueryVariables>;
export const PartyAssetsDocument = gql`
query PartyAssets($partyId: ID!) {
party(id: $partyId) {
id
accountsConnection {
edges {
node {
type
asset {
...PartyAssetFields
}
}
}
}
}
}
${PartyAssetFieldsFragmentDoc}`;
/**
* __usePartyAssetsQuery__
*
* To run a query within a React component, call `usePartyAssetsQuery` and pass it any options that fit your needs.
* When your component renders, `usePartyAssetsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = usePartyAssetsQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function usePartyAssetsQuery(baseOptions: Apollo.QueryHookOptions<PartyAssetsQuery, PartyAssetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<PartyAssetsQuery, PartyAssetsQueryVariables>(PartyAssetsDocument, options);
}
export function usePartyAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<PartyAssetsQuery, PartyAssetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<PartyAssetsQuery, PartyAssetsQueryVariables>(PartyAssetsDocument, options);
}
export type PartyAssetsQueryHookResult = ReturnType<typeof usePartyAssetsQuery>;
export type PartyAssetsLazyQueryHookResult = ReturnType<typeof usePartyAssetsLazyQuery>;
export type PartyAssetsQueryResult = Apollo.QueryResult<PartyAssetsQuery, PartyAssetsQueryVariables>;
export type AssetsQueryResult = Apollo.QueryResult<AssetsQuery, AssetsQueryVariables>;
+3 -3
View File
@@ -1,4 +1,4 @@
import { TradingOption } from '@vegaprotocol/ui-toolkit';
import { Option } from '@vegaprotocol/ui-toolkit';
import type { AssetFieldsFragment } from './__generated__/Asset';
import classNames from 'classnames';
import { t } from '@vegaprotocol/i18n';
@@ -28,7 +28,7 @@ export const Balance = ({
export const AssetOption = ({ asset, balance }: AssetOptionProps) => {
return (
<TradingOption key={asset.id} value={asset.id}>
<Option key={asset.id} value={asset.id}>
<div className="flex flex-col items-start">
<div className="flex flex-row align-baseline gap-2">
<span>{asset.name}</span>{' '}
@@ -49,6 +49,6 @@ export const AssetOption = ({ asset, balance }: AssetOptionProps) => {
</span>
</div>
</div>
</TradingOption>
</Option>
);
};
-47
View File
@@ -1,47 +0,0 @@
import merge from 'lodash/merge';
import type { PartyAssetsQuery } from './__generated__/Assets';
import * as Types from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
export const partyAssetsQuery = (
override?: PartialDeep<PartyAssetsQuery>
): PartyAssetsQuery => {
const defaultAssets: PartyAssetsQuery = {
party: {
__typename: 'Party',
id: 'partyId',
accountsConnection: {
edges: partyAccountFields.map((node) => ({
__typename: 'AccountEdge',
node,
})),
},
},
};
return merge(defaultAssets, override);
};
const partyAccountFields = [
{
__typename: 'AccountBalance',
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
asset: {
__typename: 'Asset',
id: 'asset-id',
symbol: 'tEURO',
name: 'Euro',
status: Types.AssetStatus.STATUS_ENABLED,
},
},
{
__typename: 'AccountBalance',
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
asset: {
__typename: 'Asset',
id: 'asset-id-2',
symbol: 'tDAI',
name: 'DAI',
status: Types.AssetStatus.STATUS_ENABLED,
},
},
] as const;
+1 -1
View File
@@ -2,7 +2,6 @@
export * from '../accounts/src/lib/accounts.mock';
export * from '../assets/src/lib/asset.mock';
export * from '../assets/src/lib/assets.mock';
export * from '../assets/src/lib/party-assets.mock';
export * from '../candles-chart/src/lib/candles.mock';
export * from '../candles-chart/src/lib/chart.mock';
export * from '../deal-ticket/src/hooks/estimate-order.mock';
@@ -11,6 +10,7 @@ export * from '../environment/src/utils/node.mock';
export * from '../environment/src/components/node-guard/node-guard.mock';
export * from '../fills/src/lib/fills.mock';
export * from '../proposals/src/lib/proposals-data-provider/proposals.mock';
export * from '../ledger/src/lib/ledger-entries.mock';
export * from '../market-depth/src/lib/market-depth.mock';
export * from '../markets/src/lib/components/market-info/market-info.mock';
export * from '../markets/src/lib/market-candles.mock';
@@ -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);
@@ -15,7 +15,7 @@ import {
} from 'date-fns';
import { formatForInput } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { TradingInputError } from '@vegaprotocol/ui-toolkit';
import { InputError } from '@vegaprotocol/ui-toolkit';
const defaultValue: Schema.DateRange = {};
export interface DateRangeFilterProps extends IFilterParams {
@@ -195,7 +195,7 @@ export const DateRangeFilter = forwardRef(
}, [value, props]);
const notification = useMemo(() => {
const not = error ? <TradingInputError>{error}</TradingInputError> : null;
const not = error ? <InputError>{error}</InputError> : null;
return (
<div className="ag-filter-apply-panel flex min-h-[2rem]">{not}</div>
);
@@ -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,114 @@
import { FormGroup, Input, InputError } 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 (
<InputError testId="deal-ticket-error-message-size-limit">
{sizeError}
</InputError>
);
}
if (priceError) {
return (
<InputError testId="deal-ticket-error-message-price-limit">
{priceError}
</InputError>
);
}
return null;
};
return (
<div className="mb-2">
<div className="flex items-start gap-4">
<div className="flex-1">
<FormGroup
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 }) => (
<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>
</div>
<div className="pt-7 leading-10">@</div>
<div className="flex-1">
<FormGroup
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 }) => (
<Input
id="input-price-quote"
className="w-full"
type="number"
step={priceStep}
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
{...field}
/>
)}
/>
</FormGroup>
</div>
</div>
{renderError()}
</div>
);
};
@@ -0,0 +1,97 @@
import {
addDecimalsFormatNumber,
toDecimal,
validateAmount,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { Input, InputError, 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-sm">{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 }) => (
<Input
id="input-order-size-market"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
{...field}
/>
)}
/>
</div>
<div className="pt-7 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-7': !inAuction })}
>
{priceFormatted && quoteName ? (
<>
~{priceFormatted} {quoteName}
</>
) : (
'-'
)}
</div>
</div>
</div>
{sizeError && (
<InputError
intent="danger"
testId="deal-ticket-error-message-size-market"
>
{sizeError}
</InputError>
)}
</div>
);
};
@@ -4,9 +4,9 @@ import type { OrderFormValues } from '../../hooks/use-form-values';
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
TradingFormGroup,
TradingInput,
TradingInputError,
FormGroup,
Input,
InputError,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
@@ -32,9 +32,9 @@ export const DealTicketSizeIceberg = ({
const renderPeakSizeError = () => {
if (peakSizeError) {
return (
<TradingInputError testId="deal-ticket-peak-error-message-size-limit">
<InputError testId="deal-ticket-peak-error-message-size-limit">
{peakSizeError}
</TradingInputError>
</InputError>
);
}
@@ -44,9 +44,9 @@ export const DealTicketSizeIceberg = ({
const renderMinimumSizeError = () => {
if (minimumVisibleSizeError) {
return (
<TradingInputError testId="deal-ticket-minimum-error-message-size-limit">
<InputError testId="deal-ticket-minimum-error-message-size-limit">
{minimumVisibleSizeError}
</TradingInputError>
</InputError>
);
}
@@ -57,7 +57,7 @@ export const DealTicketSizeIceberg = ({
<div className="mb-2">
<div className="flex items-center gap-4">
<div className="flex-1">
<TradingFormGroup
<FormGroup
label={
<Tooltip
description={
@@ -93,7 +93,7 @@ export const DealTicketSizeIceberg = ({
validate: validateAmount(sizeStep, 'peakSize'),
}}
render={({ field }) => (
<TradingInput
<Input
id="input-order-peak-size"
className="w-full"
type="number"
@@ -106,14 +106,14 @@ export const DealTicketSizeIceberg = ({
/>
)}
/>
</TradingFormGroup>
</FormGroup>
</div>
<div className="flex-0 items-center">
<div className="flex"></div>
<div className="flex"></div>
</div>
<div className="flex-1">
<TradingFormGroup
<FormGroup
label={
<Tooltip
description={
@@ -151,7 +151,7 @@ export const DealTicketSizeIceberg = ({
validate: validateAmount(sizeStep, 'minimumVisibleSize'),
}}
render={({ field }) => (
<TradingInput
<Input
id="input-order-minimum-size"
className="w-full"
type="number"
@@ -164,7 +164,7 @@ export const DealTicketSizeIceberg = ({
/>
)}
/>
</TradingFormGroup>
</FormGroup>
</div>
</div>
{renderPeakSizeError()}
@@ -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,26 +2,24 @@ 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,
Radio,
RadioGroup,
Input,
Checkbox,
FormGroup,
InputError,
Select,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { getDerivedPrice } from '@vegaprotocol/markets';
import type { Market } from '@vegaprotocol/markets';
import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
import { t } from '@vegaprotocol/i18n';
import { ExpirySelector } from './expiry-selector';
import { SideSelector } from './side-selector';
@@ -36,10 +34,10 @@ import { TypeToggle } from './type-selector';
import {
useDealTicketFormValues,
DealTicketType,
type StopOrderFormValues,
dealTicketTypeToOrderType,
isStopOrderType,
} from '../../hooks/use-form-values';
import type { StopOrderFormValues } from '../../hooks/use-form-values';
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-submission';
import { DealTicketButton } from './deal-ticket-button';
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
@@ -51,8 +49,6 @@ export interface StopOrderProps {
submit: (order: StopOrdersSubmission) => void;
}
const trailingPercentOffsetStep = '0.1';
const getDefaultValues = (
type: Schema.OrderType,
storedValues?: Partial<StopOrderFormValues>
@@ -66,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);
@@ -528,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;
@@ -578,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',
@@ -616,110 +199,286 @@ 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 justify-end pb-3 gap-2">
<ReduceOnly />
</div>
<hr className="mb-4 border-vega-clight-500 dark:border-vega-cdark-500" />
<div className="flex justify-between pb-2 gap-2">
<FormGroup 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>
}
/>
<RadioGroup
name="triggerDirection"
onChange={onChange}
value={value}
orientation="horizontal"
className="mb-2"
>
<Radio
value={
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_RISES_ABOVE
}
id="triggerDirection-risesAbove"
label={'Rises above'}
/>
<Radio
value={
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_FALLS_BELOW
}
id="triggerDirection-fallsBelow"
label={'Falls below'}
/>
</RadioGroup>
);
}}
/>
</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;
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">
<Input
data-testid="triggerPrice"
type="number"
step={priceStep}
appendElement={asset.symbol}
value={value || ''}
{...props}
/>
<Radio
value={Schema.OrderType.TYPE_LIMIT}
id={`ocoTypeLimit`}
label={'Limit'}
</div>
);
}}
/>
{errors.triggerPrice && (
<InputError testId="stop-order-error-message-trigger-price">
{errors.triggerPrice.message}
</InputError>
)}
</div>
)}
{!isPriceTrigger && (
<div className="mb-2">
<Controller
name="triggerTrailingPercentOffset"
control={control}
rules={{
required: t('You need provide a trailing percent offset'),
min: {
value: trailingPercentOffsetStep,
message: t(
'Trailing percent offset cannot be lower than ' +
trailingPercentOffsetStep
),
},
max: {
value: '99.9',
message: t(
'Trailing percent offset cannot be higher than 99.9'
),
},
validate: validateAmount(
trailingPercentOffsetStep,
'Trailing percentage offset'
),
}}
render={({ field }) => {
const { value, ...props } = field;
return (
<div className="mb-2">
<Input
type="number"
step={trailingPercentOffsetStep}
appendElement="%"
data-testid="triggerTrailingPercentOffset"
value={value || ''}
{...props}
/>
</RadioGroup>
</div>
);
}}
/>
{errors.triggerTrailingPercentOffset && (
<InputError testId="stop-order-error-message-trigger-trailing-percent-offset">
{errors.triggerTrailingPercentOffset.message}
</InputError>
)}
</div>
)}
<Controller
name="triggerType"
control={control}
rules={{ deps: ['triggerTrailingPercentOffset', 'triggerPrice'] }}
render={({ field }) => {
const { onChange, value } = field;
return (
<RadioGroup
onChange={onChange}
value={value}
orientation="horizontal"
>
<Radio value="price" id="triggerType-price" label={'Price'} />
<Radio
value="trailingPercentOffset"
id="triggerType-trailingPercentOffset"
label={'Trailing Percent Offset'}
/>
</RadioGroup>
);
}}
/>
</FormGroup>
<div className="mb-2">
<div className="flex items-start gap-4">
<FormGroup
labelFor="input-price-quote"
label={t(`Size`)}
className="!mb-0 flex-1"
>
<Controller
name="size"
control={control}
rules={{
required: t('You need to provide a size'),
min: {
value: sizeStep,
message: t('Size cannot be lower than ' + sizeStep),
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={({ field }) => {
const { value, ...props } = field;
return (
<Input
id="order-size"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
value={value || ''}
{...props}
/>
);
}}
/>
</FormGroup>
<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 justify-end mb-2 gap-2">
<ReduceOnly />
<div className="pt-7 leading-10">@</div>
<div className="flex-1">
{type === Schema.OrderType.TYPE_LIMIT ? (
<FormGroup
labelFor="input-price-quote"
label={t(`Price (${quoteName})`)}
labelAlign="right"
className="!mb-0"
>
<Controller
name="price"
control={control}
rules={{
deps: 'type',
required: t('You need provide a price'),
min: {
value: priceStep,
message: t('Price cannot be lower than ' + priceStep),
},
validate: validateAmount(priceStep, 'Price'),
}}
render={({ field }) => {
const { value, ...props } = field;
return (
<Input
id="input-price-quote"
className="w-full"
type="number"
step={priceStep}
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
value={value || ''}
{...props}
/>
);
}}
/>
</FormGroup>
) : (
<div
className="text-sm text-right pt-7 leading-10"
data-testid="price"
>
{priceFormatted && quoteName
? `~${priceFormatted} ${quoteName}`
: '-'}
</div>
)}
</div>
</>
)}
</div>
{errors.size && (
<InputError testId="stop-order-error-message-size">
{errors.size.message}
</InputError>
)}
{!errors.size &&
errors.price &&
type === Schema.OrderType.TYPE_LIMIT && (
<InputError testId="stop-order-error-message-price">
{errors.price.message}
</InputError>
)}
</div>
<div className="mb-2">
<FormGroup
label={t('Time in force')}
labelFor="select-time-in-force"
compact={true}
>
<Controller
name="timeInForce"
control={control}
render={({ field }) => (
<Select
id="select-time-in-force"
className="w-full"
data-testid="order-tif"
{...field}
>
<option
key={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
value={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
>
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_IOC)}
</option>
<option
key={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
value={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
>
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_FOK)}
</option>
</Select>
)}
/>
</FormGroup>
{errors.timeInForce && (
<InputError testId="stop-error-message-tif">
{errors.timeInForce.message}
</InputError>
)}
</div>
<div className="flex gap-2 pb-2 justify-between">
<Controller
name="expire"
control={control}
@@ -727,41 +486,39 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
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);
}}
onCheckedChange={onCheckedChange}
checked={value}
name="expire"
label={t('Expire')}
label={<span className="text-xs">{t('Expire')}</span>}
/>
);
}}
/>
<Checkbox
name="reduce-only"
checked={true}
disabled={true}
label={
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
<span className="text-xs">{t('Reduce only')}</span>
</Tooltip>
}
/>
</div>
{expire && (
<>
<FormGroup label={t('Strategy')} labelFor="expiryStrategy">
<FormGroup
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"
>
<RadioGroup orientation="horizontal" {...field}>
<Radio
disabled={oco}
value={
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
}
@@ -780,12 +537,11 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
}}
/>
</FormGroup>
<div className="mb-4">
<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,
Checkbox,
InputError,
Intent,
Notification,
Tooltip,
@@ -29,15 +28,11 @@ 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';
import {
validateExpiration,
validateMarketState,
@@ -57,6 +52,8 @@ import {
useMarketAccountBalance,
useAccountBalance,
} from '@vegaprotocol/accounts';
import { OrderType } from '@vegaprotocol/types';
import { useDataProvider } from '@vegaprotocol/data-provider';
import {
DealTicketType,
@@ -67,7 +64,6 @@ import type { OrderFormValues } from '../../hooks/use-form-values';
import { useDealTicketFormValues } from '../../hooks/use-form-values';
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
import noop from 'lodash/noop';
import { isNonPersistentOrder } from '../../utils/time-in-force-persistance';
export const REDUCE_ONLY_TOOLTIP =
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.';
@@ -172,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;
@@ -227,8 +222,8 @@ export const DealTicket = ({
});
const openVolume = useOpenVolume(pubKey, market.id) ?? '0';
const orders = activeOrders
? activeOrders.map<Schema.OrderInfo>((order) => ({
isMarketOrder: order.type === Schema.OrderType.TYPE_MARKET,
? activeOrders.map<OrderInfo>((order) => ({
isMarketOrder: order.type === OrderType.TYPE_MARKET,
price: order.price,
remaining: order.remaining,
side: order.side,
@@ -236,7 +231,7 @@ export const DealTicket = ({
: [];
if (normalizedOrder) {
orders.push({
isMarketOrder: normalizedOrder.type === Schema.OrderType.TYPE_MARKET,
isMarketOrder: normalizedOrder.type === OrderType.TYPE_MARKET,
price: normalizedOrder.price ?? '0',
remaining: normalizedOrder.size,
side: normalizedOrder.side,
@@ -304,10 +299,12 @@ export const DealTicket = ({
pubKey,
]);
const nonPersistentOrder = isNonPersistentOrder(timeInForce);
const disablePostOnlyCheckbox = nonPersistentOrder;
const disableReduceOnlyCheckbox = !nonPersistentOrder;
const disableIcebergCheckbox = nonPersistentOrder;
const disablePostOnlyCheckbox = [
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
].includes(timeInForce);
const disableReduceOnlyCheckbox = !disablePostOnlyCheckbox;
const onSubmit = useCallback(
(formValues: OrderFormValues) => {
@@ -335,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={
@@ -373,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}
@@ -462,25 +388,7 @@ export const DealTicket = ({
<TimeInForceSelector
value={field.value}
orderType={type}
onSelect={(value) => {
// If GTT is selected and no expiresAt time is set, or its
// behind current time then reset the value to current time
if (
value === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT &&
(!expiresAt || new Date(expiresAt).getTime() < Date.now())
) {
setValue('expiresAt', formatForInput(new Date()), {
shouldValidate: true,
});
}
// iceberg orders must be persistent orders, so if user
// switches to to a non persisten tif value, remove iceberg selection
if (iceberg && isNonPersistentOrder(value)) {
setValue('iceberg', false);
}
field.onChange(value);
}}
onSelect={field.onChange}
market={market}
marketData={marketData}
errorMessage={errors.timeInForce?.message}
@@ -493,7 +401,6 @@ export const DealTicket = ({
name="expiresAt"
control={control}
rules={{
required: t('You need provide a expiry time/date'),
validate: validateExpiration,
}}
render={({ field }) => (
@@ -505,7 +412,7 @@ export const DealTicket = ({
)}
/>
)}
<div className="flex justify-between pb-2 gap-2">
<div className="flex gap-2 pb-2 justify-between">
<Controller
name="postOnly"
control={control}
@@ -571,7 +478,7 @@ export const DealTicket = ({
</div>
{type === Schema.OrderType.TYPE_LIMIT && (
<>
<div className="flex justify-between pb-2 gap-2">
<div className="flex gap-2 pb-2 justify-between">
<Controller
name="iceberg"
control={control}
@@ -580,7 +487,6 @@ export const DealTicket = ({
name="iceberg"
checked={field.value}
onCheckedChange={field.onChange}
disabled={disableIcebergCheckbox}
label={
<Tooltip
description={
@@ -1,8 +1,4 @@
import {
TradingFormGroup,
TradingInput,
TradingInputError,
} from '@vegaprotocol/ui-toolkit';
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
import { formatForInput } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useRef } from 'react';
@@ -18,30 +14,29 @@ export const ExpirySelector = ({
onSelect,
errorMessage,
}: ExpirySelectorProps) => {
const minDateRef = useRef(new Date());
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(minDateRef.current)}
hasError={!!errorMessage}
/>
{errorMessage && (
<TradingInputError testId="deal-ticket-error-message-expiry">
{errorMessage}
</TradingInputError>
)}
</TradingFormGroup>
</div>
<FormGroup
label={t('Expiry time/date')}
labelFor="expiration"
compact={true}
>
<Input
data-testid="date-picker-field"
id="expiration"
type="datetime-local"
value={dateFormatted}
onChange={(e) => onSelect(e.target.value)}
min={minDate}
/>
{errorMessage && (
<InputError testId="deal-ticket-error-message-expiry">
{errorMessage}
</InputError>
)}
</FormGroup>
);
};
@@ -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';
@@ -1,7 +1,7 @@
import {
TradingFormGroup,
TradingInputError,
TradingSelect,
FormGroup,
InputError,
Select,
Tooltip,
SimpleGrid,
} from '@vegaprotocol/ui-toolkit';
@@ -90,34 +90,31 @@ export const TimeInForceSelector = ({
};
return (
<div className="mb-4">
<TradingFormGroup
label={t('Time in force')}
labelFor="select-time-in-force"
compact={true}
<FormGroup
label={t('Time in force')}
labelFor="select-time-in-force"
compact={true}
>
<Select
id="select-time-in-force"
value={value}
onChange={(e) => {
onSelect(e.target.value as Schema.OrderTimeInForce);
}}
className="w-full"
data-testid="order-tif"
>
<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>
))}
</Select>
{errorMessage && (
<InputError testId="deal-ticket-error-message-tif">
{renderError(errorMessage)}
</InputError>
)}
</FormGroup>
);
};
@@ -1,5 +1,5 @@
import {
TradingInputError,
InputError,
SimpleGrid,
Tooltip,
TradingDropdown,
@@ -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,
}
@@ -178,9 +178,9 @@ export const TypeSelector = ({
value={value}
/>
{errorMessage && (
<TradingInputError testId="deal-ticket-error-message-type">
<InputError testId="deal-ticket-error-message-type">
{renderError(errorMessage as MarketModeValidationType)}
</TradingInputError>
</InputError>
)}
</>
);

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