Compare commits

..
Author SHA1 Message Date
Matthew Russell 8d0cc27401 chore: update pr template to not close issues 2023-08-29 07:19:59 -07:00
171 changed files with 2703 additions and 3691 deletions
-1
View File
@@ -15,7 +15,6 @@ on:
- types
- utils
- i18n
- wallet
jobs:
publish:
+3
View File
@@ -3,3 +3,6 @@
# Lint commit messages to ensure they follow conventional commit standards
yarn commitlint --edit "${1}"
# Lint all staged files
yarn lint-staged
-5
View File
@@ -1,5 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Lint all staged files
yarn lint-staged
-8
View File
@@ -1,8 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Lint all staged files
yarn nx format:check
# Test all projects with changes
yarn nx affected -t test --exclude trading
@@ -1,68 +0,0 @@
context('Oracle page', { tags: '@smoke' }, () => {
describe('Verify elements on page', () => {
before('create market and navigate to oracle page', () => {
cy.createMarket();
cy.visit('/oracles');
});
it('should see oracle data', () => {
cy.getByTestId('oracle-details').should('have.length.at.least', 2);
cy.getByTestId('oracle-details')
.should('exist')
.eq(0)
.within(() => {
cy.get('tr')
.eq(0)
.within(() => {
cy.get('th').should('have.text', 'ID');
cy.get('a').invoke('text').should('have.length', 64);
cy.get('a')
.should('have.attr', 'href')
.and('contain', '/oracles/');
});
cy.get('tr')
.eq(1)
.within(() => {
cy.get('th').should('have.text', 'Type');
cy.get('td').should('have.text', 'External data');
});
cy.get('tr')
.eq(2)
.within(() => {
cy.get('th').should('have.text', 'Signer');
cy.getByTestId('keytype').should('have.text', 'Vega');
cy.get('a').invoke('text').should('have.length', 64);
cy.get('a')
.should('have.attr', 'href')
.and('contain', '/parties/');
});
cy.get('tr')
.eq(3)
.within(() => {
cy.get('th').should('have.text', 'Settlement for');
cy.get('a').invoke('text').should('have.length', 64);
cy.get('a')
.should('have.attr', 'href')
.and('contain', '/markets/');
});
cy.get('tr')
.eq(4)
.within(() => {
cy.get('th').should('have.text', 'Matched data');
cy.get('td').should('have.text', '❌');
});
cy.get('details')
.eq(0)
.within(() => {
cy.contains('Filter').click();
cy.get('.language-json').should('exist');
});
cy.get('details')
.eq(1)
.within(() => {
cy.contains('JSON').click();
cy.get('.language-json').should('exist');
});
});
});
});
});
@@ -1,12 +1,10 @@
import { t } from '@vegaprotocol/i18n';
import type { components } from '../../../../../types/explorer';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import SizeInMarket from '../../../size-in-market/size-in-market';
export interface TxDetailsOrderIcebergDetailsProps {
iceberg: components['schemas']['v1IcebergOpts'];
size: components['schemas']['v1OrderSubmission']['size'];
marketId?: string;
}
/**
@@ -30,7 +28,6 @@ export interface TxDetailsOrderIcebergDetailsProps {
export const TxOrderIcebergDetails = ({
iceberg,
size,
marketId,
}: TxDetailsOrderIcebergDetailsProps) => {
return (
<div
@@ -39,28 +36,15 @@ export const TxOrderIcebergDetails = ({
>
<Tooltip description={t('Iceberg: Minimum visible size')}>
<span className="align-bottom text-vega-orange-650">
{marketId ? (
<SizeInMarket
size={iceberg.minimumVisibleSize}
marketId={marketId}
/>
) : (
iceberg.minimumVisibleSize
)}
{iceberg.minimumVisibleSize || '-'}
</span>
</Tooltip>
<Tooltip description={t('Iceberg: Total size')}>
<span className="text-sm text-vega-blue-600 mx-3">
{marketId ? <SizeInMarket size={size} marketId={marketId} /> : size}
</span>
<span className="text-sm text-vega-blue-600 mx-3">{size}</span>
</Tooltip>
<Tooltip description={t('Iceberg: Visible peak')}>
<span className="align-top text-vega-yellow-600">
{marketId ? (
<SizeInMarket size={iceberg.peakSize} marketId={marketId} />
) : (
iceberg.peakSize
)}
{iceberg.peakSize || '-'}
</span>
</Tooltip>
</div>
@@ -81,11 +81,7 @@ export const TxDetailsOrder = ({
<TableRow modifier="bordered">
<TableCell>{t('Iceberg details')}</TableCell>
<TableCell>
<TxOrderIcebergDetails
iceberg={iceberg}
size={size}
marketId={marketId}
/>
<TxOrderIcebergDetails iceberg={iceberg} size={size} />
</TableCell>
</TableRow>
) : null}
@@ -38,12 +38,7 @@ const Oracles = () => {
const dataConnection = o?.node.dataConnection;
return (
<div
id={id}
key={id}
className="mb-10"
data-testid="oracle-details"
>
<div id={id} key={id} className="mb-10">
<OracleDetails
id={id}
dataSource={o?.node}
-4
View File
@@ -20,10 +20,6 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_VEGA_REST_URL=http://localhost:3008/api/v2/
NX_SUCCESSOR_MARKETS=true
NX_VEGA_EXPLORER_URL=https://explorer.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
#Test configuration variables
CYPRESS_FAIRGROUND=false
CYPRESS_VEGA_URL=http://localhost:3008/graphql
+1 -3
View File
@@ -22,8 +22,6 @@ 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/firefox/addon/vega-wallet-fairground
#Test configuration variables
CYPRESS_FAIRGROUND=false
@@ -31,4 +29,4 @@ LC_ALL="en_US.UTF-8"
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=true
+2 -2
View File
@@ -20,7 +20,7 @@ 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/firefox/addon/vega-wallet-fairground
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
@@ -30,4 +30,4 @@ CYPRESS_FAIRGROUND=false
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
NX_METAMASK_SNAPS=false
+2 -2
View File
@@ -15,11 +15,11 @@ 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/firefox/addon/vega-wallet-fairground
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_METAMASK_SNAPS=true
+1 -1
View File
@@ -15,7 +15,7 @@ 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/firefox/addon/vega-wallet-mainnet
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
+1 -1
View File
@@ -14,7 +14,7 @@ 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/firefox/addon/vega-wallet-mainnet
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
+1 -1
View File
@@ -11,7 +11,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
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/firefox/addon/vega-wallet-fairground
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
+1 -1
View File
@@ -16,7 +16,7 @@ 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/firefox/addon/vega-wallet-fairground
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
+1 -1
View File
@@ -13,7 +13,7 @@ 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/firefox/addon/vega-wallet-fairground
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.
+4 -37
View File
@@ -41,7 +41,6 @@ import {
AppFailure,
NodeSwitcherDialog,
useNodeSwitcherStore,
DocsLinks,
} from '@vegaprotocol/environment';
import { ENV } from './config';
import type { InMemoryCacheConfig } from '@apollo/client';
@@ -110,17 +109,8 @@ const Web3Container = ({
store.connectors,
store.initialize,
]);
const {
ETHEREUM_PROVIDER_URL,
ETH_LOCAL_PROVIDER_URL,
ETH_WALLET_MNEMONIC,
VEGA_ENV,
VEGA_URL,
VEGA_EXPLORER_URL,
CHROME_EXTENSION_URL,
MOZILLA_EXTENSION_URL,
VEGA_WALLET_URL,
} = useEnvironment();
const { ETHEREUM_PROVIDER_URL, ETH_LOCAL_PROVIDER_URL, ETH_WALLET_MNEMONIC } =
useEnvironment();
useEffect(() => {
if (chainId) {
return initializeConnectors(
@@ -149,33 +139,10 @@ const Web3Container = ({
return <SplashLoader />;
}
if (
!VEGA_URL ||
!VEGA_WALLET_URL ||
!VEGA_EXPLORER_URL ||
!DocsLinks ||
!CHROME_EXTENSION_URL ||
!MOZILLA_EXTENSION_URL
) {
return null;
}
return (
<Web3Provider connectors={connectors}>
<Web3Connector connectors={connectors} chainId={Number(chainId)}>
<VegaWalletProvider
config={{
network: VEGA_ENV,
vegaUrl: VEGA_URL,
vegaWalletServiceUrl: VEGA_WALLET_URL,
links: {
explorer: VEGA_EXPLORER_URL,
concepts: DocsLinks?.VEGA_WALLET_CONCEPTS_URL,
chromeExtensionUrl: CHROME_EXTENSION_URL,
mozillaExtensionUrl: MOZILLA_EXTENSION_URL,
},
}}
>
<VegaWalletProvider>
<ContractsProvider>
<AppLoader>
<BalanceManager>
@@ -308,7 +275,7 @@ const AppContainer = () => {
<Router>
<ScrollToTop />
<AppStateProvider>
<div className="min-h-full text-white grid">
<div className="grid min-h-full text-white">
<NodeGuard
skeleton={<div>{t('Loading')}</div>}
failure={
@@ -54,7 +54,7 @@ export const WalletCardRow = ({
}) => {
const ref = React.useRef<HTMLDivElement | null>(null);
useAnimateValue(ref, value);
const [integers, decimalsPlaces, separator] = useNumberParts(value, decimals);
const [integers, decimalsPlaces] = useNumberParts(value, decimals);
return (
<div
@@ -75,10 +75,7 @@ export const WalletCardRow = ({
className="font-mono flex-1 text-right"
data-testid="associated-amount"
>
<span>
{integers}
{separator}
</span>
<span>{integers}.</span>
<span>{decimalsPlaces}</span>
</span>
)}
@@ -113,10 +110,7 @@ export const WalletCardAsset = ({
border,
subheading,
}: WalletCardAssetProps) => {
const [integers, decimalsPlaces, separator] = useNumberParts(
balance,
decimals
);
const [integers, decimalsPlaces] = useNumberParts(balance, decimals);
return (
<div className="flex flex-nowrap mt-2 mb-4">
@@ -138,10 +132,7 @@ export const WalletCardAsset = ({
</div>
</div>
<div className="px-2 basis-full font-mono" data-testid="currency-value">
<span>
{integers}
{separator}
</span>
<span>{integers}.</span>
<span className="text-neutral-400">{decimalsPlaces}</span>
</div>
</div>
+5 -4
View File
@@ -1,4 +1,4 @@
import { FLAGS } from '@vegaprotocol/environment';
import { ENV } from '@vegaprotocol/environment';
import {
JsonRpcConnector,
ViewConnector,
@@ -13,9 +13,10 @@ export const jsonRpc = new JsonRpcConnector();
export const injected = new InjectedConnector();
export const view = new ViewConnector(urlParams.get('address'));
export const snap = FLAGS.METAMASK_SNAPS
? new SnapConnector(DEFAULT_SNAP_ID)
: undefined;
export const snap = new SnapConnector(
ENV.VEGA_URL ? new URL(ENV.VEGA_URL).origin : undefined,
DEFAULT_SNAP_ID
);
export const Connectors = {
injected,
@@ -1,7 +1,6 @@
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
import type { VegaWalletConfig } from '@vegaprotocol/wallet';
import { render, screen } from '@testing-library/react';
import { generateProposal } from '../../test-helpers/generate-proposals';
import { Proposal } from './proposal';
@@ -44,23 +43,11 @@ jest.mock('../list-asset', () => ({
ListAsset: () => <div data-testid="proposal-list-asset"></div>,
}));
const vegaWalletConfig: VegaWalletConfig = {
network: 'TESTNET',
vegaUrl: 'https://vega.xyz',
vegaWalletServiceUrl: 'https://wallet.vega.xyz',
links: {
explorer: 'explorer',
concepts: 'concepts',
chromeExtensionUrl: 'chrome',
mozillaExtensionUrl: 'mozilla',
},
};
const renderComponent = (proposal: ProposalQuery['proposal']) => {
render(
<MemoryRouter>
<MockedProvider>
<VegaWalletProvider config={vegaWalletConfig}>
<VegaWalletProvider>
<Proposal
restData={{}}
proposal={proposal as ProposalQuery['proposal']}
@@ -4,7 +4,6 @@ import { VoteButtons } from './vote-buttons';
import { VoteState } from './use-user-vote';
import { ProposalState } from '@vegaprotocol/types';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { mockWalletContext } from '../../test-helpers/mocks';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { MockedProvider } from '@apollo/react-testing';
@@ -68,7 +67,7 @@ describe('Vote buttons', () => {
disconnect: jest.fn(),
selectPubKey: jest.fn(),
connector: null,
} as unknown as VegaWalletContextShape;
};
render(
<AppStateProvider>
@@ -114,7 +114,6 @@ describe('Raw proposal form', () => {
{
pubKey,
sendTx: mockSendTx,
links: { explorer: 'explorer' },
} as unknown as VegaWalletContextShape
}
>
@@ -1,7 +1,7 @@
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import type { MockedResponse } from '@apollo/client/testing';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import type { PubKey, VegaWalletContextShape } from '@vegaprotocol/wallet';
import type { PubKey } from '@vegaprotocol/wallet';
import type { VoteValue } from '@vegaprotocol/types';
import type { UserVoteQuery } from '../components/vote-details/__generated__/Vote';
import { UserVoteDocument } from '../components/vote-details/__generated__/Vote';
@@ -21,7 +21,7 @@ export const mockWalletContext = {
disconnect: jest.fn(),
selectPubKey: jest.fn(),
connector: null,
} as unknown as VegaWalletContextShape;
};
const mockEthereumConfig = {
network_id: '3',
@@ -0,0 +1,475 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import type { DataSourceDefinition } from '@vegaprotocol/types';
import {
MarketState,
MarketStateMapping,
PropertyKeyType,
} from '@vegaprotocol/types';
import { addDays, subDays } from 'date-fns';
import {
chainIdQuery,
statisticsQuery,
createDataConnection,
oracleSpecDataConnectionQuery,
createMarketFragment,
marketsQuery,
marketsDataQuery,
createMarketsDataFragment,
assetQuery,
networkParamsQuery,
nodeGuardQuery,
} from '@vegaprotocol/mock';
import {
addDecimalsFormatNumber,
getDateTimeFormat,
} from '@vegaprotocol/utils';
describe('Closed markets', { tags: '@smoke' }, () => {
const settlementDataProperty = 'settlement-data-property';
const settlementDataPropertyKey = {
__typename: 'PropertyKey' as const,
name: settlementDataProperty,
type: PropertyKeyType.TYPE_INTEGER,
numberDecimalPlaces: 2,
};
const settlementDataSourceData: DataSourceDefinition = {
sourceType: {
sourceType: {
filters: [
{
__typename: 'Filter',
key: settlementDataPropertyKey,
},
],
},
},
};
const rowSelector =
'[data-testid="tab-closed-markets"] .ag-center-cols-container .ag-row';
const assetsResult = assetQuery();
// @ts-ignore asset definitely exists
const settlementAsset = assetsResult.assetsConnection.edges[0].node;
const settledMarket = createMarketFragment({
id: '0',
state: MarketState.STATE_SETTLED,
marketTimestamps: {
open: subDays(new Date(), 10).toISOString(),
close: subDays(new Date(), 4).toISOString(),
},
tradableInstrument: {
instrument: {
product: {
dataSourceSpecBinding: {
settlementDataProperty,
},
dataSourceSpecForTradingTermination: {
id: 'market-1-trading-termination-oracle-id',
},
dataSourceSpecForSettlementData: {
id: 'market-1-settlement-data-oracle-id',
data: settlementDataSourceData,
},
settlementAsset,
},
},
},
});
const terminatedMarket = createMarketFragment({
id: '1',
state: MarketState.STATE_TRADING_TERMINATED,
marketTimestamps: {
open: subDays(new Date(), 10).toISOString(),
close: null, // market
},
tradableInstrument: {
instrument: {
metadata: {
tags: [
`settlement-expiry-date:${addDays(new Date(), 4).toISOString()}`,
],
},
product: {
dataSourceSpecBinding: {
settlementDataProperty,
},
dataSourceSpecForSettlementData: {
id: 'market-1-settlement-data-oracle-id',
data: settlementDataSourceData,
},
},
},
},
});
const delayedSettledMarket = createMarketFragment({
id: '2',
state: MarketState.STATE_TRADING_TERMINATED,
marketTimestamps: {
open: subDays(new Date(), 10).toISOString(),
close: null, // market
},
tradableInstrument: {
instrument: {
metadata: {
tags: [
`settlement-expiry-date:${subDays(new Date(), 2).toISOString()}`,
],
},
product: {
dataSourceSpecBinding: {
settlementDataProperty,
},
dataSourceSpecForSettlementData: {
id: 'market-1-settlement-data-oracle-id',
data: settlementDataSourceData,
},
},
},
},
});
const unknownMarket = createMarketFragment({
id: '3',
state: MarketState.STATE_SETTLED,
});
const closedMarketsResult = [
{
node: settledMarket,
},
{
node: terminatedMarket,
},
{
node: delayedSettledMarket,
},
{ node: unknownMarket },
{
node: createMarketFragment({ id: '4', state: MarketState.STATE_PENDING }),
},
{
node: createMarketFragment({ id: '5', state: MarketState.STATE_ACTIVE }),
},
];
const settledMarketData = createMarketsDataFragment({
market: {
id: settledMarket.id,
},
bestBidPrice: '1000',
bestOfferPrice: '2000',
markPrice: '1500',
});
const closedMarketsDataResult = [
{
node: {
data: settledMarketData,
},
},
{
node: {
data: createMarketsDataFragment({
market: {
id: terminatedMarket.id,
},
}),
},
},
{
node: {
data: createMarketsDataFragment({
market: {
id: delayedSettledMarket.id,
},
}),
},
},
{
node: {
data: createMarketsDataFragment({
market: {
id: unknownMarket.id,
},
}),
},
},
];
const specDataConnection = createDataConnection();
before(() => {
cy.setOnBoardingViewed();
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'NodeGuard', nodeGuardQuery());
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
aliasGQLQuery(
req,
'Markets',
marketsQuery({
marketsConnection: {
edges: closedMarketsResult,
},
})
);
aliasGQLQuery(
req,
'MarketsData',
marketsDataQuery({
marketsConnection: {
edges: closedMarketsDataResult,
},
})
);
aliasGQLQuery(
req,
'OracleSpecDataConnection',
oracleSpecDataConnectionQuery()
);
});
cy.mockSubscription();
cy.visit('/#/markets/all');
cy.get('[data-testid="Closed markets"]').click();
});
it('renders a settled market', () => {
const expectedMarkets = closedMarketsResult.filter((edge) => {
return [
MarketState.STATE_SETTLED,
MarketState.STATE_TRADING_TERMINATED,
].includes(edge.node.state);
});
const product = settledMarket.tradableInstrument.instrument.product;
// rows should be filtered to only include settled/terminated markets
cy.get(rowSelector).should('have.length', expectedMarkets.length);
// check each column in the first row renders correctly
// 6001-MARK-001
cy.get(rowSelector)
.first()
.find('[col-id="code"]')
.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()
.find('[col-id="name"]')
.should('have.text', settledMarket.tradableInstrument.instrument.name);
// 6001-MARK-003
cy.get(rowSelector)
.first()
.find('[col-id="state"]')
.should('have.text', MarketStateMapping[settledMarket.state]);
// 6001-MARK-004
// 6001-MARK-005
// 6001-MARK-009
// 6001-MARK-008
// 6001-MARK-010
cy.get(rowSelector)
.first()
.find('[col-id="settlementDate"]')
.find('[data-testid="link"]')
.should(($el) => {
const href = $el.attr('href');
expect(href).to.match(
new RegExp(
`/oracles/${product.dataSourceSpecForTradingTermination.id}`
)
);
})
.should('have.text', '4 days ago')
.should(
'have.attr',
'title',
getDateTimeFormat().format(
new Date(settledMarket.marketTimestamps.close)
)
);
// 6001-MARK-011
cy.get(rowSelector)
.first()
.find('[col-id="bestBidPrice"]')
.should(
'have.text',
addDecimalsFormatNumber(
settledMarketData.bestBidPrice,
settledMarket.decimalPlaces
)
);
// 6001-MARK-012
cy.get(rowSelector)
.first()
.find('[col-id="bestOfferPrice"]')
.should(
'have.text',
addDecimalsFormatNumber(
settledMarketData.bestOfferPrice,
settledMarket.decimalPlaces
)
);
// 6001-MARK-013
cy.get(rowSelector).first().find('[col-id="markPrice"]').should(
'have.text',
addDecimalsFormatNumber(
settledMarketData.markPrice,
settledMarket.decimalPlaces
)
);
// 6001-MARK-014
// 6001-MARK-015
// 6001-MARK-016
cy.get(rowSelector)
.first()
.find('[col-id="settlementDataOracleId"]')
.find('[data-testid="link"]')
.should(($el) => {
const href = $el.attr('href');
expect(href).to.match(
new RegExp(`/oracles/${product.dataSourceSpecForSettlementData.id}`)
);
})
.should(
'have.text',
addDecimalsFormatNumber(
// @ts-ignore cannot deep un-partial
specDataConnection.externalData.data.data[0].value,
settlementDataPropertyKey.numberDecimalPlaces
)
);
// 6001-MARK-018
cy.get(rowSelector)
.first()
.find('[col-id="settlementAsset"]')
.should('have.text', product.settlementAsset.symbol);
// 6001-MARK-020
cy.get('.ag-pinned-right-cols-container')
.find('[col-id="market-actions"]')
.first()
.find('button svg')
.should('exist');
if (Cypress.env('NX_SUCCESSOR_MARKETS')) {
cy.get(rowSelector)
.find('[col-id="successorMarket"]')
.first()
.should('have.text', '-');
}
});
// test market list for market in terminated state
it('renders a terminated market', () => {
cy.get(rowSelector)
.eq(1)
.find('[col-id="state"]')
.should('have.text', MarketStateMapping[terminatedMarket.state]);
// 6001-MARK-006
// 6001-MARK-007
cy.get(rowSelector)
.eq(1)
.find('[col-id="settlementDate"]')
.find('[data-testid="link"]')
.should('have.text', 'Expected in 4 days');
});
it('renders a terminated market which was expected to have settled', () => {
cy.get(rowSelector)
.eq(2)
.find('[col-id="settlementDate"]')
.should('have.class', 'text-danger')
.find('[data-testid="link"]')
.should('have.text', 'Expected 2 days ago');
});
it('renders terminated market which doesnt have settlement date metadata', () => {
cy.get(rowSelector)
.eq(3)
.find('[col-id="settlementDate"]')
.find('[data-testid="link"]')
.should('have.text', 'Unknown');
});
it('can open asset detail dialog', () => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Asset', assetsResult);
});
cy.get(rowSelector)
.first()
.find('[col-id="settlementAsset"]')
.find('button')
.click();
// 6001-MARK-019
cy.get('[data-testid="dialog-title"]').should(
'have.text',
`Asset details - ${settlementAsset.symbol}`
);
cy.get('[data-testid="dialog-close"]').click();
});
it('can open row actions', () => {
cy.get('.ag-pinned-right-cols-container')
.find('[col-id="market-actions"]')
.first()
.find('button')
.click();
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');
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(1)
.find('a')
.then(($el) => {
const href = $el.attr('href');
expect(/\/markets\/0/.test(href || '')).to.equal(true);
})
.should('have.text', 'View on Explorer');
});
});
describe('no closed markets', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all');
cy.get('[data-testid="Closed markets"]').click();
});
it('can see no markets message', () => {
// 6001-MARK-034
cy.getByTestId('tab-closed-markets').should('contain.text', 'No markets');
});
});
@@ -133,7 +133,11 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
validateMarketDataRow(4, 'Decimals', '5');
validateMarketDataRow(5, 'Quantum', '1');
validateMarketDataRow(6, 'Status', 'Enabled');
validateMarketDataRow(7, 'Contract address', '0x0158…78a4');
validateMarketDataRow(
7,
'Contract address',
'0x0158031158Bb4dF2AD02eAA31e8963E84EA978a4'
);
validateMarketDataRow(8, 'Withdrawal threshold', '0.0005');
validateMarketDataRow(9, 'Lifetime limit', '1,230');
validateMarketDataRow(10, 'Infrastructure fee account balance', '0.00001');
@@ -0,0 +1,117 @@
const orderbookTab = 'Orderbook';
const orderbookTable = 'tab-orderbook';
const askPrice = 'price-9894185';
const bidPrice = 'price-9889001';
const askVolume = 'ask-vol-9894185';
const bidVolume = 'bid-vol-9889001';
const askCumulative = 'cumulative-vol-9894185';
const bidCumulative = 'cumulative-vol-9889001';
const midPrice = 'middle-mark-price-4612690000';
const priceResolution = 'resolution';
const dealTicketPrice = 'order-price';
const dealTicketSize = 'order-size';
const resPrice = 'price-990';
describe('order book', { tags: '@smoke' }, () => {
before(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
beforeEach(() => {
cy.mockTradingPage();
});
it('show order book', () => {
// 6003-ORDB-001
// 6003-ORDB-002
cy.getByTestId(orderbookTab).click();
cy.getByTestId(orderbookTable).should('be.visible');
cy.getByTestId(orderbookTable).should('not.be.empty');
});
it('show orders prices', () => {
// 6003-ORDB-003
cy.getByTestId(askPrice).should('have.text', '98.94185');
cy.getByTestId(bidPrice).should('have.text', '98.89001');
});
it('show prices volumes', () => {
// 6003-ORDB-004
cy.getByTestId(askVolume).should('have.text', '1');
cy.getByTestId(bidVolume).should('have.text', '1');
});
it('show prices cumulative volumes', () => {
// 6003-ORDB-005
cy.getByTestId(askCumulative).should('have.text', '38');
cy.getByTestId(bidCumulative).should('have.text', '7');
});
it('show mid price', () => {
// 6003-ORDB-006
cy.getByTestId(midPrice).should('have.text', '46,126.90');
});
it('sort prices descending', () => {
// 6003-ORDB-007
const prices: number[] = [];
cy.getByTestId(orderbookTable).within(() => {
cy.get('[data-testid*=price]')
.each(($el) => {
prices.push(Number($el.text()));
})
.then(() => {
expect(prices).to.deep.equal(prices.sort((a, b) => b - a));
});
});
});
it('copy price to deal ticket form', () => {
// 6003-ORDB-009
cy.getByTestId(askPrice).click();
cy.getByTestId(dealTicketPrice).should('have.value', '98.94185');
});
it('copy size to deal ticket form', () => {
// 6003-ORDB-009
cy.getByTestId(bidCumulative).click();
cy.getByTestId(dealTicketSize).should('have.value', '7');
});
it('copy size to deal ticket form', () => {
// 6003-ORDB-009
cy.getByTestId(bidVolume).click();
cy.getByTestId(dealTicketSize).should('have.value', '1');
});
it('change price resolution', () => {
// 6003-ORDB-008
const resolutions = [
'0.00000',
'0.0000',
'0.000',
'0.00',
'0.0',
'0',
'10',
'100',
'1,000',
'10,000',
];
cy.getByTestId(priceResolution).click();
cy.get('[role="menu"]')
.find('[role="menuitem"]')
.each(($el, index) => {
expect($el.text()).to.equal(resolutions[index]);
});
cy.get('[role="menuitem"]').eq(4).click();
cy.getByTestId(resPrice).should('have.text', '99.0');
cy.getByTestId(askPrice).should('not.exist');
cy.getByTestId(bidPrice).should('not.exist');
});
});
@@ -33,7 +33,9 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
it('must see the price unit', function () {
// 7002-SORD-018
cy.getByTestId(orderPriceField).next().should('have.text', 'DAI');
cy.getByTestId(orderPriceField)
.siblings('label')
.should('have.text', 'Price (DAI)');
});
it('must see warning when placing an order with expiry date in past', () => {
@@ -43,10 +43,11 @@ describe('ethereum wallet', { tags: '@smoke', testIsolation: true }, () => {
// 0004-EWAL-005
// 0004-EWAL-006
const ethWalletAddress = Cypress.env('ETHEREUM_WALLET_ADDRESS');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
connectEthereumWallet('MetaMask');
cy.getByTestId('ethereum-address').should('have.text', '0xEe7D…d94F');
cy.getByTestId('ethereum-address').should('have.text', ethWalletAddress);
cy.getByTestId('disconnect-ethereum-wallet')
.should('have.text', 'Disconnect')
.click();
-2
View File
@@ -12,8 +12,6 @@ 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/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
+1 -1
View File
@@ -13,7 +13,7 @@ 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/firefox/addon/vega-wallet-fairground
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"
+1 -1
View File
@@ -14,7 +14,7 @@ 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/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+1 -1
View File
@@ -14,7 +14,7 @@ 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://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/firefox/addon/vega-wallet-mainnet
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
+1 -1
View File
@@ -14,7 +14,7 @@ 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://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/firefox/addon/vega-wallet-mainnet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet-mainnet
# TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.20.19-core-0.71.6
+1 -1
View File
@@ -14,7 +14,7 @@ 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/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+1 -1
View File
@@ -15,7 +15,7 @@ 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://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/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+1 -1
View File
@@ -16,7 +16,7 @@ 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/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
@@ -1,49 +1,27 @@
import React, { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
LocalStoragePersistTabs as Tabs,
Tab,
TradingAnchorButton,
} from '@vegaprotocol/ui-toolkit';
import { LocalStoragePersistTabs as Tabs, Tab } from '@vegaprotocol/ui-toolkit';
import { Markets } from './markets';
import { Proposed } from './proposed';
import { usePageTitleStore } from '../../stores';
import { Closed } from './closed';
import {
DApp,
TOKEN_NEW_MARKET_PROPOSAL,
useLinks,
} from '@vegaprotocol/environment';
export const MarketsPage = () => {
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
const tokenLink = useLinks(DApp.Token);
const externalLink = tokenLink(TOKEN_NEW_MARKET_PROPOSAL);
useEffect(() => {
updateTitle(titlefy(['Markets']));
}, [updateTitle]);
return (
<div className="h-full pt-0.5 pb-3 px-1.5">
<div className="h-full my-1 border rounded-sm border-default">
<div className="h-full my-1 border border-default rounded-sm">
<Tabs storageKey="console-markets">
<Tab id="open-markets" name={t('Open markets')}>
<Markets />
</Tab>
<Tab
id="proposed-markets"
name={t('Proposed markets')}
menu={
<TradingAnchorButton size="extra-small" href={externalLink}>
{t('Propose a new market')}
</TradingAnchorButton>
}
>
<Tab id="proposed-markets" name={t('Proposed markets')}>
<Proposed />
</Tab>
<Tab id="closed-markets" name={t('Closed markets')}>
+19 -1
View File
@@ -1,6 +1,24 @@
import { t } from '@vegaprotocol/i18n';
import {
DApp,
TOKEN_NEW_MARKET_PROPOSAL,
useLinks,
} from '@vegaprotocol/environment';
import { ProposalsList } from '@vegaprotocol/proposals';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { SuccessorMarketRenderer } from './successor-market-cell';
export const Proposed = () => {
return <ProposalsList SuccessorMarketRenderer={SuccessorMarketRenderer} />;
const tokenLink = useLinks(DApp.Token);
const externalLink = tokenLink(TOKEN_NEW_MARKET_PROPOSAL);
return (
<>
<div className="h-[400px]">
<ProposalsList SuccessorMarketRenderer={SuccessorMarketRenderer} />
</div>
<ExternalLink className="py-4 px-[11px] text-sm" href={externalLink}>
{t('Propose a new market')}
</ExternalLink>
</>
);
};
@@ -4,7 +4,7 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import compact from 'lodash/compact';
import uniqBy from 'lodash/uniqBy';
import type { ChangeEvent } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { AccountHistoryQuery } from './__generated__/AccountHistory';
import { useAccountHistoryQuery } from './__generated__/AccountHistory';
import * as Schema from '@vegaprotocol/types';
@@ -12,13 +12,12 @@ import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import {
AsyncRenderer,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Splash,
Toggle,
TradingButton,
TradingDropdown,
TradingDropdownContent,
TradingDropdownItem,
TradingDropdownTrigger,
} from '@vegaprotocol/ui-toolkit';
import { AccountTypeMapping } from '@vegaprotocol/types';
import { PriceChart } from 'pennant';
@@ -151,7 +150,6 @@ const AccountHistoryManager = ({
)
: null;
}, [accounts, marketFilterCb]);
const resolveMarket = useCallback(
(m: Market) => {
setMarket(m);
@@ -176,114 +174,111 @@ const AccountHistoryManager = ({
}),
[pubKey, asset, accountType, range, market?.id]
);
const { data } = useAccountHistoryQuery({
variables,
skip: !asset || !pubKey,
});
const accountTypeMenu = useMemo(() => {
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{accountType
? `${
AccountTypeMapping[
accountType as keyof typeof Schema.AccountType
]
} Account`
: t('Select account type')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{[
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
Schema.AccountType.ACCOUNT_TYPE_BOND,
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
].map((type) => (
<DropdownMenuItem
key={type}
onClick={() => setAccountType(type as Schema.AccountType)}
>
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}, [accountType]);
const assetsMenu = useMemo(() => {
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{asset ? asset.symbol : t('Select asset')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{assets.map((a) => (
<DropdownMenuItem key={a.id} onClick={() => setAssetId(a.id)}>
{a.symbol}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}, [asset, assets, setAssetId]);
const marketsMenu = useMemo(() => {
return accountType === Schema.AccountType.ACCOUNT_TYPE_MARGIN &&
markets?.length ? (
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{market
? market.tradableInstrument.instrument.code
: t('Select market')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{market && (
<DropdownMenuItem key="0" onClick={() => setMarket(null)}>
{t('All markets')}
</DropdownMenuItem>
)}
{markets?.map((m) => (
<DropdownMenuItem key={m.id} onClick={() => resolveMarket(m)}>
{m.tradableInstrument.instrument.code}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : null;
}, [markets, market, accountType, resolveMarket]);
useEffect(() => {
if (
accountType !== Schema.AccountType.ACCOUNT_TYPE_MARGIN ||
market?.tradableInstrument.instrument.product.settlementAsset.id !==
asset?.id
) {
setMarket(null);
}
}, [accountType, asset?.id, market]);
return (
<div className="flex flex-col h-full gap-2">
<div className="flex flex-wrap justify-between px-1 pt-2 gap-2">
<div className="flex items-center gap-1 shrink-0">
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{accountType
? `${
AccountTypeMapping[
accountType as keyof typeof Schema.AccountType
]
} Account`
: t('Select account type')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{[
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
Schema.AccountType.ACCOUNT_TYPE_BOND,
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
].map((type) => (
<TradingDropdownItem
key={type}
onClick={() => {
setAccountType(type as Schema.AccountType);
// if not a margin account clear any market selection
if (type !== Schema.AccountType.ACCOUNT_TYPE_MARGIN) {
setMarket(null);
}
}}
>
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{asset ? asset.symbol : t('Select asset')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{assets.map((a) => (
<TradingDropdownItem
key={a.id}
onClick={() => {
setAssetId(a.id);
// if the selected asset is different to the selected market clear the market
if (
a.id !==
market?.tradableInstrument.instrument.product
.settlementAsset.id
) {
setMarket(null);
}
}}
>
{a.symbol}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{market
? market.tradableInstrument.instrument.code
: t('Select market')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{market && (
<TradingDropdownItem key="0" onClick={() => setMarket(null)}>
{t('All markets')}
</TradingDropdownItem>
)}
{markets?.map((m) => (
<TradingDropdownItem
key={m.id}
onClick={() => resolveMarket(m)}
>
{m.tradableInstrument.instrument.code}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<div className="h-full w-full flex flex-col gap-8">
<div className="w-full flex flex-col-reverse lg:flex-row items-start lg:items-center justify-between gap-4 px-2">
<div className="flex items-center gap-4 shrink-0">
<>
{accountTypeMenu}
{assetsMenu}
{marketsMenu}
</>
</div>
<div className="justify-items-end">
<div className="pt-1 justify-items-end">
<Toggle
id="account-history-date-range"
name="account-history-date-range"
@@ -292,19 +287,16 @@ const AccountHistoryManager = ({
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setRange(e.target.value as keyof typeof DateRange)
}
size="sm"
/>
</div>
</div>
<div className="flex-1">
<div className="h-5/6 px-4">
{asset && (
<div className="h-full">
<AccountHistoryChart
data={data}
accountType={accountType}
asset={asset}
/>
</div>
<AccountHistoryChart
data={data}
accountType={accountType}
asset={asset}
/>
)}
</div>
</div>
@@ -1,5 +1,5 @@
import { t } from '@vegaprotocol/i18n';
import { TradingButton } from '@vegaprotocol/ui-toolkit';
import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
import { ViewType, useSidebar } from '../sidebar';
export const AccountsMenu = () => {
@@ -8,6 +8,7 @@ export const AccountsMenu = () => {
return (
<>
<TradingButton
intent={Intent.Primary}
size="extra-small"
data-testid="open-transfer"
onClick={() => setView({ type: ViewType.Transfer })}
@@ -15,6 +16,7 @@ export const AccountsMenu = () => {
{t('Transfer')}
</TradingButton>
<TradingButton
intent={Intent.Primary}
size="extra-small"
onClick={() => setView({ type: ViewType.Deposit })}
>
@@ -1,7 +1,6 @@
import type { InMemoryCacheConfig } from '@apollo/client';
import {
AppFailure,
DocsLinks,
NetworkLoader,
NodeGuard,
useEnvironment,
@@ -18,32 +17,16 @@ export const DynamicLoader = dynamic(() => import('../preloader/preloader'), {
});
export const AppLoader = ({ children }: { children: ReactNode }) => {
const {
error,
VEGA_URL,
VEGA_ENV,
VEGA_WALLET_URL,
VEGA_EXPLORER_URL,
MAINTENANCE_PAGE,
MOZILLA_EXTENSION_URL,
CHROME_EXTENSION_URL,
} = useEnvironment();
const { error, VEGA_URL, MAINTENANCE_PAGE } = useEnvironment((store) => ({
error: store.error,
VEGA_URL: store.VEGA_URL,
MAINTENANCE_PAGE: store.MAINTENANCE_PAGE,
}));
if (MAINTENANCE_PAGE) {
return <MaintenancePage />;
}
if (
!VEGA_URL ||
!VEGA_WALLET_URL ||
!VEGA_EXPLORER_URL ||
!CHROME_EXTENSION_URL ||
!MOZILLA_EXTENSION_URL ||
!DocsLinks
) {
return null;
}
return (
<NetworkLoader
cache={cacheConfig}
@@ -57,21 +40,7 @@ export const AppLoader = ({ children }: { children: ReactNode }) => {
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
>
<Web3Provider>
<VegaWalletProvider
config={{
network: VEGA_ENV,
vegaUrl: VEGA_URL,
vegaWalletServiceUrl: VEGA_WALLET_URL,
links: {
explorer: VEGA_EXPLORER_URL,
concepts: DocsLinks.VEGA_WALLET_CONCEPTS_URL,
chromeExtensionUrl: CHROME_EXTENSION_URL,
mozillaExtensionUrl: MOZILLA_EXTENSION_URL,
},
}}
>
{children}
</VegaWalletProvider>
<VegaWalletProvider>{children}</VegaWalletProvider>
</Web3Provider>
</NodeGuard>
</NetworkLoader>
@@ -1,5 +1,5 @@
import { t } from '@vegaprotocol/i18n';
import { TradingButton } from '@vegaprotocol/ui-toolkit';
import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
import { ViewType, useSidebar } from '../sidebar';
export const DepositsMenu = () => {
@@ -7,6 +7,7 @@ export const DepositsMenu = () => {
return (
<TradingButton
intent={Intent.Primary}
size="extra-small"
onClick={() => setView({ type: ViewType.Deposit })}
data-testid="deposit-button"
@@ -1,12 +1,13 @@
import { t } from '@vegaprotocol/i18n';
import {
TradingDropdown,
TradingDropdownCheckboxItem,
TradingDropdownContent,
TradingDropdownItemIndicator,
TradingDropdownTrigger,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItemIndicator,
DropdownMenuTrigger,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { MarketSelectorButton } from './market-selector-button';
type Assets = Array<{ id: string; symbol: string }>;
@@ -24,19 +25,17 @@ export const AssetDropdown = ({
}
return (
<TradingDropdown
<DropdownMenu
trigger={
<TradingDropdownTrigger data-testid="asset-trigger">
<MarketSelectorButton>
{triggerText({ assets, checkedAssets })}
</MarketSelectorButton>
</TradingDropdownTrigger>
<DropdownMenuTrigger data-testid="asset-trigger">
<TriggerText assets={assets} checkedAssets={checkedAssets} />
</DropdownMenuTrigger>
}
>
<TradingDropdownContent>
<DropdownMenuContent>
{assets?.map((a) => {
return (
<TradingDropdownCheckboxItem
<DropdownMenuCheckboxItem
key={a.id}
checked={checkedAssets.includes(a.id)}
onCheckedChange={(checked) => {
@@ -47,16 +46,16 @@ export const AssetDropdown = ({
data-testid={`asset-id-${a.id}`}
>
{a.symbol}
<TradingDropdownItemIndicator />
</TradingDropdownCheckboxItem>
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
);
})}
</TradingDropdownContent>
</TradingDropdown>
</DropdownMenuContent>
</DropdownMenu>
);
};
const triggerText = ({
const TriggerText = ({
assets,
checkedAssets,
}: {
@@ -73,5 +72,9 @@ const triggerText = ({
text = t(`${checkedAssets.length} Assets`);
}
return text;
return (
<span className="flex justify-between items-center">
{text} <VegaIcon name={VegaIconNames.CHEVRON_DOWN} />
</span>
);
};
@@ -1,3 +1,2 @@
export * from './market-selector';
export * from './market-selector-item';
export * from './market-selector-button';
@@ -1,23 +0,0 @@
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import type { ButtonHTMLAttributes } from 'react';
import { forwardRef } from 'react';
export const MarketSelectorButton = forwardRef<
HTMLButtonElement,
ButtonHTMLAttributes<HTMLButtonElement>
>((props, ref) => (
<button
{...props}
className={classNames(
'flex items-center justify-between px-2 border rounded gap-1',
'border-vega-clight-600 dark:border-vega-cdark-600 bg-vega-clight-700 dark:bg-vega-cdark-700',
'text-secondary data-[state=open]:text-vega-clight-50 dark:data-[state=open]:text-vega-cdark-50'
)}
ref={ref}
>
{props.children}
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} />
</button>
));
MarketSelectorButton.displayName = 'MarketSelectorButton';
@@ -69,7 +69,6 @@ describe('MarketSelectorItem', () => {
targetStake: '1000000',
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
priceMonitoringBounds: null,
lastTradedPrice: '100',
};
const candles = [
@@ -31,7 +31,7 @@ export const MarketSelectorItem = ({
<div style={style} role="row">
<Link
to={`/markets/${market.id}`}
className={classNames('h-full flex items-center gap-2 mx-2 px-2', {
className={classNames('h-full flex items-center gap-2 px-4', {
'hover:bg-vega-clight-700 dark:hover:bg-vega-cdark-700':
market.id !== currentMarketId,
'bg-vega-clight-600 dark:bg-vega-cdark-600':
@@ -94,7 +94,7 @@ const MarketData = ({
return (
<>
<div className="w-2/5" role="gridcell">
<h3 className="overflow-hidden text-sm text-ellipsis lg:text-base whitespace-nowrap">
<h3 className="text-ellipsis text-sm lg:text-base whitespace-nowrap overflow-hidden">
{market.tradableInstrument.instrument.code}{' '}
{allProducts && productType && (
<MarketProductPill productType={productType} />
@@ -107,7 +107,7 @@ const MarketData = ({
)}
</div>
<div
className="w-1/5 overflow-hidden text-xs lg:text-sm whitespace-nowrap text-ellipsis"
className="w-1/5 text-xs lg:text-sm whitespace-nowrap text-ellipsis overflow-hidden"
title={instrument.product.settlementAsset.symbol}
data-testid="market-selector-price"
role="gridcell"
@@ -115,14 +115,14 @@ const MarketData = ({
{price} {instrument.product.settlementAsset.symbol}
</div>
<div
className="w-1/5 overflow-hidden text-xs text-right lg:text-sm whitespace-nowrap text-ellipsis"
className="w-1/5 text-xs lg:text-sm text-right whitespace-nowrap text-ellipsis overflow-hidden"
title={t('24h vol')}
data-testid="market-selector-volume"
role="gridcell"
>
{volume}
</div>
<div className="flex justify-end w-1/5" role="gridcell">
<div className="w-1/5 flex justify-end" role="gridcell">
{oneDayCandles && (
<Sparkline
width={64}
@@ -262,7 +262,9 @@ describe('MarketSelector', () => {
await userEvent.click(screen.getByTestId('sort-trigger'));
const options = screen.getAllByTestId(/sort-item/);
expect(options.map((o) => o.textContent?.trim())).toEqual(
Object.entries(Sort).map(([key]) => SortTypeMapping[key as SortType])
Object.entries(Sort)
.filter(([key]) => key !== Sort.None)
.map(([key]) => SortTypeMapping[key as SortType])
);
await userEvent.click(screen.getByTestId('sort-item-Gained'));
expect(
@@ -40,7 +40,7 @@ export const MarketSelector = ({
const [filter, setFilter] = useState<Filter>({
searchTerm: '',
product: Product.All,
sort: Sort.TopTraded,
sort: Sort.None,
assets: [],
});
const allProducts = filter.product === Product.All;
@@ -52,8 +52,8 @@ export const MarketSelector = ({
}, [reload]);
return (
<div data-testid="market-selector" className="md:w-[580px]">
<div className="px-2 pt-2 mb-2">
<div data-testid="market-selector">
<div className="pt-2 px-2 mb-2">
<ProductSelector
product={filter.product}
onSelect={(product) => {
@@ -106,6 +106,9 @@ export const MarketSelector = ({
currentSort={filter.sort}
onSelect={(sort) => {
setFilter((curr) => {
if (curr.sort === sort) {
return { ...curr, sort: Sort.None };
}
return {
...curr,
sort,
@@ -291,9 +294,9 @@ const List = ({
const Skeleton = () => {
return (
<div className="px-2 mb-2">
<div className="p-4 rounded-lg bg-vega-light-100 dark:bg-vega-dark-100">
<div className="w-full h-3 mb-2 bg-vega-light-200 dark:bg-vega-dark-200" />
<div className="mb-2 px-2">
<div className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg p-4">
<div className="w-full h-3 bg-vega-light-200 dark:bg-vega-dark-200 mb-2" />
<div className="w-2/3 h-3 bg-vega-light-200 dark:bg-vega-dark-200" />
</div>
</div>
@@ -33,14 +33,11 @@ export const ProductSelector = ({
return (
<div className="flex mb-2">
{Object.keys(Product).map((t) => {
const classes = classNames(
'text-sm px-3 py-1.5 rounded hover:text-vega-clight-50 dark:hover:text-vega-cdark-50',
{
'bg-vega-clight-500 dark:bg-vega-cdark-500 text-default':
t === product,
'text-secondary': t !== product,
}
);
const classes = classNames('px-3 py-1.5 rounded', {
'bg-vega-clight-500 dark:bg-vega-cdark-500 text-default':
t === product,
'text-secondary': t !== product,
});
return (
<button
key={t}
@@ -56,7 +53,7 @@ export const ProductSelector = ({
})}
<Link
to={Routes.MARKETS}
className="flex items-center ml-auto text-sm gap-2"
className="flex items-center gap-2 ml-auto"
title={t('See all markets')}
>
<span className="underline underline-offset-4">{t('Browse')}</span>
@@ -1,16 +1,17 @@
import { t } from '@vegaprotocol/i18n';
import {
TradingDropdown,
TradingDropdownContent,
TradingDropdownItemIndicator,
TradingDropdownRadioGroup,
TradingDropdownRadioItem,
TradingDropdownTrigger,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItemIndicator,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { MarketSelectorButton } from './market-selector-button';
export const Sort = {
None: 'None',
Gained: 'Gained',
Lost: 'Lost',
New: 'New',
@@ -22,15 +23,17 @@ export type SortType = keyof typeof Sort;
export const SortTypeMapping: {
[key in SortType]: string;
} = {
[Sort.TopTraded]: 'Top traded',
[Sort.None]: 'None',
[Sort.Gained]: 'Top gaining',
[Sort.Lost]: 'Top losing',
[Sort.New]: 'New markets',
[Sort.TopTraded]: 'Top traded',
};
const SortIconMapping: {
[key in SortType]: VegaIconNames;
} = {
[Sort.None]: null as unknown as VegaIconNames, // not shown in list
[Sort.Gained]: VegaIconNames.TREND_UP,
[Sort.Lost]: VegaIconNames.TREND_DOWN,
[Sort.New]: VegaIconNames.STAR,
@@ -45,38 +48,43 @@ export const SortDropdown = ({
onSelect: (sort: SortType) => void;
}) => {
return (
<TradingDropdown
<DropdownMenu
trigger={
<TradingDropdownTrigger data-testid="sort-trigger">
<MarketSelectorButton>
{SortTypeMapping[currentSort]}
</MarketSelectorButton>
</TradingDropdownTrigger>
<DropdownMenuTrigger data-testid="sort-trigger">
<span className="flex justify-between items-center">
{currentSort === SortTypeMapping.None
? t('Sort')
: SortTypeMapping[currentSort]}{' '}
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} />
</span>
</DropdownMenuTrigger>
}
>
<TradingDropdownContent>
<TradingDropdownRadioGroup
<DropdownMenuContent>
<DropdownMenuRadioGroup
value={currentSort}
onValueChange={(value) => onSelect(value as SortType)}
>
{Object.keys(Sort).map((key) => {
return (
<TradingDropdownRadioItem
inset
key={key}
value={key}
data-testid={`sort-item-${key}`}
>
<span className="flex gap-2">
<VegaIcon name={SortIconMapping[key as SortType]} />{' '}
{SortTypeMapping[key as SortType]}
</span>
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
);
})}
</TradingDropdownRadioGroup>
</TradingDropdownContent>
</TradingDropdown>
{Object.keys(Sort)
.filter((s) => s !== Sort.None)
.map((key) => {
return (
<DropdownMenuRadioItem
inset
key={key}
value={key}
data-testid={`sort-item-${key}`}
>
<span className="flex gap-2">
<VegaIcon name={SortIconMapping[key as SortType]} />{' '}
{SortTypeMapping[key as SortType]}
</span>
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
);
})}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -12,10 +12,7 @@ import { useMarketList } from '@vegaprotocol/markets';
import type { Filter } from '../../components/market-selector';
import { subDays } from 'date-fns';
jest.mock('@vegaprotocol/markets', () => ({
...jest.requireActual('@vegaprotocol/markets'),
useMarketList: jest.fn(),
}));
jest.mock('@vegaprotocol/markets');
const mockUseMarketList = useMarketList as jest.Mock;
describe('useMarketSelectorList', () => {
@@ -23,7 +20,7 @@ describe('useMarketSelectorList', () => {
const defaultArgs: Filter = {
searchTerm: '',
product: Product.Future,
sort: Sort.TopTraded,
sort: Sort.None,
assets: [],
};
return renderHook((args) => useMarketSelectorList(args), {
@@ -112,21 +109,21 @@ describe('useMarketSelectorList', () => {
rerender({
searchTerm: '',
product: Product.Spot as 'Future',
sort: Sort.TopTraded,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[1]]);
rerender({
searchTerm: '',
product: Product.Perpetual as 'Future',
sort: Sort.TopTraded,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[2]]);
rerender({
searchTerm: '',
product: Product.All,
sort: Sort.TopTraded,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual(markets);
@@ -192,7 +189,7 @@ describe('useMarketSelectorList', () => {
const { result, rerender } = setup({
searchTerm: '',
product: Product.Future,
sort: Sort.TopTraded,
sort: Sort.None,
assets: ['asset-0'],
});
expect(result.current.markets).toEqual([markets[0], markets[1]]);
@@ -200,7 +197,7 @@ describe('useMarketSelectorList', () => {
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.TopTraded,
sort: Sort.None,
assets: ['asset-0', 'asset-1'],
});
@@ -213,7 +210,7 @@ describe('useMarketSelectorList', () => {
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.TopTraded,
sort: Sort.None,
assets: ['asset-0', 'asset-1', 'asset-2'],
});
@@ -223,7 +220,7 @@ describe('useMarketSelectorList', () => {
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.TopTraded,
sort: Sort.None,
assets: ['asset-invalid'],
});
@@ -278,28 +275,28 @@ describe('useMarketSelectorList', () => {
const { result, rerender } = setup({
searchTerm: 'abc',
product: Product.Future,
sort: Sort.TopTraded,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[0]]);
rerender({
searchTerm: 'def',
product: Product.Future,
sort: Sort.TopTraded,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[1], markets[2]]);
rerender({
searchTerm: 'defg',
product: Product.Future,
sort: Sort.TopTraded,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[2]]);
rerender({
searchTerm: 'zzz',
product: Product.Future,
sort: Sort.TopTraded,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([]);
@@ -308,14 +305,14 @@ describe('useMarketSelectorList', () => {
rerender({
searchTerm: 'aaa',
product: Product.Future,
sort: Sort.TopTraded,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[0]]);
rerender({
searchTerm: 'ggg',
product: Product.Future,
sort: Sort.TopTraded,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([
@@ -325,15 +322,11 @@ describe('useMarketSelectorList', () => {
]);
});
it('sorts by top traded by default', () => {
it('sorts by state and volume by default', () => {
const markets = [
createMarketFragment({
id: 'market-0',
state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment
data: {
markPrice: '1',
},
state: MarketState.STATE_PENDING,
// @ts-ignore candles not on fragment
candles: [
{
@@ -344,10 +337,16 @@ describe('useMarketSelectorList', () => {
createMarketFragment({
id: 'market-1',
state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment
data: {
markPrice: '1',
},
// @ts-ignore candles not on fragment
candles: [
{
volume: '200',
},
],
}),
createMarketFragment({
id: 'market-2',
state: MarketState.STATE_ACTIVE,
// @ts-ignore candles not on fragment
candles: [
{
@@ -356,30 +355,12 @@ describe('useMarketSelectorList', () => {
],
}),
createMarketFragment({
id: 'market-2',
state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment
data: {
markPrice: '1',
},
// @ts-ignore candles not on fragment
candles: [
{
volume: '300',
},
],
}),
createMarketFragment({
state: MarketState.STATE_PENDING,
id: 'market-3',
state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment
data: {
markPrice: '1',
},
// @ts-ignore candles not on fragment
candles: [
{
volume: '400',
volume: '100',
},
],
}),
@@ -394,15 +375,14 @@ describe('useMarketSelectorList', () => {
const { result } = setup({
searchTerm: '',
product: Product.Future,
sort: Sort.TopTraded,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([
markets[3],
markets[1],
markets[2],
markets[0],
markets[1],
markets[3],
]);
});
@@ -1,7 +1,11 @@
import { useMemo } from 'react';
import orderBy from 'lodash/orderBy';
import { MarketState } from '@vegaprotocol/types';
import { calcTradedFactor, useMarketList } from '@vegaprotocol/markets';
import {
calcCandleVolume,
calcTradedFactor,
useMarketList,
} from '@vegaprotocol/markets';
import { priceChangePercentage } from '@vegaprotocol/utils';
import type { Filter } from '../../components/market-selector/market-selector';
import { Sort } from './sort-dropdown';
@@ -56,6 +60,22 @@ export const useMarketSelectorList = ({
return false;
});
if (sort === Sort.None) {
// Sort by market state primarily and AtoZ secondarily
return orderBy(
markets,
[
(m) => MARKET_TEMPLATE.indexOf(m.state),
(m) => {
if (!m.candles?.length) return 0;
const vol = calcCandleVolume(m.candles);
return Number(vol || 0);
},
],
['asc', 'desc']
);
}
if (sort === Sort.Gained || sort === Sort.Lost) {
const dir = sort === Sort.Gained ? 'desc' : 'asc';
return orderBy(
@@ -1,5 +1,5 @@
import { t } from '@vegaprotocol/i18n';
import { TradingButton } from '@vegaprotocol/ui-toolkit';
import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
import { usePositionsStore } from '../positions-container';
export const PositionsMenu = () => {
@@ -7,6 +7,7 @@ export const PositionsMenu = () => {
const toggle = usePositionsStore((store) => store.toggleClosedMarkets);
return (
<TradingButton
intent={Intent.Primary}
size="extra-small"
data-testid="open-transfer"
onClick={toggle}
@@ -24,8 +24,8 @@ export const Settings = () => {
>
<Switch
name="settings-telemetry-switch"
onCheckedChange={(isOn) => setIsApproved(isOn ? 'true' : 'false')}
checked={isApproved === 'true'}
onCheckedChange={(isOn) => setIsApproved(isOn)}
checked={isApproved}
/>
</SettingsGroup>
<SettingsGroup label={t('Toast location')}>
@@ -9,8 +9,7 @@ import {
} from './sidebar';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
jest.mock('../node-health', () => ({
NodeHealthContainer: () => <span data-testid="node-health" />,
@@ -32,20 +31,16 @@ jest.mock('../welcome-dialog', () => ({
GetStarted: () => <div data-testid="get-started" />,
}));
const walletContext = {
pubKeys: [{ publicKey: 'pubkey' }],
} as VegaWalletContextShape;
describe('Sidebar', () => {
it.each(['/markets/all', '/portfolio'])(
'does not render ticket and info',
(path) => {
render(
<VegaWalletContext.Provider value={walletContext}>
<VegaWalletProvider>
<MemoryRouter initialEntries={[path]}>
<Sidebar />
</MemoryRouter>
</VegaWalletContext.Provider>
</VegaWalletProvider>
);
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
@@ -63,11 +58,11 @@ describe('Sidebar', () => {
it('renders ticket and info on market pages', () => {
render(
<VegaWalletContext.Provider value={walletContext}>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
</VegaWalletContext.Provider>
</VegaWalletProvider>
);
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
@@ -84,11 +79,11 @@ describe('Sidebar', () => {
it('renders selected state', async () => {
render(
<VegaWalletContext.Provider value={walletContext}>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
</VegaWalletContext.Provider>
</VegaWalletProvider>
);
const settingsButton = screen.getByTestId(ViewType.Settings);
@@ -112,13 +107,13 @@ describe('Sidebar', () => {
describe('SidebarContent', () => {
it('renders the correct content', () => {
const { container } = render(
<VegaWalletContext.Provider value={walletContext}>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/markets/ABC']}>
<Routes>
<Route path="/markets/:marketId" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
</VegaWalletContext.Provider>
</VegaWalletProvider>
);
expect(container).toBeEmptyDOMElement();
@@ -138,13 +133,13 @@ describe('SidebarContent', () => {
it('closes sidebar if market id is required but not present', () => {
const { container } = render(
<VegaWalletContext.Provider value={walletContext}>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/portfolio']}>
<Routes>
<Route path="/portfolio" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
</VegaWalletContext.Provider>
</VegaWalletProvider>
);
act(() => {
@@ -0,0 +1 @@
export * from './vega-wallet-container';
@@ -0,0 +1,27 @@
import { render, screen } from '@testing-library/react';
import { VegaWalletContainer } from './vega-wallet-container';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import type { PartialDeep } from 'type-fest';
const generateJsx = (context: PartialDeep<VegaWalletContextShape>) => {
return (
<VegaWalletContext.Provider value={context as VegaWalletContextShape}>
<VegaWalletContainer>
<div data-testid="child" />
</VegaWalletContainer>
</VegaWalletContext.Provider>
);
};
describe('VegaWalletContainer', () => {
it('doesnt render children if not connected', () => {
render(generateJsx({ pubKey: null }));
expect(screen.queryByTestId('child')).not.toBeInTheDocument();
});
it('renders children if connected', () => {
render(generateJsx({ pubKey: '0x123' }));
expect(screen.getByTestId('child')).toBeInTheDocument();
});
});
@@ -0,0 +1,35 @@
import type { ReactNode } from 'react';
import { t } from '@vegaprotocol/i18n';
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
interface VegaWalletContainerProps {
children: ReactNode;
}
export const VegaWalletContainer = ({ children }: VegaWalletContainerProps) => {
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
const { pubKey } = useVegaWallet();
if (!pubKey) {
return (
<Splash>
<div className="text-center">
<p className="mb-4" data-testid="connect-vega-wallet-text">
{t('Connect your Vega wallet')}
</p>
<Button
onClick={openVegaWalletDialog}
data-testid="vega-wallet-connect"
>
{t('Connect')}
</Button>
</div>
</Splash>
);
}
return <>{children}</>;
};
@@ -23,20 +23,18 @@ import { Links, Routes } from '../../pages/client-router';
import { useGlobalStore } from '../../stores';
import { useSidebar, ViewType } from '../sidebar';
import * as constants from '../constants';
import { useOnboardingStore } from './welcome-dialog';
interface Props {
lead?: string;
}
const GetStartedButton = ({ step }: { step: OnboardingStep }) => {
const { CHROME_EXTENSION_URL, MOZILLA_EXTENSION_URL } = useEnvironment();
const navigate = useNavigate();
const [, setOnboardingViewed] = useLocalStorage(
constants.ONBOARDING_VIEWED_KEY
);
const dismiss = useOnboardingStore((store) => store.dismiss);
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(
@@ -48,13 +46,7 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => {
openVegaWalletDialog();
};
if (step === OnboardingStep.ONBOARDING_WALLET_STEP) {
return (
<GetWalletButton
className="justify-between"
chromeExtensionUrl={CHROME_EXTENSION_URL}
mozillaExtensionUrl={MOZILLA_EXTENSION_URL}
/>
);
return <GetWalletButton className="justify-between" />;
} else if (step === OnboardingStep.ONBOARDING_CONNECT_STEP) {
buttonText = t('Connect');
} else if (step === OnboardingStep.ONBOARDING_DEPOSIT_STEP) {
@@ -62,7 +54,7 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => {
onClickHandle = () => {
navigate(link);
setView({ type: ViewType.Deposit });
dismiss();
update({ onBoardingDismissed: true });
};
} else if (step === OnboardingStep.ONBOARDING_ORDER_STEP) {
buttonText = t('Dismiss');
@@ -2,35 +2,29 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TelemetryApproval } from './telemetry-approval';
describe('TelemetryApproval', () => {
it('click on buttons should be properly handled', async () => {
const mockSetTelemetryValue = jest.fn();
render(
<TelemetryApproval
telemetryValue="false"
setTelemetryValue={mockSetTelemetryValue}
/>
);
expect(
screen.getByRole('button', { name: 'No thanks' })
).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'No thanks' }));
expect(mockSetTelemetryValue).toHaveBeenCalledWith('false');
expect(screen.getByText('Share data')).toBeInTheDocument();
await userEvent.click(screen.getByText('Share data'));
expect(mockSetTelemetryValue).toHaveBeenCalledWith('true');
});
jest.mock('@vegaprotocol/logger', () => ({
SentryInit: () => undefined,
SentryClose: () => undefined,
}));
it('confirm button should have proper text', async () => {
const mockSetTelemetryValue = jest.fn();
render(
<TelemetryApproval
telemetryValue="true"
setTelemetryValue={mockSetTelemetryValue}
/>
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({ VEGA_ENV: 'test', SENTRY_DSN: 'sentry-dsn' }),
}));
describe('TelemetryApproval', () => {
it('click on checkbox should be properly handled', async () => {
const helpText = 'My help text';
render(<TelemetryApproval helpText={helpText} />);
expect(screen.getByRole('checkbox')).toHaveAttribute(
'data-state',
'unchecked'
);
expect(screen.getByText('Continue sharing data')).toBeInTheDocument();
await userEvent.click(screen.getByText('Continue sharing data'));
expect(mockSetTelemetryValue).toHaveBeenCalledWith('true');
await userEvent.click(screen.getByRole('checkbox'));
expect(screen.getByRole('checkbox')).toHaveAttribute(
'data-state',
'checked'
);
expect(screen.getByText('Share usage data')).toBeInTheDocument();
expect(screen.getByText(helpText)).toBeInTheDocument();
});
});
@@ -1,69 +1,21 @@
import {
Intent,
TradingButton,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { TradingCheckbox } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
interface Props {
telemetryValue: string;
setTelemetryValue: (value: string) => void;
}
export const TelemetryApproval = ({
telemetryValue,
setTelemetryValue,
}: Props) => {
export const TelemetryApproval = ({ helpText }: { helpText: string }) => {
const [isApproved, setIsApproved] = useTelemetryApproval();
return (
<div className="flex flex-col">
<div className="flex flex-col py-3">
<div className="mr-4" role="form">
<p className="mb-4">
{t(
'Help us identify bugs and improve Vega Governance by sharing anonymous usage data.'
)}
</p>
<div className="flex items-start mb-2 gap-3">
<VegaIcon name={VegaIconNames.EYE_OFF} size={18} />
<div className="flex flex-col gap-1">
<h6 className="font-semibold">{t('Anonymous')}</h6>
<p className="text-muted">
{t('Your identity is always anonymous on Vega')}
</p>
</div>
</div>
<div className="flex items-start mb-4 gap-3">
<VegaIcon name={VegaIconNames.COG} size={18} />
<div className="flex flex-col gap-1">
<h6 className="font-semibold">{t('Optional')}</h6>
<p className="text-muted">
{t('You can opt out any time via settings')}
</p>
</div>
</div>
<div className="flex flex-col items-center justify-around gap-2">
<TradingButton
onClick={() => setTelemetryValue('false')}
size="small"
intent={Intent.None}
data-testid="do-not-share-data-button"
fill
>
{t('No thanks')}
</TradingButton>
<TradingButton
onClick={() => setTelemetryValue('true')}
intent={Intent.Info}
data-testid="share-data-button"
size="small"
fill
>
{telemetryValue === 'true'
? t('Continue sharing data')
: t('Share data')}
</TradingButton>
</div>
<TradingCheckbox
label={<span className="text-lg pl-1">{t('Share usage data')}</span>}
checked={isApproved}
name="telemetry-approval"
onCheckedChange={() => setIsApproved(!isApproved)}
/>
</div>
<div className="text-sm text-vega-light-300 dark:text-vega-dark-300 ml-6">
<span>{helpText}</span>
</div>
</div>
);
@@ -5,17 +5,17 @@ import { useNavigate } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import type { ReactNode } from 'react';
import { useOnboardingStore } from './welcome-dialog';
import { useGlobalStore } from '../../stores';
export const WelcomeDialogContent = () => {
const { VEGA_ENV } = useEnvironment();
const dismiss = useOnboardingStore((store) => store.dismiss);
const update = useGlobalStore((store) => store.update);
const navigate = useNavigate();
const browseMarkets = () => {
const link = Links[Routes.MARKETS]();
navigate(link);
dismiss();
update({ onBoardingDismissed: true });
};
const lead =
VEGA_ENV === Networks.MAINNET
@@ -1,9 +1,7 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { Dialog, Intent, useToasts } from '@vegaprotocol/ui-toolkit';
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { useEnvironment } from '@vegaprotocol/environment';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { WelcomeDialogContent } from './welcome-dialog-content';
@@ -14,41 +12,15 @@ import {
OnboardingStep,
} from './use-get-onboarding-step';
import * as constants from '../constants';
import { TelemetryApproval } from './telemetry-approval';
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
import { useCallback } from 'react';
const ONBOARDING_STORAGE_KEY = 'vega_onboarding_dismiss_store';
export const useOnboardingStore = create<{
dismissed: boolean;
dismiss: () => void;
}>()(
persist(
(set) => ({
dismissed: false,
dismiss: () => set(() => ({ dismissed: true })),
}),
{
name: ONBOARDING_STORAGE_KEY,
}
)
);
const TELEMETRY_APPROVAL_TOAST_ID = 'telemetry_tost_id';
export const WelcomeDialog = () => {
const { VEGA_ENV } = useEnvironment();
const navigate = useNavigate();
const [telemetryValue, setTelemetryValue, isTelemetryNeeded, closeTelemetry] =
useTelemetryApproval();
const [onBoardingViewed] = useLocalStorage(constants.ONBOARDING_VIEWED_KEY);
const dismiss = useOnboardingStore((store) => store.dismiss);
const dismissed = useOnboardingStore((store) => store.dismissed);
const update = useGlobalStore((store) => store.update);
const dismissed = useGlobalStore((store) => store.onBoardingDismissed);
const currentStep = useGetOnboardingStep();
const isTelemetryPopupNeeded =
isTelemetryNeeded &&
(onBoardingViewed === 'true' ||
currentStep > OnboardingStep.ONBOARDING_ORDER_STEP);
const navigate = useNavigate();
const isOnboardingDialogNeeded =
onBoardingViewed !== 'true' &&
currentStep &&
@@ -57,58 +29,12 @@ export const WelcomeDialog = () => {
const marketId = useGlobalStore((store) => store.marketId);
const onClose = () => {
if (isTelemetryPopupNeeded) {
closeTelemetry();
} else {
const link = marketId
? Links[Routes.MARKET](marketId)
: Links[Routes.HOME]();
navigate(link);
dismiss();
}
const link = marketId
? Links[Routes.MARKET](marketId)
: Links[Routes.HOME]();
navigate(link);
update({ onBoardingDismissed: true });
};
const [setToast, hasToast, removeToast] = useToasts((store) => [
store.setToast,
store.hasToast,
store.remove,
]);
const onApprovalClose = useCallback(() => {
closeTelemetry();
removeToast(TELEMETRY_APPROVAL_TOAST_ID);
}, [removeToast, closeTelemetry]);
const setTelemetryApprovalAndClose = useCallback(
(value: string) => {
setTelemetryValue(value);
onApprovalClose();
},
[setTelemetryValue, onApprovalClose]
);
if (isTelemetryPopupNeeded) {
const toast: Toast = {
id: TELEMETRY_APPROVAL_TOAST_ID,
intent: Intent.Primary,
content: (
<>
<h3 className="mb-1 text-sm uppercase">
{t('Improve vega console')}
</h3>
<TelemetryApproval
telemetryValue={telemetryValue}
setTelemetryValue={setTelemetryApprovalAndClose}
/>
</>
),
onClose: onApprovalClose,
};
if (!hasToast(TELEMETRY_APPROVAL_TOAST_ID)) {
setToast(toast);
}
return;
}
const title = (
<span className="font-alpha calt" data-testid="welcome-title">
{t('Console')}{' '}
@@ -1,5 +1,5 @@
import { t } from '@vegaprotocol/i18n';
import { TradingButton } from '@vegaprotocol/ui-toolkit';
import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
import { ViewType, useSidebar } from '../sidebar';
export const WithdrawalsMenu = () => {
@@ -7,6 +7,7 @@ export const WithdrawalsMenu = () => {
return (
<TradingButton
intent={Intent.Primary}
size="extra-small"
onClick={() => setView({ type: ViewType.Withdraw })}
data-testid="withdraw-dialog-button"
@@ -1,40 +1,19 @@
import { renderHook, act, waitFor } from '@testing-library/react';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { SentryInit, SentryClose } from '@vegaprotocol/logger';
import {
STORAGE_KEY,
STORAGE_SECOND_KEY,
useTelemetryApproval,
} from './use-telemetry-approval';
import { Networks } from '@vegaprotocol/environment';
import { STORAGE_KEY, useTelemetryApproval } from './use-telemetry-approval';
const mockSetValue = jest.fn();
let mockStorageHookApprovalResult: [string | null, jest.Mock] = [
null,
mockSetValue,
];
const mockSetSecondValue = jest.fn();
let mockStorageHookViewedResult: [string | null, jest.Mock] = [
null,
mockSetSecondValue,
];
const mockRemoveValue = jest.fn();
jest.mock('@vegaprotocol/logger');
jest.mock('@vegaprotocol/react-helpers', () => ({
...jest.requireActual('@vegaprotocol/react-helpers'),
useLocalStorage: jest.fn((key: string) => {
if (key === 'vega_telemetry_approval') {
return mockStorageHookApprovalResult;
}
return mockStorageHookViewedResult;
}),
useLocalStorage: jest
.fn()
.mockImplementation(() => [false, mockSetValue, mockRemoveValue]),
}));
let mockVegaEnv = 'test';
jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'),
useEnvironment: jest.fn(() => ({
VEGA_ENV: mockVegaEnv,
SENTRY_DSN: 'sentry-dsn',
})),
useEnvironment: () => ({ VEGA_ENV: 'test', SENTRY_DSN: 'sentry-dsn' }),
}));
describe('useTelemetryApproval', () => {
@@ -42,71 +21,32 @@ describe('useTelemetryApproval', () => {
jest.clearAllMocks();
});
it('when empty hook should return proper array', () => {
it('hook should return proper array', () => {
const { result } = renderHook(() => useTelemetryApproval());
expect(result.current[0]).toEqual('');
expect(result.current[0]).toEqual(false);
expect(result.current[1]).toEqual(expect.any(Function));
expect(result.current[2]).toEqual(true);
expect(result.current[3]).toEqual(expect.any(Function));
expect(useLocalStorage).toHaveBeenCalledWith(STORAGE_KEY);
expect(useLocalStorage).toHaveBeenCalledWith(STORAGE_SECOND_KEY);
expect(mockSetValue).toHaveBeenCalledWith('true');
expect(mockSetSecondValue).not.toHaveBeenCalledWith('true');
});
it('when approval not empty but viewed is empty should return proper array', () => {
mockStorageHookApprovalResult = ['false', mockSetValue];
const { result } = renderHook(() => useTelemetryApproval());
expect(result.current[0]).toEqual('false');
expect(result.current[1]).toEqual(expect.any(Function));
expect(result.current[2]).toEqual(true);
expect(result.current[3]).toEqual(expect.any(Function));
expect(useLocalStorage).toHaveBeenCalledWith(STORAGE_KEY);
expect(useLocalStorage).toHaveBeenCalledWith(STORAGE_SECOND_KEY);
expect(mockSetValue).not.toHaveBeenCalled();
expect(mockSetSecondValue).not.toHaveBeenCalled();
});
it('when NOT empty hook should return proper array', () => {
mockStorageHookApprovalResult = ['false', mockSetValue];
mockStorageHookViewedResult = ['true', mockSetSecondValue];
const { result } = renderHook(() => useTelemetryApproval());
expect(result.current[0]).toEqual('false');
expect(result.current[1]).toEqual(expect.any(Function));
expect(result.current[2]).toEqual(false);
expect(result.current[3]).toEqual(expect.any(Function));
expect(useLocalStorage).toHaveBeenCalledWith(STORAGE_KEY);
expect(mockSetValue).not.toHaveBeenCalled();
});
it('on mainnet hook should init properly', () => {
mockStorageHookApprovalResult = [null, mockSetValue];
mockVegaEnv = Networks.MAINNET;
renderHook(() => useTelemetryApproval());
expect(mockSetValue).toHaveBeenCalledWith('false');
});
it('hook should init stuff properly', async () => {
const { result } = renderHook(() => useTelemetryApproval());
await act(() => {
result.current[1]('true');
result.current[1](true);
});
await waitFor(() => {
expect(SentryInit).toHaveBeenCalled();
expect(mockSetValue).toHaveBeenCalledWith('true');
expect(mockSetSecondValue).toHaveBeenCalledWith('true');
expect(mockSetValue).toHaveBeenCalledWith('1');
});
});
it('hook should close stuff properly', async () => {
const { result } = renderHook(() => useTelemetryApproval());
await act(() => {
result.current[1]('false');
result.current[1](false);
});
await waitFor(() => {
expect(SentryClose).toHaveBeenCalled();
expect(mockSetValue).toHaveBeenCalledWith('false');
expect(mockSetSecondValue).toHaveBeenCalledWith('true');
expect(mockRemoveValue).toHaveBeenCalledWith();
});
});
});
@@ -1,51 +1,25 @@
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { useCallback, useEffect, useState } from 'react';
import { useCallback } from 'react';
import { SentryInit, SentryClose } from '@vegaprotocol/logger';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { useEnvironment } from '@vegaprotocol/environment';
export const STORAGE_KEY = 'vega_telemetry_approval';
export const STORAGE_SECOND_KEY = 'vega_telemetry_viewed';
export const useTelemetryApproval = (): [
value: string,
setValue: (value: string) => void,
shouldOpen: boolean,
close: () => void
value: boolean,
setValue: (value: boolean) => void
] => {
const { VEGA_ENV, SENTRY_DSN } = useEnvironment();
const defaultTelemetryValue =
VEGA_ENV === Networks.MAINNET ? 'false' : 'true';
const [value, setValue] = useLocalStorage(STORAGE_KEY);
const [viewedValue, setViewedValue] = useLocalStorage(STORAGE_SECOND_KEY);
const [shouldOpen, setShouldOpen] = useState(!value || !viewedValue);
const close = useCallback(() => {
setShouldOpen(false);
setViewedValue('true');
}, [setViewedValue]);
const manageValue = useCallback(
(value: string) => {
if (value === 'true' && SENTRY_DSN) {
const [value, setValue, removeValue] = useLocalStorage(STORAGE_KEY);
const setApprove = useCallback(
(value: boolean) => {
if (value && SENTRY_DSN) {
SentryInit(SENTRY_DSN, VEGA_ENV);
return setValue('true');
return setValue('1');
}
SentryClose();
setValue('false');
removeValue();
},
[setValue, SENTRY_DSN, VEGA_ENV]
[setValue, removeValue, SENTRY_DSN, VEGA_ENV]
);
const setTelemetryValue = useCallback(
(value: string) => {
setShouldOpen(false);
setViewedValue('true');
manageValue(value);
},
[manageValue, setViewedValue]
);
useEffect(() => {
if (!value) {
manageValue(defaultTelemetryValue);
}
}, [value, manageValue, defaultTelemetryValue]);
return [value || '', setTelemetryValue, shouldOpen, close];
return [Boolean(value), setApprove];
};
+5 -4
View File
@@ -1,4 +1,4 @@
import { FLAGS } from '@vegaprotocol/environment';
import { ENV } from '@vegaprotocol/environment';
import {
JsonRpcConnector,
ViewConnector,
@@ -18,9 +18,10 @@ if (typeof window !== 'undefined') {
view = new ViewConnector();
}
export const snap = FLAGS.METAMASK_SNAPS
? new SnapConnector(DEFAULT_SNAP_ID)
: undefined;
export const snap = new SnapConnector(
ENV.VEGA_URL ? new URL(ENV.VEGA_URL).origin : undefined,
DEFAULT_SNAP_ID
);
export const Connectors = {
injected,
+2
View File
@@ -4,6 +4,7 @@ import produce from 'immer';
interface GlobalStore {
marketId: string | null;
onBoardingDismissed: boolean;
eagerConnecting: boolean;
update: (store: Partial<Omit<GlobalStore, 'update'>>) => void;
}
@@ -15,6 +16,7 @@ interface PageTitleStore {
export const useGlobalStore = create<GlobalStore>()((set) => ({
marketId: LocalStorage.getItem('marketId') || null,
onBoardingDismissed: false,
eagerConnecting: false,
update: (newState) => {
set(
@@ -2,8 +2,8 @@ import { ETHERSCAN_ADDRESS, useEtherscanLink } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import {
ActionsDropdown,
TradingDropdownCopyItem,
TradingDropdownItem,
DropdownMenuCopyItem,
DropdownMenuItem,
Link,
VegaIcon,
VegaIconNames,
@@ -30,49 +30,49 @@ export const AccountsActionsDropdown = ({
return (
<ActionsDropdown>
<TradingDropdownItem
<DropdownMenuItem
key={'deposit'}
data-testid="deposit"
onClick={onClickDeposit}
>
<VegaIcon name={VegaIconNames.DEPOSIT} size={16} />
{t('Deposit')}
</TradingDropdownItem>
<TradingDropdownItem
</DropdownMenuItem>
<DropdownMenuItem
key={'withdraw'}
data-testid="withdraw"
onClick={onClickWithdraw}
>
<VegaIcon name={VegaIconNames.WITHDRAW} size={16} />
{t('Withdraw')}
</TradingDropdownItem>
<TradingDropdownItem
</DropdownMenuItem>
<DropdownMenuItem
key={'transfer'}
data-testid="transfer"
onClick={onClickTransfer}
>
<VegaIcon name={VegaIconNames.TRANSFER} size={16} />
{t('Transfer')}
</TradingDropdownItem>
<TradingDropdownItem
</DropdownMenuItem>
<DropdownMenuItem
key={'breakdown'}
data-testid="breakdown"
onClick={onClickBreakdown}
>
<VegaIcon name={VegaIconNames.BREAKDOWN} size={16} />
{t('View usage breakdown')}
</TradingDropdownItem>
<TradingDropdownItem
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
openAssetDialog(assetId, e.target as HTMLElement);
}}
>
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View asset details')}
</TradingDropdownItem>
<TradingDropdownCopyItem value={assetId} text={t('Copy asset ID')} />
</DropdownMenuItem>
<DropdownMenuCopyItem value={assetId} text={t('Copy asset ID')} />
{assetContractAddress && (
<TradingDropdownItem>
<DropdownMenuItem>
<Link
href={etherscanLink(
ETHERSCAN_ADDRESS.replace(':hash', assetContractAddress)
@@ -84,7 +84,7 @@ export const AccountsActionsDropdown = ({
{t('View on Etherscan')}
</span>
</Link>
</TradingDropdownItem>
</DropdownMenuItem>
)}
</ActionsDropdown>
);
+11 -15
View File
@@ -11,13 +11,9 @@ import type {
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { COL_DEFS } from '@vegaprotocol/datagrid';
import {
Intent,
TradingButton,
VegaIcon,
VegaIconNames,
TooltipCellComponent,
} from '@vegaprotocol/ui-toolkit';
import { Button, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type {
IGetRowsParams,
@@ -190,7 +186,7 @@ export const AccountTable = ({
) : (
<>
<span className="underline">{valueFormatted}</span>
<span className="inline-block ml-2 w-14 text-muted">
<span className="ml-2 inline-block w-14 text-muted">
{t('0.00%')}
</span>
</>
@@ -252,26 +248,26 @@ export const AccountTable = ({
colId: 'accounts-actions',
field: 'asset.id',
...COL_DEFS.actions,
minWidth: showDepositButton ? 105 : COL_DEFS.actions.minWidth,
maxWidth: showDepositButton ? 105 : COL_DEFS.actions.maxWidth,
minWidth: showDepositButton ? 130 : COL_DEFS.actions.minWidth,
maxWidth: showDepositButton ? 130 : COL_DEFS.actions.maxWidth,
cellRenderer: ({
value: assetId,
node,
}: VegaICellRendererParams<AccountFields, 'asset.id'>) => {
if (!assetId) return null;
if (node.rowPinned && node.data?.balance === '0') {
if (node.rowPinned && node.data?.total === '0') {
return (
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
<TradingButton
size="extra-small"
intent={Intent.Primary}
<Button
size="xs"
variant="primary"
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(assetId);
}}
>
<VegaIcon name={VegaIconNames.DEPOSIT} /> {t('Deposit')}
</TradingButton>
</Button>
</CenteredGridCellWrapper>
);
}
+8 -8
View File
@@ -8,6 +8,7 @@ import {
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
Button,
TradingFormGroup,
TradingInput,
TradingInputError,
@@ -15,7 +16,6 @@ import {
TradingSelect,
Tooltip,
TradingCheckbox,
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import type { Transfer } from '@vegaprotocol/wallet';
import { normalizeTransfer } from '@vegaprotocol/wallet';
@@ -276,9 +276,9 @@ export const TransferForm = ({
decimals={asset?.decimals}
/>
)}
<TradingButton type="submit" fill={true}>
<Button type="submit" variant="primary" fill={true}>
{t('Confirm transfer')}
</TradingButton>
</Button>
</form>
);
};
@@ -309,8 +309,8 @@ export const TransferFee = ({
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
return (
<div className="flex flex-col mb-4 text-xs gap-2">
<div className="flex flex-wrap items-center justify-between gap-1">
<div className="mb-4 flex flex-col gap-2 text-xs">
<div className="flex justify-between gap-1 items-center flex-wrap">
<Tooltip
description={t(
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to %s`,
@@ -324,7 +324,7 @@ export const TransferFee = ({
{formatNumber(fee, decimals)}
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-1">
<div className="flex justify-between gap-1 items-center flex-wrap">
<Tooltip
description={t(
`The total amount to be transferred (without the fee)`
@@ -337,7 +337,7 @@ export const TransferFee = ({
{formatNumber(amount, decimals)}
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-1">
<div className="flex justify-between gap-1 items-center flex-wrap">
<Tooltip
description={t(
`The total amount taken from your account. The amount to be transferred plus the fee.`
@@ -384,7 +384,7 @@ export const AddressField = ({
setIsInput((curr) => !curr);
onChange();
}}
className="absolute top-0 right-0 ml-auto text-sm underline"
className="ml-auto text-sm absolute top-0 right-0 underline"
>
{isInput ? t('Select from wallet') : t('Enter manually')}
</button>
+3 -4
View File
@@ -2,10 +2,9 @@ import { t } from '@vegaprotocol/i18n';
import {
Button,
Dialog,
Icon,
Splash,
SyntaxHighlighter,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { create } from 'zustand';
import { AssetDetailsTable } from './asset-details-table';
@@ -83,7 +82,7 @@ export const AssetDetailsDialog = ({
return (
<Dialog
title={title}
icon={<VegaIcon name={VegaIconNames.INFO} />}
icon={<Icon name="info-sign"></Icon>}
open={open}
onChange={(isOpen) => onChange(isOpen)}
onCloseAutoFocus={(e) => {
@@ -98,7 +97,7 @@ export const AssetDetailsDialog = ({
}}
>
{content}
<p className="my-4 text-xs">
<p className="text-sm my-4">
{t(
'There is 1 unit of the settlement asset (%s) to every 1 quote unit.',
[assetSymbol]
+4 -7
View File
@@ -3,8 +3,7 @@ import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type * as Schema from '@vegaprotocol/types';
import type { KeyValueTableRowProps } from '@vegaprotocol/ui-toolkit';
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { CopyWithTooltip, truncateMiddle } from '@vegaprotocol/ui-toolkit';
import { CopyWithTooltip, Icon } from '@vegaprotocol/ui-toolkit';
import {
KeyValueTable,
KeyValueTableRow,
@@ -57,7 +56,7 @@ export const rows: Rows = [
key: AssetDetail.ID,
label: t('ID'),
tooltip: '',
value: (asset) => truncateMiddle(asset.id),
value: (asset) => asset.id,
},
{
key: AssetDetail.TYPE,
@@ -110,12 +109,10 @@ export const rows: Rows = [
return (
<>
<EtherscanLink address={asset.source.contractAddress}>
{truncateMiddle(asset.source.contractAddress)}
</EtherscanLink>{' '}
<EtherscanLink address={asset.source.contractAddress} />{' '}
<CopyWithTooltip text={asset.source.contractAddress}>
<button title={t('Copy address to clipboard')}>
<VegaIcon size={14} name={VegaIconNames.COPY} />
<Icon size={3} name="duplicate" />
</button>
</CopyWithTooltip>
</>
+2 -2
View File
@@ -1,4 +1,4 @@
import { TradingOption, truncateMiddle } from '@vegaprotocol/ui-toolkit';
import { TradingOption } from '@vegaprotocol/ui-toolkit';
import type { AssetFieldsFragment } from './__generated__/Asset';
import classNames from 'classnames';
import { t } from '@vegaprotocol/i18n';
@@ -45,7 +45,7 @@ export const AssetOption = ({ asset, balance }: AssetOptionProps) => {
{balance}
<div className="text-[12px] font-mono w-full text-left break-all">
<span className="text-vega-light-300 dark:text-vega-dark-300">
{truncateMiddle(asset.id)}
{asset.id}
</span>
</div>
</div>
@@ -3,31 +3,15 @@ import userEvent from '@testing-library/user-event';
import { CandlesMenu } from './candles-menu';
describe('CandlesMenu', () => {
it('should render with the correct default studies', async () => {
it('should render with volume study showing by default', async () => {
render(<CandlesMenu />);
await userEvent.click(
screen.getByRole('button', {
name: 'Studies',
screen.getByText('Studies', {
selector: '[type="button"]',
})
);
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(screen.getByText('Volume')).toHaveAttribute('data-state', 'checked');
expect(screen.getByText('MACD')).toHaveAttribute('data-state', 'checked');
});
it('should render with the correct default overlays', async () => {
render(<CandlesMenu />);
await userEvent.click(
screen.getByRole('button', {
name: 'Overlays',
})
);
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(screen.getByText('Moving average')).toHaveAttribute(
'data-state',
'checked'
);
});
});
+51 -61
View File
@@ -10,14 +10,13 @@ import {
studyLabels,
} from 'pennant';
import {
TradingButton,
TradingDropdown,
TradingDropdownCheckboxItem,
TradingDropdownContent,
TradingDropdownItemIndicator,
TradingDropdownRadioGroup,
TradingDropdownRadioItem,
TradingDropdownTrigger,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItemIndicator,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
Icon,
} from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@blueprintjs/icons';
@@ -45,76 +44,69 @@ export const CandlesMenu = () => {
} = useCandlesChartSettings();
const triggerClasses = 'text-xs';
const contentAlign = 'end';
const triggerButtonProps = { size: 'extra-small' } as const;
return (
<>
<TradingDropdown
<DropdownMenu
trigger={
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
{t(`Interval: ${intervalLabels[interval]}`)}
</TradingButton>
</TradingDropdownTrigger>
<DropdownMenuTrigger className={triggerClasses}>
{t(`Interval: ${intervalLabels[interval]}`)}
</DropdownMenuTrigger>
}
>
<TradingDropdownContent align={contentAlign}>
<TradingDropdownRadioGroup
<DropdownMenuContent align={contentAlign}>
<DropdownMenuRadioGroup
value={interval}
onValueChange={(value) => {
setInterval(value as Interval);
}}
>
{Object.values(Interval).map((timeInterval) => (
<TradingDropdownRadioItem
<DropdownMenuRadioItem
key={timeInterval}
inset
value={timeInterval}
>
{intervalLabels[timeInterval]}
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
))}
</TradingDropdownRadioGroup>
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
<Icon name={chartTypeIcon.get(chartType) as IconName} />
</TradingButton>
</TradingDropdownTrigger>
<DropdownMenuTrigger className={triggerClasses}>
<Icon name={chartTypeIcon.get(chartType) as IconName} />
</DropdownMenuTrigger>
}
>
<TradingDropdownContent align={contentAlign}>
<TradingDropdownRadioGroup
<DropdownMenuContent align={contentAlign}>
<DropdownMenuRadioGroup
value={chartType}
onValueChange={(value) => {
setType(value as ChartType);
}}
>
{Object.values(ChartType).map((type) => (
<TradingDropdownRadioItem key={type} inset value={type}>
<DropdownMenuRadioItem key={type} inset value={type}>
{chartTypeLabels[type]}
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
))}
</TradingDropdownRadioGroup>
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
{t('Overlays')}
</TradingButton>
</TradingDropdownTrigger>
<DropdownMenuTrigger className={triggerClasses}>
{t('Overlays')}
</DropdownMenuTrigger>
}
>
<TradingDropdownContent align={contentAlign}>
<DropdownMenuContent align={contentAlign}>
{Object.values(Overlay).map((overlay) => (
<TradingDropdownCheckboxItem
<DropdownMenuCheckboxItem
key={overlay}
checked={overlays.includes(overlay)}
onCheckedChange={() => {
@@ -129,23 +121,21 @@ export const CandlesMenu = () => {
}}
>
{overlayLabels[overlay]}
<TradingDropdownItemIndicator />
</TradingDropdownCheckboxItem>
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
{t('Studies')}
</TradingButton>
</TradingDropdownTrigger>
<DropdownMenuTrigger className={triggerClasses}>
{t('Studies')}
</DropdownMenuTrigger>
}
>
<TradingDropdownContent align={contentAlign}>
<DropdownMenuContent align={contentAlign}>
{Object.values(Study).map((study) => (
<TradingDropdownCheckboxItem
<DropdownMenuCheckboxItem
key={study}
checked={studies.includes(study)}
onCheckedChange={() => {
@@ -160,11 +150,11 @@ export const CandlesMenu = () => {
}}
>
{studyLabels[study]}
<TradingDropdownItemIndicator />
</TradingDropdownCheckboxItem>
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
))}
</TradingDropdownContent>
</TradingDropdown>
</DropdownMenuContent>
</DropdownMenu>
</>
);
};
@@ -15,8 +15,8 @@ interface StoredSettings {
const DEFAULT_CHART_SETTINGS = {
interval: Interval.I15M,
type: ChartType.CANDLE,
overlays: [Overlay.MOVING_AVERAGE],
studies: [Study.MACD, Study.VOLUME],
overlays: [],
studies: [Study.VOLUME],
};
export const useCandlesChartSettingsStore = create<
@@ -65,8 +65,6 @@ export function addSetVegaWallet() {
Cypress.Commands.add('setVegaWallet', () => {
cy.window().then((win) => {
win.localStorage.setItem('vega_onboarding_viewed', 'true');
win.localStorage.setItem('vega_telemetry_approval', 'false');
win.localStorage.setItem('vega_telemetry_viewed', 'true');
win.localStorage.setItem(
'vega_wallet_config',
JSON.stringify({
@@ -83,8 +81,6 @@ export function addSetOnBoardingViewed() {
Cypress.Commands.add('setOnBoardingViewed', () => {
cy.window().then((win) => {
win.localStorage.setItem('vega_onboarding_viewed', 'true');
win.localStorage.setItem('vega_telemetry_approval', 'false');
win.localStorage.setItem('vega_telemetry_viewed', 'true');
});
});
}
@@ -0,0 +1,25 @@
import { t } from '@vegaprotocol/i18n';
import { Side } from '@vegaprotocol/types';
import classNames from 'classnames';
interface Props {
side: Side;
label?: string;
}
export const DealTicketButton = ({ side, label }: Props) => {
const buttonClasses = classNames(
'px-10 py-2 uppercase rounded-md text-white w-full',
{
'bg-market-red': side === Side.SIDE_SELL,
'bg-market-green-550': side === Side.SIDE_BUY,
}
);
return (
<div className="mb-2">
<button type="submit" data-testid="place-order" className={buttonClasses}>
{label || t('Place order')}
</button>
</div>
);
};
@@ -1,4 +1,7 @@
import { useCallback, useState } from 'react';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import classnames from 'classnames';
import type { ReactNode } from 'react';
import { t } from '@vegaprotocol/i18n';
import { FeesBreakdown } from '@vegaprotocol/markets';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
@@ -13,6 +16,7 @@ import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
import {
NOTIONAL_SIZE_TOOLTIP_TEXT,
MARGIN_DIFF_TOOLTIP_TEXT,
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
TOTAL_MARGIN_AVAILABLE,
@@ -21,54 +25,114 @@ import {
MARGIN_ACCOUNT_TOOLTIP_TEXT,
} from '../../constants';
import { useEstimateFees } from '../../hooks';
import { KeyValue } from './key-value';
const emptyValue = '-';
export interface DealTicketFeeDetailPros {
label: string;
value?: string | null | undefined;
symbol: string;
indent?: boolean | undefined;
labelDescription?: ReactNode;
formattedValue?: string;
onClick?: () => void;
}
export const DealTicketFeeDetail = ({
label,
value,
labelDescription,
symbol,
indent,
onClick,
formattedValue,
}: DealTicketFeeDetailPros) => {
const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`;
const valueElement = onClick ? (
<button onClick={onClick} className="text-muted">
{displayValue}
</button>
) : (
<div className="text-muted">{displayValue}</div>
);
return (
<div
data-testid={
'deal-ticket-fee-' + label.toLocaleLowerCase().replace(/\s/g, '-')
}
key={typeof label === 'string' ? label : 'value-dropdown'}
className={classnames(
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
{ 'ml-2': indent }
)}
>
<Tooltip description={labelDescription}>
<div>{label}</div>
</Tooltip>
<Tooltip description={`${value ?? '-'} ${symbol || ''}`}>
{valueElement}
</Tooltip>
</div>
);
};
export interface DealTicketFeeDetailsProps {
assetSymbol: string;
order: OrderSubmissionBody['orderSubmission'];
market: Market;
notionalSize: string | null;
}
export const DealTicketFeeDetails = ({
assetSymbol,
order,
market,
notionalSize,
}: DealTicketFeeDetailsProps) => {
const feeEstimate = useEstimateFees(order);
const { settlementAsset: asset } =
market.tradableInstrument.instrument.product;
const { decimals: assetDecimals, quantum } = asset;
const marketDecimals = market.decimalPlaces;
const quoteName = market.tradableInstrument.instrument.product.quoteName;
return (
<KeyValue
label={t('Fees')}
value={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
}
formattedValue={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
}
labelDescription={
<>
<span>
{t(
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
)}
</span>
<FeesBreakdown
fees={feeEstimate?.fees}
feeFactors={market.fees.factors}
symbol={assetSymbol}
decimals={assetDecimals}
/>
</>
}
symbol={assetSymbol}
/>
<>
<DealTicketFeeDetail
label={t('Notional')}
value={formatValue(notionalSize, marketDecimals)}
formattedValue={formatValue(notionalSize, marketDecimals)}
symbol={quoteName}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
/>
<DealTicketFeeDetail
label={t('Fees')}
value={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
}
formattedValue={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
}
labelDescription={
<>
<span>
{t(
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
)}
</span>
<FeesBreakdown
fees={feeEstimate?.fees}
feeFactors={market.fees.factors}
symbol={assetSymbol}
decimals={assetDecimals}
/>
</>
}
symbol={assetSymbol}
/>
</>
);
};
@@ -145,7 +209,7 @@ export const DealTicketMarginDetails = ({
BigInt(marginAccountBalance);
deductionFromCollateral = (
<KeyValue
<DealTicketFeeDetail
indent
label={t('Deduction from collateral')}
value={formatRange(
@@ -172,7 +236,7 @@ export const DealTicketMarginDetails = ({
/>
);
projectedMargin = (
<KeyValue
<DealTicketFeeDetail
label={t('Projected margin')}
value={formatRange(
marginEstimate?.bestCase.initialLevel,
@@ -244,7 +308,7 @@ export const DealTicketMarginDetails = ({
return (
<>
<KeyValue
<DealTicketFeeDetail
label={t('Margin required')}
value={formatRange(
marginRequiredBestCase,
@@ -260,7 +324,7 @@ export const DealTicketMarginDetails = ({
labelDescription={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}
symbol={assetSymbol}
/>
<KeyValue
<DealTicketFeeDetail
label={t('Total margin available')}
indent
value={formatValue(totalMarginAvailable, assetDecimals)}
@@ -278,7 +342,7 @@ export const DealTicketMarginDetails = ({
)}
/>
{deductionFromCollateral}
<KeyValue
<DealTicketFeeDetail
label={t('Current margin allocation')}
indent
onClick={
@@ -294,7 +358,7 @@ export const DealTicketMarginDetails = ({
)}
/>
{projectedMargin}
<KeyValue
<DealTicketFeeDetail
label={t('Liquidation price estimate')}
value={liquidationPriceEstimate}
formattedValue={liquidationPriceEstimate}
@@ -32,7 +32,7 @@ export const DealTicketSizeIceberg = ({
const renderPeakSizeError = () => {
if (peakSizeError) {
return (
<TradingInputError testId="deal-ticket-peak-error-message">
<TradingInputError testId="deal-ticket-peak-error-message-size-limit">
{peakSizeError}
</TradingInputError>
);
@@ -44,7 +44,7 @@ export const DealTicketSizeIceberg = ({
const renderMinimumSizeError = () => {
if (minimumVisibleSizeError) {
return (
<TradingInputError testId="deal-ticket-minimum-error-message">
<TradingInputError testId="deal-ticket-minimum-error-message-size-limit">
{minimumVisibleSizeError}
</TradingInputError>
);
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { generateMarket } from '../../test-helpers';
import { StopOrder } from './deal-ticket-stop-order';
@@ -12,7 +12,6 @@ import {
useDealTicketFormValues,
} from '../../hooks/use-form-values';
import type { FeatureFlags } from '@vegaprotocol/environment';
import { formatForInput } from '@vegaprotocol/utils';
jest.mock('zustand');
jest.mock('./deal-ticket-fee-details', () => ({
@@ -58,7 +57,7 @@ const orderSideBuy = 'order-side-SIDE_BUY';
const orderSideSell = 'order-side-SIDE_SELL';
const triggerDirectionRisesAbove = 'triggerDirection-risesAbove';
const triggerDirectionFallsBelow = 'triggerDirection-fallsBelow';
// const triggerDirectionFallsBelow = 'triggerDirection-fallsBelow';
const expiryStrategySubmit = 'expiryStrategy-submit';
const expiryStrategyCancel = 'expiryStrategy-cancel';
@@ -66,7 +65,6 @@ const expiryStrategyCancel = 'expiryStrategy-cancel';
const triggerTypePrice = 'triggerType-price';
const triggerTypeTrailingPercentOffset = 'triggerType-trailingPercentOffset';
const oco = 'oco';
const expire = 'expire';
const datePicker = 'date-picker-field';
const timeInForce = 'order-tif';
@@ -78,8 +76,6 @@ const triggerPriceWarningMessage = 'stop-order-warning-message-trigger-price';
const triggerTrailingPercentOffsetErrorMessage =
'stop-order-error-message-trigger-trailing-percent-offset';
const ocoPostfix = (id: string, postfix = true) => (postfix ? `${id}-oco` : id);
describe('StopOrder', () => {
beforeEach(() => {
localStorage.clear();
@@ -111,7 +107,6 @@ describe('StopOrder', () => {
'checked'
);
expect(screen.getByTestId(expire).dataset.state).toEqual('unchecked');
expect(screen.getByTestId(oco).dataset.state).toEqual('unchecked');
await userEvent.click(screen.getByTestId(expire));
await waitFor(() => {
expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual(
@@ -120,32 +115,6 @@ describe('StopOrder', () => {
});
});
it('calculate notional for market limit', async () => {
render(generateJsx());
await userEvent.type(screen.getByTestId(sizeInput), '10');
await userEvent.type(screen.getByTestId(priceInput), '10');
expect(screen.getByTestId('deal-ticket-fee-notional')).toHaveTextContent(
'Notional100.00 BTC'
);
});
it('calculates notional for limit order', async () => {
render(generateJsx());
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeMarket));
await userEvent.type(screen.getByTestId(sizeInput), '10');
// price trigger is selected but it's empty, calculate base on size and marketPrice prop
expect(screen.getByTestId('deal-ticket-fee-notional')).toHaveTextContent(
'Notional20.00 BTC'
);
await userEvent.type(screen.getByTestId(triggerPriceInput), '3');
// calculate base on size and price trigger
expect(screen.getByTestId('deal-ticket-fee-notional')).toHaveTextContent(
'Notional30.00 BTC'
);
});
it('should use local storage state for initial values', async () => {
const values: Partial<StopOrderFormValues> = {
type: Schema.OrderType.TYPE_LIMIT,
@@ -156,11 +125,6 @@ describe('StopOrder', () => {
expire: true,
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS,
expiresAt: '2023-07-27T16:43:27.000',
oco: true,
ocoType: Schema.OrderType.TYPE_LIMIT,
ocoSize: '0.2',
ocoPrice: '300.23',
ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
};
useDealTicketFormValues.setState({
@@ -179,22 +143,10 @@ describe('StopOrder', () => {
expect(screen.getByTestId(sizeInput)).toHaveDisplayValue(
values.size as string
);
expect(screen.getByTestId(timeInForce)).toHaveValue(values.timeInForce);
expect(screen.getByTestId('order-tif')).toHaveValue(values.timeInForce);
expect(screen.getByTestId(priceInput)).toHaveDisplayValue(
values.price as string
);
expect(screen.getByTestId(ocoPostfix(sizeInput))).toHaveDisplayValue(
values.ocoSize as string
);
expect(screen.getByTestId(ocoPostfix(timeInForce))).toHaveValue(
values.ocoTimeInForce
);
expect(screen.getByTestId(ocoPostfix(priceInput))).toHaveDisplayValue(
values.ocoPrice as string
);
expect(screen.getByTestId('ocoTypeLimit').dataset.state).toEqual('checked');
expect(screen.getByTestId(expire).dataset.state).toEqual('checked');
expect(screen.getByTestId(expiryStrategyCancel).dataset.state).toEqual(
'checked'
@@ -202,9 +154,6 @@ describe('StopOrder', () => {
expect(screen.getByTestId(datePicker)).toHaveDisplayValue(
values.expiresAt as string
);
await userEvent.click(screen.getByTestId(orderTypeMarket));
expect(screen.getByTestId(oco).dataset.state).toEqual('unchecked');
});
it('does not submit if no wallet connected', async () => {
@@ -225,239 +174,145 @@ describe('StopOrder', () => {
expect(submit).toBeCalled();
});
it.each([
{ fieldName: 'size', ocoValue: false },
{ fieldName: 'ocoSize', ocoValue: true },
])('validates $fieldName field', async ({ ocoValue }) => {
it('validates size field', async () => {
render(generateJsx());
if (ocoValue) {
await userEvent.click(screen.getByTestId(oco));
}
await userEvent.click(screen.getByTestId(submitButton));
const getByTestId = (id: string) =>
screen.getByTestId(ocoPostfix(id, ocoValue));
const queryByTestId = (id: string) =>
screen.queryByTestId(ocoPostfix(id, ocoValue));
// default value should be invalid
expect(getByTestId(sizeErrorMessage)).toBeInTheDocument();
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(getByTestId(sizeInput), '0.01');
expect(getByTestId(sizeErrorMessage)).toBeInTheDocument();
await userEvent.type(screen.getByTestId(sizeInput), '0.01');
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(getByTestId(sizeInput));
await userEvent.type(getByTestId(sizeInput), '0.1');
expect(queryByTestId(sizeErrorMessage)).toBeNull();
await userEvent.clear(screen.getByTestId(sizeInput));
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
expect(screen.queryByTestId(sizeErrorMessage)).toBeNull();
});
it.each([
{ fieldName: 'price', ocoValue: false },
{ fieldName: 'ocoPrice', ocoValue: true },
])('validates $fieldName field', async ({ ocoValue }) => {
it('validates price field', async () => {
render(generateJsx());
if (ocoValue) {
await userEvent.click(screen.getByTestId(oco));
}
await userEvent.click(screen.getByTestId(submitButton));
const getByTestId = (id: string) =>
screen.getByTestId(ocoPostfix(id, ocoValue));
const queryByTestId = (id: string) =>
screen.queryByTestId(ocoPostfix(id, ocoValue));
expect(getByTestId(priceErrorMessage)).toBeInTheDocument();
await userEvent.type(getByTestId(priceInput), '0.001');
expect(getByTestId(priceErrorMessage)).toBeInTheDocument();
// price error message should not show if size has error
// expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
// await userEvent.type(screen.getByTestId(sizeInput), '0.1');
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
await userEvent.type(screen.getByTestId(priceInput), '0.001');
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
// switch to market order type error should disappear
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeMarket));
await userEvent.click(screen.getByTestId(submitButton));
expect(queryByTestId(priceErrorMessage)).toBeNull();
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
// switch back to limit type
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeLimit));
await userEvent.click(screen.getByTestId(submitButton));
expect(getByTestId(priceErrorMessage)).toBeInTheDocument();
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(getByTestId(priceInput), '0.001');
expect(getByTestId(priceErrorMessage)).toBeInTheDocument();
await userEvent.type(screen.getByTestId(priceInput), '0.001');
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(getByTestId(priceInput));
await userEvent.type(getByTestId(priceInput), '0.01');
expect(queryByTestId(priceErrorMessage)).toBeNull();
await userEvent.clear(screen.getByTestId(priceInput));
await userEvent.type(screen.getByTestId(priceInput), '0.01');
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
});
it.each([
{ fieldName: 'triggerPrice', ocoValue: false },
{ fieldName: 'ocoTriggerPrice', ocoValue: true },
])('validates $fieldName field', async ({ ocoValue }) => {
it('validates trigger price field', async () => {
render(generateJsx());
if (ocoValue) {
await userEvent.click(screen.getByTestId(oco));
await userEvent.click(screen.getByTestId(triggerDirectionFallsBelow));
}
await userEvent.click(screen.getByTestId(submitButton));
const getByTestId = (id: string) =>
screen.getByTestId(ocoPostfix(id, ocoValue));
const queryByTestId = (id: string) =>
screen.queryByTestId(ocoPostfix(id, ocoValue));
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// switch to trailing percentage offset trigger type
await userEvent.click(getByTestId(triggerTypeTrailingPercentOffset));
expect(queryByTestId(triggerPriceErrorMessage)).toBeNull();
await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset));
expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
// switch back to price trigger type
await userEvent.click(getByTestId(triggerTypePrice));
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
await userEvent.click(screen.getByTestId(triggerTypePrice));
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(getByTestId(triggerPriceInput), '0.001');
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.001');
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// clear and fill using value causing immediate trigger
await userEvent.clear(getByTestId(triggerPriceInput));
await userEvent.type(getByTestId(triggerPriceInput), '0.01');
expect(queryByTestId(triggerPriceErrorMessage)).toBeNull();
expect(queryByTestId(triggerPriceWarningMessage)).toBeInTheDocument();
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(getByTestId(triggerPriceInput), '2');
expect(queryByTestId(triggerPriceWarningMessage)).toBeNull();
await userEvent.type(screen.getByTestId(triggerPriceInput), '2');
expect(screen.queryByTestId(triggerPriceWarningMessage)).toBeNull();
});
it.each([
{ fieldName: 'trailingPercentageOffset', ocoValue: false },
{ fieldName: 'ocoTrailingPercentageOffset', ocoValue: true },
])('validates $fieldName field', async ({ ocoValue }) => {
it('validates trigger trailing percentage offset field', async () => {
render(generateJsx());
if (ocoValue) {
await userEvent.click(screen.getByTestId(oco));
}
await userEvent.click(screen.getByTestId(submitButton));
const getByTestId = (id: string) =>
screen.getByTestId(ocoPostfix(id, ocoValue));
const queryByTestId = (id: string) =>
screen.queryByTestId(ocoPostfix(id, ocoValue));
// should not show error with default form values
expect(queryByTestId(triggerTrailingPercentOffsetErrorMessage)).toBeNull();
await userEvent.click(screen.getByTestId(submitButton));
expect(
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeNull();
// switch to trailing percentage offset trigger type
await userEvent.click(getByTestId(triggerTypeTrailingPercentOffset));
await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset));
expect(
getByTestId(triggerTrailingPercentOffsetErrorMessage)
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(
getByTestId(triggerTrailingPercentOffsetInput),
screen.getByTestId(triggerTrailingPercentOffsetInput),
'0.09'
);
expect(
getByTestId(triggerTrailingPercentOffsetErrorMessage)
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(getByTestId(triggerTrailingPercentOffsetInput));
await userEvent.type(getByTestId(triggerTrailingPercentOffsetInput), '0.1');
expect(queryByTestId(triggerTrailingPercentOffsetErrorMessage)).toBeNull();
await userEvent.clear(
screen.getByTestId(triggerTrailingPercentOffsetInput)
);
await userEvent.type(
screen.getByTestId(triggerTrailingPercentOffsetInput),
'0.1'
);
expect(
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeNull();
// to big value should be invalid
await userEvent.clear(getByTestId(triggerTrailingPercentOffsetInput));
await userEvent.clear(
screen.getByTestId(triggerTrailingPercentOffsetInput)
);
await userEvent.type(
getByTestId(triggerTrailingPercentOffsetInput),
screen.getByTestId(triggerTrailingPercentOffsetInput),
'99.91'
);
expect(
getByTestId(triggerTrailingPercentOffsetErrorMessage)
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(getByTestId(triggerTrailingPercentOffsetInput));
await userEvent.clear(
screen.getByTestId(triggerTrailingPercentOffsetInput)
);
await userEvent.type(
getByTestId(triggerTrailingPercentOffsetInput),
screen.getByTestId(triggerTrailingPercentOffsetInput),
'99.9'
);
expect(queryByTestId(triggerTrailingPercentOffsetErrorMessage)).toBeNull();
});
it('sync oco trigger', async () => {
render(generateJsx());
await userEvent.click(screen.getByTestId(oco));
expect(
screen.getByTestId(triggerDirectionRisesAbove).dataset.state
).toEqual('checked');
expect(
screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow)).dataset.state
).toEqual('checked');
await userEvent.click(screen.getByTestId(triggerDirectionFallsBelow));
expect(
screen.getByTestId(triggerDirectionRisesAbove).dataset.state
).toEqual('unchecked');
expect(
screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow)).dataset.state
).toEqual('unchecked');
await userEvent.click(
screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow))
);
expect(
screen.getByTestId(triggerDirectionRisesAbove).dataset.state
).toEqual('checked');
expect(
screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow)).dataset.state
).toEqual('checked');
});
it('disables submit expiry strategy when OCO selected', async () => {
render(generateJsx());
await userEvent.click(screen.getByTestId(expire));
await userEvent.click(screen.getByTestId(expiryStrategySubmit));
await userEvent.click(screen.getByTestId(oco));
expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual(
'unchecked'
);
expect(screen.getByTestId(expiryStrategySubmit)).toBeDisabled();
await userEvent.click(screen.getByTestId(oco));
await userEvent.click(screen.getByTestId(expiryStrategySubmit));
expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual(
'checked'
);
expect(screen.getByTestId(expiryStrategySubmit)).not.toBeDisabled();
});
it('sets expiry time/date to now if expiry is changed to checked', async () => {
const now = 24 * 60 * 60 * 1000;
render(generateJsx());
jest.spyOn(global.Date, 'now').mockImplementation(() => now);
await userEvent.click(screen.getByTestId(expire));
// expiry time/date was empty it should be set to now
expect(
new Date(screen.getByTestId<HTMLInputElement>(datePicker).value).getTime()
).toEqual(now);
// set to the value in the past (now - 1s)
fireEvent.change(screen.getByTestId<HTMLInputElement>(datePicker), {
target: { value: formatForInput(new Date(now - 1000)) },
});
expect(
new Date(
screen.getByTestId<HTMLInputElement>(datePicker).value
).getTime() + 1000
).toEqual(now);
// switch expiry off and on
await userEvent.click(screen.getByTestId(expire));
await userEvent.click(screen.getByTestId(expire));
// expiry time/date was in the past it should be set to now
expect(
new Date(screen.getByTestId<HTMLInputElement>(datePicker).value).getTime()
).toEqual(now);
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeNull();
});
});
@@ -1,12 +1,8 @@
import { useRef, useCallback, useEffect } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type {
OrderSubmissionBody,
StopOrdersSubmission,
} from '@vegaprotocol/wallet';
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
import {
formatForInput,
formatValue,
removeDecimal,
toDecimal,
validateAmount,
@@ -23,9 +19,6 @@ import {
TradingInputError as InputError,
TradingSelect as Select,
Tooltip,
TradingButton as Button,
Pill,
Intent,
} from '@vegaprotocol/ui-toolkit';
import { getDerivedPrice } from '@vegaprotocol/markets';
import type { Market } from '@vegaprotocol/markets';
@@ -38,7 +31,6 @@ import {
REDUCE_ONLY_TOOLTIP,
stopSubmit,
getNotionalSize,
getAssetUnit,
} from './deal-ticket';
import { TypeToggle } from './type-selector';
import {
@@ -49,10 +41,9 @@ import {
} 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';
import { validateExpiration } from '../../utils';
import { NOTIONAL_SIZE_TOOLTIP_TEXT } from '../../constants';
import { KeyValue } from './key-value';
export interface StopOrderProps {
market: Market;
@@ -87,7 +78,7 @@ const Trigger = ({
control,
watch,
priceStep,
quoteName,
assetSymbol,
oco,
marketPrice,
decimalPlaces,
@@ -95,7 +86,7 @@ const Trigger = ({
control: Control<StopOrderFormValues>;
watch: UseFormWatch<StopOrderFormValues>;
priceStep: string;
quoteName: string;
assetSymbol: string;
oco?: boolean;
marketPrice?: string | null;
decimalPlaces: number;
@@ -190,7 +181,7 @@ const Trigger = ({
data-testid={`triggerPrice${oco ? '-oco' : ''}`}
type="number"
step={priceStep}
appendElement={<Pill size="xs">{quoteName}</Pill>}
appendElement={assetSymbol}
value={value || ''}
hasError={!!fieldState.error}
{...props}
@@ -258,7 +249,7 @@ const Trigger = ({
<Input
type="number"
step={trailingPercentOffsetStep}
appendElement={<Pill size="xs">%</Pill>}
appendElement="%"
data-testid={`triggerTrailingPercentOffset${
oco ? '-oco' : ''
}`}
@@ -320,14 +311,10 @@ const Size = ({
control,
sizeStep,
oco,
isLimitType,
assetUnit,
}: {
control: Control<StopOrderFormValues>;
sizeStep: string;
oco?: boolean;
isLimitType: boolean;
assetUnit?: string;
}) => {
return (
<Controller
@@ -345,7 +332,7 @@ const Size = ({
const { value, ...props } = field;
const id = `order-size${oco ? '-oco' : ''}`;
return (
<div className={isLimitType ? 'mb-4' : 'mb-2'}>
<div className="mb-4">
<FormGroup labelFor={id} label={t(`Size`)} compact>
<Input
id={id}
@@ -354,7 +341,6 @@ const Size = ({
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
appendElement={assetUnit && <Pill size="xs">{assetUnit}</Pill>}
data-testid={id}
value={value || ''}
hasError={!!fieldState.error}
@@ -408,8 +394,12 @@ const Price = ({
const { value, ...props } = field;
const id = `order-price${oco ? '-oco' : ''}`;
return (
<div className="mb-2">
<FormGroup labelFor={id} label={t('Price')} compact={true}>
<div className="mb-4">
<FormGroup
labelFor={id}
label={t(`Price (${quoteName})`)}
compact={true}
>
<Input
id={id}
className="w-full"
@@ -419,7 +409,6 @@ const Price = ({
onWheel={(e) => e.currentTarget.blur()}
value={value || ''}
hasError={!!fieldState.error}
appendElement={<Pill size="xs">{quoteName}</Pill>}
{...props}
/>
</FormGroup>
@@ -445,17 +434,17 @@ const TimeInForce = ({
oco?: boolean;
}) => (
<Controller
name={oco ? 'ocoTimeInForce' : 'timeInForce'}
name="timeInForce"
control={control}
render={({ field, fieldState }) => {
const id = `order-tif${oco ? '-oco' : ''}`;
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={id}
data-testid="order-tif"
hasError={!!fieldState.error}
{...field}
>
@@ -497,255 +486,6 @@ const ReduceOnly = () => (
/>
);
const NotionalAndFees = ({
market,
marketPrice,
side,
size,
price,
timeInForce,
triggerPrice,
triggerType,
type,
}: Pick<
OrderSubmissionBody['orderSubmission'],
'side' | 'size' | 'timeInForce' | 'type' | 'price'
> &
Pick<StopOrderProps, 'market' | 'marketPrice'> &
Pick<StopOrderFormValues, 'triggerType' | 'triggerPrice'>) => {
const { quoteName, settlementAsset: asset } =
market.tradableInstrument.instrument.product;
const isPriceTrigger = triggerType === 'price';
const derivedPrice = getDerivedPrice(
{
type,
price,
},
type === Schema.OrderType.TYPE_MARKET && isPriceTrigger && triggerPrice
? removeDecimal(triggerPrice, market.decimalPlaces)
: marketPrice || '0'
);
const notionalSize = getNotionalSize(
derivedPrice,
size,
market.decimalPlaces,
market.positionDecimalPlaces
);
return (
<div className="mb-4">
<KeyValue
label={t('Notional')}
value={formatValue(notionalSize, market.decimalPlaces)}
formattedValue={formatValue(notionalSize, market.decimalPlaces)}
symbol={quoteName}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
/>
<DealTicketFeeDetails
order={{
marketId: market.id,
price: derivedPrice,
side,
size,
timeInForce,
type,
}}
assetSymbol={asset.symbol}
market={market}
/>
</div>
);
};
const formatSizeAtPrice = ({
assetUnit,
decimalPlaces,
positionDecimalPlaces,
price,
quoteName,
side,
size,
type,
}: Pick<StopOrderFormValues, 'price' | 'side' | 'size' | 'type'> & {
assetUnit?: string;
decimalPlaces: number;
positionDecimalPlaces: number;
quoteName: string;
}) =>
`${formatValue(
removeDecimal(size, positionDecimalPlaces),
positionDecimalPlaces
)} ${assetUnit} @ ${
type === Schema.OrderType.TYPE_MARKET
? 'market'
: `${formatValue(
removeDecimal(price || '0', decimalPlaces),
decimalPlaces
)} ${quoteName}`
}`;
const formatTrigger = ({
decimalPlaces,
triggerDirection,
triggerPrice,
triggerTrailingPercentOffset,
triggerType,
quoteName,
}: Pick<
StopOrderFormValues,
| 'triggerDirection'
| 'triggerType'
| 'triggerPrice'
| 'triggerTrailingPercentOffset'
> & {
decimalPlaces: number;
quoteName: string;
}) =>
`${
triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
? t('above')
: t('below')
} ${
triggerType === 'price'
? `${formatValue(
removeDecimal(triggerPrice || '', decimalPlaces),
decimalPlaces
)} ${quoteName}`
: `${(Number(triggerTrailingPercentOffset) || 0).toFixed(1)}% ${t(
'trailing'
)}`
}`;
const SubmitButton = ({
assetUnit,
market,
oco,
ocoPrice,
ocoSize,
ocoTriggerPrice,
ocoTriggerTrailingPercentOffset,
ocoTriggerType,
ocoType,
price,
side,
size,
triggerDirection,
triggerPrice,
triggerTrailingPercentOffset,
triggerType,
type,
}: Pick<
StopOrderFormValues,
| 'oco'
| 'ocoPrice'
| 'ocoSize'
| 'ocoTriggerPrice'
| 'ocoTriggerTrailingPercentOffset'
| 'ocoTriggerType'
| 'ocoType'
| 'price'
| 'side'
| 'size'
| 'triggerDirection'
| 'triggerPrice'
| 'triggerTrailingPercentOffset'
| 'triggerType'
| 'type'
> &
Pick<StopOrderProps, 'market'> & { assetUnit?: string }) => {
const { quoteName } = market.tradableInstrument.instrument.product;
const risesAbove =
triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE;
const subLabel = oco ? (
<>
{formatSizeAtPrice({
assetUnit,
decimalPlaces: market.decimalPlaces,
positionDecimalPlaces: market.positionDecimalPlaces,
price: risesAbove ? price : ocoPrice,
quoteName,
side,
size: risesAbove ? size : ocoSize,
type,
})}{' '}
{formatTrigger({
decimalPlaces: market.decimalPlaces,
quoteName,
triggerDirection:
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
triggerPrice: risesAbove ? triggerPrice : ocoTriggerPrice,
triggerTrailingPercentOffset: risesAbove
? triggerTrailingPercentOffset
: ocoTriggerTrailingPercentOffset,
triggerType: risesAbove ? triggerType : ocoTriggerType,
})}
<br />
{formatSizeAtPrice({
assetUnit,
decimalPlaces: market.decimalPlaces,
positionDecimalPlaces: market.positionDecimalPlaces,
price: !risesAbove ? price : ocoPrice,
quoteName,
side,
size: !risesAbove ? size : ocoSize,
type: ocoType,
})}{' '}
{formatTrigger({
decimalPlaces: market.decimalPlaces,
quoteName,
triggerDirection:
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW,
triggerPrice: !risesAbove ? triggerPrice : ocoTriggerPrice,
triggerTrailingPercentOffset: !risesAbove
? triggerTrailingPercentOffset
: ocoTriggerTrailingPercentOffset,
triggerType: !risesAbove ? triggerType : ocoTriggerType,
})}
</>
) : (
<>
{formatSizeAtPrice({
assetUnit,
decimalPlaces: market.decimalPlaces,
positionDecimalPlaces: market.positionDecimalPlaces,
price,
quoteName,
side,
size,
type,
})}
<br />
{t('Trigger')}{' '}
{formatTrigger({
decimalPlaces: market.decimalPlaces,
quoteName,
triggerDirection,
triggerPrice,
triggerTrailingPercentOffset,
triggerType,
})}
</>
);
return (
<Button
intent={side === Schema.Side.SIDE_BUY ? Intent.Success : Intent.Danger}
data-testid="place-order"
type="submit"
className="w-full"
subLabel={subLabel}
>
{t(
oco
? 'Place OCO stop order'
: type === Schema.OrderType.TYPE_MARKET
? 'Place market stop order'
: 'Place limit stop order'
)}
</Button>
);
};
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
const { pubKey, isReadOnly } = useVegaWallet();
const setType = useDealTicketFormValues((state) => state.setType);
@@ -781,40 +521,50 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
},
[market.id, market.decimalPlaces, market.positionDecimalPlaces, submit]
);
const expire = watch('expire');
const expiresAt = watch('expiresAt');
const oco = watch('oco');
const ocoPrice = watch('ocoPrice');
const ocoSize = watch('ocoSize');
const ocoTimeInForce = watch('ocoTimeInForce');
const ocoTriggerPrice = watch('ocoTriggerPrice');
const ocoTriggerTrailingPercentOffset = watch(
'ocoTriggerTrailingPercentOffset'
);
const ocoTriggerType = watch('ocoTriggerType');
const ocoType = watch('ocoType');
const price = watch('price');
const side = watch('side');
const size = watch('size');
const timeInForce = watch('timeInForce');
const triggerDirection = watch('triggerDirection');
const triggerPrice = watch('triggerPrice');
const triggerTrailingPercentOffset = watch('triggerTrailingPercentOffset');
const expire = watch('expire');
const triggerType = watch('triggerType');
const triggerPrice = watch('triggerPrice');
const timeInForce = watch('timeInForce');
const rawPrice = watch('price');
const rawSize = watch('size');
const oco = watch('oco');
const expiresAt = watch('expiresAt');
useEffect(() => {
const storedSize = storedFormValues?.[dealTicketType]?.size;
if (storedSize && size !== storedSize) {
setValue('size', storedSize);
const size = storedFormValues?.[dealTicketType]?.size;
if (size && rawSize !== size) {
setValue('size', size);
}
}, [storedFormValues, dealTicketType, size, setValue]);
}, [storedFormValues, dealTicketType, rawSize, setValue]);
useEffect(() => {
const storedPrice = storedFormValues?.[dealTicketType]?.price;
if (storedPrice && price !== storedPrice) {
setValue('price', storedPrice);
const price = storedFormValues?.[dealTicketType]?.price;
if (price && rawPrice !== price) {
setValue('price', price);
}
}, [storedFormValues, dealTicketType, price, setValue]);
}, [storedFormValues, dealTicketType, rawPrice, setValue]);
const isPriceTrigger = triggerType === 'price';
const size = removeDecimal(rawSize, market.positionDecimalPlaces);
const price =
marketPrice &&
getDerivedPrice(
{
type,
price: rawPrice && removeDecimal(rawPrice, market.decimalPlaces),
},
type === Schema.OrderType.TYPE_MARKET && isPriceTrigger && triggerPrice
? removeDecimal(triggerPrice, market.decimalPlaces)
: marketPrice
);
const notionalSize = getNotionalSize(
price,
size,
market.decimalPlaces,
market.positionDecimalPlaces
);
useEffect(() => {
const subscription = watch((value, { name, type }) => {
@@ -823,10 +573,8 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
return () => subscription.unsubscribe();
}, [watch, market.id, updateStoredFormValues]);
const { quoteName } = market.tradableInstrument.instrument.product;
const assetUnit = getAssetUnit(
market.tradableInstrument.instrument.metadata.tags
);
const { quoteName, settlementAsset: asset } =
market.tradableInstrument.instrument.product;
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const priceStep = toDecimal(market?.decimalPlaces);
@@ -836,10 +584,6 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
control,
});
const normalizedPrice = price && removeDecimal(price, market.decimalPlaces);
const normalizedSize =
size && removeDecimal(size, market.positionDecimalPlaces);
return (
<form
onSubmit={isReadOnly || !pubKey ? stopSubmit : handleSubmit(onSubmit)}
@@ -876,34 +620,18 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
control={control}
watch={watch}
priceStep={priceStep}
quoteName={quoteName}
assetSymbol={asset.symbol}
marketPrice={marketPrice}
decimalPlaces={market.decimalPlaces}
/>
<hr className="mb-4 border-vega-clight-500 dark:border-vega-cdark-500" />
<Size
control={control}
sizeStep={sizeStep}
isLimitType={type === Schema.OrderType.TYPE_LIMIT}
assetUnit={assetUnit}
/>
<Price
control={control}
watch={watch}
priceStep={priceStep}
quoteName={quoteName}
/>
<NotionalAndFees
market={market}
marketPrice={marketPrice}
price={normalizedPrice}
side={side}
size={normalizedSize}
timeInForce={timeInForce}
triggerPrice={triggerPrice}
triggerType={triggerType}
type={type}
/>
<Size control={control} sizeStep={sizeStep} />
<TimeInForce control={control} />
<div className="flex justify-end pb-3 gap-2">
<ReduceOnly />
@@ -954,12 +682,12 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
>
<Radio
value={Schema.OrderType.TYPE_MARKET}
id="ocoTypeMarket"
id={`ocoTypeMarket`}
label={'Market'}
/>
<Radio
value={Schema.OrderType.TYPE_LIMIT}
id="ocoTypeLimit"
id={`ocoTypeLimit`}
label={'Limit'}
/>
</RadioGroup>
@@ -971,19 +699,12 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
control={control}
watch={watch}
priceStep={priceStep}
quoteName={quoteName}
assetSymbol={asset.symbol}
marketPrice={marketPrice}
decimalPlaces={market.decimalPlaces}
oco
/>
<hr className="mb-2 border-vega-clight-500 dark:border-vega-cdark-500" />
<Size
control={control}
sizeStep={sizeStep}
assetUnit={assetUnit}
oco
isLimitType={ocoType === Schema.OrderType.TYPE_LIMIT}
/>
<Price
control={control}
watch={watch}
@@ -991,19 +712,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
quoteName={quoteName}
oco
/>
<NotionalAndFees
market={market}
marketPrice={marketPrice}
price={ocoPrice && removeDecimal(ocoPrice, market.decimalPlaces)}
side={side}
size={
ocoSize && removeDecimal(ocoSize, market.positionDecimalPlaces)
}
timeInForce={ocoTimeInForce}
triggerPrice={ocoTriggerPrice}
triggerType={ocoTriggerType}
type={ocoType}
/>
<Size control={control} sizeStep={sizeStep} oco />
<TimeInForce control={control} oco />
<div className="flex justify-end mb-2 gap-2">
<ReduceOnly />
@@ -1019,12 +728,11 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
return (
<Checkbox
onCheckedChange={(value) => {
const now = Date.now();
if (
value &&
(!expiresAt || new Date(expiresAt).getTime() < now)
(!expiresAt || new Date(expiresAt).getTime() < Date.now())
) {
setValue('expiresAt', formatForInput(new Date(now)), {
setValue('expiresAt', formatForInput(new Date()), {
shouldValidate: true,
});
}
@@ -1095,24 +803,19 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
</>
)}
<NoWalletWarning isReadOnly={isReadOnly} />
<SubmitButton
assetUnit={assetUnit}
<DealTicketButton side={side} label={t('Submit Stop Order')} />
<DealTicketFeeDetails
order={{
marketId: market.id,
price: price || undefined,
side,
size,
timeInForce,
type,
}}
notionalSize={notionalSize}
assetSymbol={asset.symbol}
market={market}
oco={oco}
ocoPrice={ocoPrice}
ocoSize={ocoSize}
ocoTriggerPrice={ocoTriggerPrice}
ocoTriggerTrailingPercentOffset={ocoTriggerTrailingPercentOffset}
ocoTriggerType={ocoTriggerType}
ocoType={ocoType}
price={price}
side={side}
size={size}
triggerDirection={triggerDirection}
triggerPrice={triggerPrice}
triggerTrailingPercentOffset={triggerTrailingPercentOffset}
triggerType={triggerType}
type={type}
/>
</form>
);
@@ -1,12 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { VegaWalletContext } from '@vegaprotocol/wallet';
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { generateMarket, generateMarketData } from '../../test-helpers';
import { DealTicket } from './deal-ticket';
@@ -20,9 +14,6 @@ import {
} from '../../hooks/use-form-values';
import * as positionsTools from '@vegaprotocol/positions';
import { OrdersDocument } from '@vegaprotocol/orders';
import { formatForInput } from '@vegaprotocol/utils';
import type { PartialDeep } from 'type-fest';
import type { Market } from '@vegaprotocol/markets';
jest.mock('zustand');
jest.mock('./deal-ticket-fee-details', () => ({
@@ -38,19 +29,12 @@ const market = generateMarket();
const marketData = generateMarketData();
const submit = jest.fn();
function generateJsx(
mocks: MockedResponse[] = [],
marketOverrides: PartialDeep<Market> = {}
) {
const joinedMarket: Market = {
...market,
...marketOverrides,
} as Market;
function generateJsx(mocks: MockedResponse[] = []) {
return (
<MockedProvider mocks={[...mocks]}>
<VegaWalletContext.Provider value={{ pubKey, isReadOnly: false } as any}>
<DealTicket
market={joinedMarket}
market={market}
marketData={marketData}
marketPrice={marketPrice}
submit={submit}
@@ -330,7 +314,7 @@ describe('DealTicket', () => {
expect(screen.getByTestId('iceberg')).toBeChecked();
});
it('should set values for a non-persistent order and disable post only checkbox', () => {
it('should set values for a non-persistent iceberg order and disable post only checkbox', () => {
const expectedOrder = {
marketId: market.id,
type: Schema.OrderType.TYPE_LIMIT,
@@ -373,9 +357,9 @@ describe('DealTicket', () => {
expect(screen.getByTestId('reduce-only')).not.toBeChecked();
expect(screen.getByTestId('post-only')).not.toBeChecked();
expect(screen.getByTestId('iceberg')).not.toBeChecked();
expect(screen.getByTestId('iceberg')).toBeDisabled();
});
// eslint-disable-next-line jest/no-disabled-tests
it('handles TIF select box dependent on order type', async () => {
render(generateJsx());
@@ -489,169 +473,4 @@ describe('DealTicket', () => {
Object.keys(Schema.OrderTimeInForce).length
);
});
it('validates size field', async () => {
render(generateJsx());
const sizeErrorMessage = 'deal-ticket-error-message-size';
const sizeInput = 'order-size';
await userEvent.click(screen.getByTestId('place-order'));
// default value should be invalid
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(screen.getByTestId(sizeInput), '0.01');
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(screen.getByTestId(sizeInput));
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
expect(screen.queryByTestId(sizeErrorMessage)).toBeNull();
});
it('validates price field', async () => {
const priceErrorMessage = 'deal-ticket-error-message-price';
const priceInput = 'order-price';
const submitButton = 'place-order';
const orderTypeMarket = 'order-type-Market';
const orderTypeLimit = 'order-type-Limit';
render(generateJsx());
await userEvent.click(screen.getByTestId(submitButton));
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
await userEvent.type(screen.getByTestId(priceInput), '0.001');
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
// switch to market order type error should disappear
await userEvent.click(screen.getByTestId(orderTypeMarket));
await userEvent.click(screen.getByTestId(submitButton));
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
// switch back to limit type
await userEvent.click(screen.getByTestId(orderTypeLimit));
await userEvent.click(screen.getByTestId(submitButton));
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(screen.getByTestId(priceInput), '0.001');
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(screen.getByTestId(priceInput));
await userEvent.type(screen.getByTestId(priceInput), '0.01');
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
});
it('validates size when positionDecimalPlaces is negative', async () => {
render(generateJsx([], { positionDecimalPlaces: -4 }));
const sizeErrorMessage = 'deal-ticket-error-message-size';
const sizeInput = 'order-size';
await userEvent.click(screen.getByTestId('place-order'));
// default value should be invalid
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
expect(screen.getByTestId(sizeErrorMessage)).toHaveTextContent(
'Size cannot be lower than 10000'
);
await userEvent.type(screen.getByTestId(sizeInput), '10001');
expect(screen.getByTestId(sizeErrorMessage)).toHaveTextContent(
'Size must be a multiple of 10000 for this market'
);
await userEvent.clear(screen.getByTestId(sizeInput));
await userEvent.type(screen.getByTestId(sizeInput), '10000');
expect(screen.queryByTestId(sizeErrorMessage)).toBeNull();
});
it('validates iceberg field', async () => {
const peakSizeErrorMessage = 'deal-ticket-peak-error-message';
const minimumSizeErrorMessage = 'deal-ticket-minimum-error-message';
const sizeInput = 'order-size';
const peakSizeInput = 'order-peak-size';
const minimumSizeInput = 'order-minimum-size';
const submitButton = 'place-order';
render(generateJsx());
await userEvent.selectOptions(
screen.getByTestId('order-tif'),
Schema.OrderTimeInForce.TIME_IN_FORCE_GFA
);
await userEvent.click(screen.getByTestId('iceberg'));
await userEvent.click(screen.getByTestId(submitButton));
// validate empty fields
expect(screen.getByTestId(peakSizeErrorMessage)).toBeInTheDocument();
expect(screen.getByTestId(minimumSizeErrorMessage)).toBeInTheDocument();
await userEvent.type(screen.getByTestId(peakSizeInput), '0.01');
await userEvent.type(screen.getByTestId(minimumSizeInput), '0.01');
// validate value smaller than step
expect(screen.getByTestId(peakSizeErrorMessage)).toBeInTheDocument();
expect(screen.getByTestId(minimumSizeErrorMessage)).toBeInTheDocument();
await userEvent.clear(screen.getByTestId(peakSizeInput));
await userEvent.type(screen.getByTestId(peakSizeInput), '0.5');
await userEvent.clear(screen.getByTestId(minimumSizeInput));
await userEvent.type(screen.getByTestId(minimumSizeInput), '0.7');
await userEvent.clear(screen.getByTestId(sizeInput));
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
// validate value higher than size
expect(screen.getByTestId(peakSizeErrorMessage)).toBeInTheDocument();
expect(screen.getByTestId(minimumSizeErrorMessage)).toBeInTheDocument();
await userEvent.clear(screen.getByTestId(sizeInput));
await userEvent.type(screen.getByTestId(sizeInput), '1');
// validate peak higher than minimum
expect(screen.queryByTestId(peakSizeErrorMessage)).toBeNull();
expect(screen.getByTestId(minimumSizeErrorMessage)).toBeInTheDocument();
await userEvent.clear(screen.getByTestId(peakSizeInput));
await userEvent.type(screen.getByTestId(peakSizeInput), '1');
await userEvent.clear(screen.getByTestId(minimumSizeInput));
await userEvent.type(screen.getByTestId(minimumSizeInput), '1');
// validate correct values
expect(screen.queryByTestId(peakSizeErrorMessage)).toBeNull();
expect(screen.queryByTestId(minimumSizeErrorMessage)).toBeNull();
});
it('sets expiry time/date to now if expiry is changed to checked', async () => {
const datePicker = 'date-picker-field';
const now = 24 * 60 * 60 * 1000;
render(generateJsx());
jest.spyOn(global.Date, 'now').mockImplementation(() => now);
await userEvent.selectOptions(
screen.getByTestId('order-tif'),
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
);
// expiry time/date was empty it should be set to now
expect(
new Date(screen.getByTestId<HTMLInputElement>(datePicker).value).getTime()
).toEqual(now);
// set to the value in the past (now - 1s)
fireEvent.change(screen.getByTestId<HTMLInputElement>(datePicker), {
target: { value: formatForInput(new Date(now - 1000)) },
});
expect(
new Date(
screen.getByTestId<HTMLInputElement>(datePicker).value
).getTime() + 1000
).toEqual(now);
// switch expiry off and on
await userEvent.selectOptions(
screen.getByTestId('order-tif'),
Schema.OrderTimeInForce.TIME_IN_FORCE_GFA
);
await userEvent.selectOptions(
screen.getByTestId('order-tif'),
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
);
// expiry time/date was in the past it should be set to now
expect(
new Date(screen.getByTestId<HTMLInputElement>(datePicker).value).getTime()
).toEqual(now);
});
});
@@ -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 { DealTicketButton } from './deal-ticket-button';
import {
DealTicketFeeDetails,
DealTicketMarginDetails,
@@ -22,8 +23,6 @@ import {
Intent,
Notification,
Tooltip,
TradingButton as Button,
Pill,
} from '@vegaprotocol/ui-toolkit';
import {
@@ -36,7 +35,6 @@ import {
validateAmount,
toDecimal,
formatForInput,
formatValue,
} from '@vegaprotocol/utils';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import { getDerivedPrice } from '@vegaprotocol/markets';
@@ -48,10 +46,7 @@ import {
validateType,
} from '../../utils';
import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error';
import {
NOTIONAL_SIZE_TOOLTIP_TEXT,
SummaryValidationType,
} from '../../constants';
import { SummaryValidationType } from '../../constants';
import type {
Market,
MarketData,
@@ -73,7 +68,6 @@ 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';
import { KeyValue } from './key-value';
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.';
@@ -124,11 +118,6 @@ const getDefaultValues = (
...storedValues,
});
export const getAssetUnit = (tags?: string[] | null) =>
tags
?.find((tag) => tag.startsWith('base:') || tag.startsWith('ticker:'))
?.replace(/^[^:]*:/, '');
export const DealTicket = ({
market,
onMarketClick,
@@ -268,10 +257,6 @@ export const DealTicket = ({
const assetSymbol =
market.tradableInstrument.instrument.product.settlementAsset.symbol;
const assetUnit = getAssetUnit(
market.tradableInstrument.instrument.metadata.tags
);
const summaryError = useMemo(() => {
if (!pubKey) {
return {
@@ -353,7 +338,6 @@ export const DealTicket = ({
const priceStep = toDecimal(market?.decimalPlaces);
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const quoteName = market.tradableInstrument.instrument.product.quoteName;
const isLimitType = type === Schema.OrderType.TYPE_LIMIT;
return (
<form
@@ -400,16 +384,18 @@ export const DealTicket = ({
message: t('Size cannot be lower than ' + sizeStep),
},
validate: validateAmount(sizeStep, 'Size'),
deps: ['peakSize', 'minimumVisibleSize'],
}}
render={({ field, fieldState }) => (
<div className={isLimitType ? 'mb-4' : 'mb-2'}>
<FormGroup label={t('Size')} labelFor="order-size" compact>
<div className="mb-4">
<FormGroup
label={t('Size')}
labelFor="input-order-size-limit"
compact
>
<Input
id="order-size"
id="input-order-size-limit"
className="w-full"
type="number"
appendElement={assetUnit && <Pill size="xs">{assetUnit}</Pill>}
step={sizeStep}
min={sizeStep}
data-testid="order-size"
@@ -425,7 +411,7 @@ export const DealTicket = ({
</div>
)}
/>
{isLimitType && (
{type === Schema.OrderType.TYPE_LIMIT && (
<Controller
name="price"
control={control}
@@ -438,15 +424,14 @@ export const DealTicket = ({
validate: validateAmount(priceStep, 'Price'),
}}
render={({ field, fieldState }) => (
<div className="mb-2">
<div className="mb-4">
<FormGroup
labelFor="input-price-quote"
label={t('Price')}
label={t(`Price (${quoteName})`)}
compact
>
<Input
id="input-price-quote"
appendElement={<Pill size="xs">{quoteName}</Pill>}
className="w-full"
type="number"
step={priceStep}
@@ -464,22 +449,6 @@ export const DealTicket = ({
)}
/>
)}
<div className="mb-4">
<KeyValue
label={t('Notional')}
value={formatValue(notionalSize, market.decimalPlaces)}
formattedValue={formatValue(notionalSize, market.decimalPlaces)}
symbol={quoteName}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
/>
<DealTicketFeeDetails
order={
normalizedOrder && { ...normalizedOrder, price: price || undefined }
}
assetSymbol={assetSymbol}
market={market}
/>
</div>
<Controller
name="timeInForce"
control={control}
@@ -496,18 +465,17 @@ export const DealTicket = ({
onSelect={(value) => {
// If GTT is selected and no expiresAt time is set, or its
// behind current time then reset the value to current time
const now = Date.now();
if (
value === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT &&
(!expiresAt || new Date(expiresAt).getTime() < now)
(!expiresAt || new Date(expiresAt).getTime() < Date.now())
) {
setValue('expiresAt', formatForInput(new Date(now)), {
setValue('expiresAt', formatForInput(new Date()), {
shouldValidate: true,
});
}
// iceberg orders must be persistent orders, so if user
// switches to a non persistent tif value, remove iceberg selection
// switches to to a non persisten tif value, remove iceberg selection
if (iceberg && isNonPersistentOrder(value)) {
setValue('iceberg', false);
}
@@ -519,7 +487,7 @@ export const DealTicket = ({
/>
)}
/>
{isLimitType &&
{type === Schema.OrderType.TYPE_LIMIT &&
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
<Controller
name="expiresAt"
@@ -601,7 +569,7 @@ export const DealTicket = ({
)}
/>
</div>
{isLimitType && (
{type === Schema.OrderType.TYPE_LIMIT && (
<>
<div className="flex justify-between pb-2 gap-2">
<Controller
@@ -656,29 +624,15 @@ export const DealTicket = ({
pubKey={pubKey}
onDeposit={onDeposit}
/>
<Button
data-testid="place-order"
type="submit"
className="w-full"
intent={side === Schema.Side.SIDE_BUY ? Intent.Success : Intent.Danger}
subLabel={`${formatValue(
normalizedOrder.size,
market.positionDecimalPlaces
)} ${assetUnit} @ ${
type === Schema.OrderType.TYPE_MARKET
? 'market'
: `${formatValue(
normalizedOrder.price,
market.decimalPlaces
)} ${quoteName}`
}`}
>
{t(
type === Schema.OrderType.TYPE_MARKET
? 'Place market order'
: 'Place limit order'
)}
</Button>
<DealTicketButton side={side} />
<DealTicketFeeDetails
order={
normalizedOrder && { ...normalizedOrder, price: price || undefined }
}
notionalSize={notionalSize}
assetSymbol={assetSymbol}
market={market}
/>
<DealTicketMarginDetails
onMarketClick={onMarketClick}
assetSymbol={assetSymbol}
@@ -1,51 +0,0 @@
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import classnames from 'classnames';
import type { ReactNode } from 'react';
export interface KeyValuePros {
label: string;
value?: string | null | undefined;
symbol: string;
indent?: boolean | undefined;
labelDescription?: ReactNode;
formattedValue?: string;
onClick?: () => void;
}
export const KeyValue = ({
label,
value,
labelDescription,
symbol,
indent,
onClick,
formattedValue,
}: KeyValuePros) => {
const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`;
const valueElement = onClick ? (
<button onClick={onClick} className="text-muted">
{displayValue}
</button>
) : (
<div className="text-muted">{displayValue}</div>
);
return (
<div
data-testid={
'deal-ticket-fee-' + label.toLocaleLowerCase().replace(/\s/g, '-')
}
key={typeof label === 'string' ? label : 'value-dropdown'}
className={classnames(
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
{ 'ml-2': indent }
)}
>
<Tooltip description={labelDescription}>
<div>{label}</div>
</Tooltip>
<Tooltip description={`${value ?? '-'} ${symbol || ''}`}>
{valueElement}
</Tooltip>
</div>
);
};
+2 -15
View File
@@ -2,11 +2,7 @@ import type { Asset } from '@vegaprotocol/assets';
import { EtherscanLink } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { Intent, Notification } from '@vegaprotocol/ui-toolkit';
import {
formatNumber,
getUnlimitedThreshold,
quantumDecimalPlaces,
} from '@vegaprotocol/utils';
import { formatNumber } from '@vegaprotocol/utils';
import type { EthStoredTxState } from '@vegaprotocol/web3';
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
@@ -192,15 +188,6 @@ const ApprovalTxFeedback = ({
}
if (tx.status === EthTxStatus.Confirmed) {
const approvedAllowanceValue = (
allowance || new BigNumber(0)
).isGreaterThan(getUnlimitedThreshold(selectedAsset.decimals))
? '∞'
: formatNumber(
allowance?.toString() || 0,
quantumDecimalPlaces(selectedAsset.quantum, selectedAsset.decimals)
);
return (
<div className="mb-4">
<Notification
@@ -211,7 +198,7 @@ const ApprovalTxFeedback = ({
<p>
{t('You approved deposits of up to %s %s.', [
selectedAsset?.symbol,
approvedAllowanceValue,
formatNumber(allowance?.toString() || 0),
])}
</p>
{txLink && <p>{txLink}</p>}
+5 -4
View File
@@ -15,7 +15,6 @@ import { useWeb3ConnectStore } from '@vegaprotocol/web3';
import { useWeb3React } from '@web3-react/core';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import type { DepositBalances } from './use-deposit-balances';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
jest.mock('@vegaprotocol/wallet');
jest.mock('@vegaprotocol/web3');
@@ -91,7 +90,7 @@ describe('Deposit form', () => {
// Assert default values (including) from/to provided by useVegaWallet and useWeb3React
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
truncateMiddle(MOCK_ETH_ADDRESS)
MOCK_ETH_ADDRESS
);
expect(screen.getByLabelText('Asset')).toHaveValue('');
expect(screen.getByLabelText('To (Vega key)')).toHaveValue('');
@@ -305,7 +304,9 @@ describe('Deposit form', () => {
target: { value: '8' },
});
fireEvent.click(screen.getByRole('button', { name: 'Deposit' }));
fireEvent.click(
screen.getByText('Deposit', { selector: '[type="submit"]' })
);
await waitFor(() => {
expect(props.submitDeposit).toHaveBeenCalledWith({
@@ -352,7 +353,7 @@ describe('Deposit form', () => {
).not.toBeInTheDocument();
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
truncateMiddle(MOCK_ETH_ADDRESS)
MOCK_ETH_ADDRESS
);
});
+11 -11
View File
@@ -13,6 +13,7 @@ import {
import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import {
Button,
TradingFormGroup,
TradingInput,
TradingInputError,
@@ -21,8 +22,6 @@ import {
Intent,
ButtonLink,
TradingSelect,
truncateMiddle,
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core';
@@ -174,7 +173,7 @@ export const DepositForm = ({
return (
<div className="text-sm" aria-describedby="ethereum-address">
<p className="mb-1 break-all" data-testid="ethereum-address">
{truncateMiddle(account)}
{account}
</p>
<DisconnectEthereumButton
onDisconnect={() => {
@@ -186,14 +185,14 @@ export const DepositForm = ({
);
}
return (
<TradingButton
<Button
onClick={openDialog}
intent={Intent.Primary}
variant="primary"
type="button"
data-testid="connect-eth-wallet-btn"
>
{t('Connect')}
</TradingButton>
</Button>
);
}}
/>
@@ -435,14 +434,15 @@ const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
/>
</div>
)}
<TradingButton
<Button
type="submit"
data-testid="deposit-submit"
variant={isActive ? 'primary' : 'default'}
fill
disabled={!isActive || invalidChain}
disabled={invalidChain}
>
{t('Deposit')}
</TradingButton>
</Button>
</>
);
};
@@ -454,7 +454,7 @@ const UseButton = (props: UseButtonProps) => {
<button
{...props}
type="button"
className="absolute top-0 right-0 ml-auto text-sm underline"
className="ml-auto text-sm absolute top-0 right-0 underline"
/>
);
};
@@ -512,7 +512,7 @@ export const AddressField = ({
setIsInput((curr) => !curr);
onChange();
}}
className="absolute top-0 right-0 ml-auto text-sm underline"
className="ml-auto text-sm absolute top-0 right-0 underline"
data-testid="enter-pubkey-manually"
>
{isInput ? t('Select from wallet') : t('Enter manually')}
+4 -10
View File
@@ -7,7 +7,7 @@ import {
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import type BigNumber from 'bignumber.js';
import { formatNumber, quantumDecimalPlaces } from '@vegaprotocol/utils';
import { formatNumber } from '@vegaprotocol/utils';
// Note: all of the values here are with correct asset's decimals
// See `libs/deposits/src/lib/use-deposit-balances.ts`
@@ -35,10 +35,7 @@ export const DepositLimits = ({
label: t('Balance available'),
rawValue: balance,
value: balance ? (
<CompactNumber
number={balance}
decimals={quantumDecimalPlaces(asset.quantum, asset.decimals)}
/>
<CompactNumber number={balance} decimals={asset.decimals} />
) : (
'-'
),
@@ -76,7 +73,7 @@ export const DepositLimits = ({
value: !exempt ? (
<CompactNumber
number={max.minus(deposited)}
decimals={quantumDecimalPlaces(asset.quantum, asset.decimals)}
decimals={asset.decimals}
/>
) : (
<div data-testid="exempt">{t('Exempt')}</div>
@@ -100,10 +97,7 @@ export const DepositLimits = ({
),
rawValue: allowance,
value: allowance ? (
<CompactNumber
number={allowance}
decimals={quantumDecimalPlaces(asset.quantum, asset.decimals)}
/>
<CompactNumber number={allowance} decimals={asset.decimals} />
) : (
'-'
),
+1 -21
View File
@@ -4,6 +4,7 @@ import { getDateTimeFormat } from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
import type { Trade } from './fills-data-provider';
import { FillsTable, getFeesBreakdown } from './fills-table';
import { generateFill } from './test-helpers';
@@ -214,27 +215,6 @@ describe('FillsTable', () => {
).toBeInTheDocument();
});
it('negative positionDecimalPoints should be properly rendered in size column', async () => {
const partyId = 'party-id';
const negativeDecimalPositionFill = generateFill({
...defaultFill,
market: {
...defaultFill.market,
positionDecimalPlaces: -4,
},
});
await act(async () => {
render(
<FillsTable partyId={partyId} rowData={[negativeDecimalPositionFill]} />
);
});
const sizeCell = screen
.getAllByRole('gridcell')
.find((c) => c.getAttribute('col-id') === 'size');
expect(sizeCell).toHaveTextContent('3,000,000,000');
});
describe('getFeesBreakdown', () => {
it('should return correct fees breakdown for a taker', () => {
const fees = {
+4 -3
View File
@@ -1,7 +1,7 @@
import { useRef, useState } from 'react';
import { z } from 'zod';
import {
TradingButton,
Button,
Loader,
TradingFormGroup,
TradingInput,
@@ -157,14 +157,15 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
<Loader size="small" />
</div>
)}
<TradingButton
<Button
variant="primary"
fill
disabled={disabled}
type="submit"
data-testid="ledger-download-button"
>
{t('Download')}
</TradingButton>
</Button>
</div>
</form>
);
@@ -1,148 +0,0 @@
import { useState } from 'react';
import {
VegaIcon,
VegaIconNames,
TradingDropdown,
TradingDropdownTrigger,
TradingDropdownContent,
TradingDropdownItem,
} from '@vegaprotocol/ui-toolkit';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
export const OrderbookControls = ({
lastTradedPrice,
resolution,
decimalPlaces,
setResolution,
}: {
lastTradedPrice: string;
resolution: number;
decimalPlaces: number;
setResolution: (resolution: number) => void;
}) => {
const [isOpen, setOpen] = useState(false);
const resolutions = createResolutions(lastTradedPrice, decimalPlaces);
const increaseResolution = () => {
const index = resolutions.indexOf(resolution);
if (index < resolutions.length - 1) {
setResolution(resolutions[index + 1]);
}
};
const decreaseResolution = () => {
const index = resolutions.indexOf(resolution);
if (index > 0) {
setResolution(resolutions[index - 1]);
}
};
return (
<div className="flex h-6">
<button
onClick={increaseResolution}
disabled={resolutions.indexOf(resolution) >= resolutions.length - 1}
className="flex items-center px-2 border-r cursor-pointer border-default disabled:cursor-default"
data-testid="plus-button"
>
<VegaIcon size={12} name={VegaIconNames.PLUS} />
</button>
<TradingDropdown
open={isOpen}
onOpenChange={(open) => setOpen(open)}
trigger={
<TradingDropdownTrigger data-testid="resolution">
<button
className="flex items-center justify-between px-2 gap-1"
style={{
minWidth: `${
Math.max.apply(
null,
resolutions.map(
(item) => formatResolution(item, decimalPlaces).length
)
) + 5
}ch`,
}}
>
<VegaIcon
size={12}
name={
isOpen ? VegaIconNames.CHEVRON_UP : VegaIconNames.CHEVRON_DOWN
}
/>
{formatResolution(resolution, decimalPlaces)}
</button>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent align="start">
{resolutions.map((r) => (
<TradingDropdownItem
key={r}
onClick={() => setResolution(r)}
className="justify-end"
>
{formatResolution(r, decimalPlaces)}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<button
onClick={decreaseResolution}
disabled={resolutions.indexOf(resolution) <= 0}
className="flex items-center px-2 cursor-pointer border-x border-default disabled:cursor-default"
data-testid="minus-button"
>
<VegaIcon size={12} name={VegaIconNames.MINUS} />
</button>
</div>
);
};
export const formatResolution = (r: number, decimalPlaces: number) => {
let num = addDecimalsFormatNumber(r, decimalPlaces);
// Remove trailing zeroes
num = num.replace(/\.?0+$/, '');
return num;
};
/**
* Create a list of resolutions based on the largest and smallest
* possible values using the last traded price and the market
* decimal places
*/
export const createResolutions = (
lastTradedPrice: string,
decimalPlaces: number
) => {
// number of levels determined by either the number
// of digits in the last traded price OR the number of decimal
// places. For example:
//
// last traded = 1 (0.001)
// dps = 3
// result = 3
//
// last traded = 100001 (1000.01
// dps = 2
// result = 6
const levelCount = Math.max(lastTradedPrice.length ?? 0, decimalPlaces + 1);
const generatedResolutions = new Array(levelCount)
.fill(null)
.map((_, i) => Math.pow(10, i));
const customResolutions = [2, 5, 20, 50, 200, 500];
const combined = customResolutions.concat(generatedResolutions);
combined.sort((a, b) => a - b);
// Remove any resolutions higher than the generated ones as
// we dont want a custom resolution higher than necessary
const resolutions = combined.filter((r) => {
return r <= generatedResolutions[generatedResolutions.length - 1];
});
return resolutions;
};
@@ -31,12 +31,21 @@ describe('compactRows', () => {
it('counts cumulative vol', () => {
const asks = compactRows(sell, VolumeType.ask, 10);
const bids = compactRows(buy, VolumeType.bid, 10);
expect(asks[0].cumulativeVol).toEqual(4950);
expect(bids[0].cumulativeVol).toEqual(579);
expect(asks[10].cumulativeVol).toEqual(390);
expect(bids[10].cumulativeVol).toEqual(4950);
expect(bids[bids.length - 1].cumulativeVol).toEqual(4950);
expect(asks[asks.length - 1].cumulativeVol).toEqual(390);
expect(asks[0].cumulativeVol.value).toEqual(4950);
expect(bids[0].cumulativeVol.value).toEqual(579);
expect(asks[10].cumulativeVol.value).toEqual(390);
expect(bids[10].cumulativeVol.value).toEqual(4950);
expect(bids[bids.length - 1].cumulativeVol.value).toEqual(4950);
expect(asks[asks.length - 1].cumulativeVol.value).toEqual(390);
});
it('updates relative data', () => {
const asks = compactRows(sell, VolumeType.ask, 10);
const bids = compactRows(buy, VolumeType.bid, 10);
expect(asks[0].cumulativeVol.relativeValue).toEqual(100);
expect(bids[0].cumulativeVol.relativeValue).toEqual(12);
expect(asks[10].cumulativeVol.relativeValue).toEqual(8);
expect(bids[10].cumulativeVol.relativeValue).toEqual(100);
});
});
+38 -20
View File
@@ -5,14 +5,18 @@ export enum VolumeType {
bid,
ask,
}
export interface CumulativeVol {
value: number;
relativeValue?: number;
}
export interface OrderbookRowData {
price: string;
volume: number;
cumulativeVol: number;
value: number;
cumulativeVol: CumulativeVol;
}
export const getPriceLevel = (price: string, resolution: number) => {
export const getPriceLevel = (price: string | bigint, resolution: number) => {
const p = BigInt(price);
const r = BigInt(resolution);
let priceLevel = (p / r) * r;
@@ -22,6 +26,25 @@ export const getPriceLevel = (price: string, resolution: number) => {
return priceLevel.toString();
};
const getMaxVolumes = (orderbookData: OrderbookRowData[]) => ({
cumulativeVol: Math.max(
orderbookData[0]?.cumulativeVol.value,
orderbookData[orderbookData.length - 1]?.cumulativeVol.value
),
});
// round instead of ceil so we will not show 0 if value if different than 0
const toPercentValue = (value?: number) => Math.ceil((value ?? 0) * 100);
const updateRelativeData = (data: OrderbookRowData[]) => {
const { cumulativeVol } = getMaxVolumes(data);
data.forEach((data, i) => {
data.cumulativeVol.relativeValue = toPercentValue(
data.cumulativeVol.value / cumulativeVol
);
});
};
const updateCumulativeVolumeByType = (
data: OrderbookRowData[],
dataType: VolumeType
@@ -30,20 +53,21 @@ const updateCumulativeVolumeByType = (
const maxIndex = data.length - 1;
if (dataType === VolumeType.bid) {
for (let i = 0; i <= maxIndex; i++) {
data[i].cumulativeVol =
data[i].volume + (i !== 0 ? data[i - 1].cumulativeVol : 0);
data[i].cumulativeVol.value =
data[i].value + (i !== 0 ? data[i - 1].cumulativeVol.value : 0);
}
} else {
for (let i = maxIndex; i >= 0; i--) {
data[i].cumulativeVol =
data[i].volume + (i !== maxIndex ? data[i + 1].cumulativeVol : 0);
data[i].cumulativeVol.value =
data[i].value +
(i !== maxIndex ? data[i + 1].cumulativeVol.value : 0);
}
}
}
};
export const compactRows = (
data: PriceLevelFieldsFragment[],
data: PriceLevelFieldsFragment[] | null | undefined,
dataType: VolumeType,
resolution: number
) => {
@@ -51,7 +75,6 @@ export const compactRows = (
getPriceLevel(row.price, resolution)
);
const orderbookData: OrderbookRowData[] = [];
Object.keys(groupedByLevel).forEach((price) => {
const { volume } = groupedByLevel[price].pop() as PriceLevelFieldsFragment;
let value = Number(volume);
@@ -60,11 +83,7 @@ export const compactRows = (
value += Number(subRow.volume);
subRow = groupedByLevel[price].pop();
}
orderbookData.push({
price,
volume: value,
cumulativeVol: 0,
});
orderbookData.push({ price, value, cumulativeVol: { value: 0 } });
});
orderbookData.sort((a, b) => {
@@ -76,9 +95,8 @@ export const compactRows = (
}
return 1;
});
updateCumulativeVolumeByType(orderbookData, dataType);
updateRelativeData(orderbookData);
return orderbookData;
};
@@ -122,7 +140,7 @@ export interface MockDataGeneratorParams {
numberOfSellRows: number;
numberOfBuyRows: number;
overlap: number;
lastTradedPrice: string;
midPrice?: string;
bestStaticBidPrice: number;
bestStaticOfferPrice: number;
}
@@ -130,14 +148,14 @@ export interface MockDataGeneratorParams {
export const generateMockData = ({
numberOfSellRows,
numberOfBuyRows,
lastTradedPrice,
midPrice,
overlap,
bestStaticBidPrice,
bestStaticOfferPrice,
}: MockDataGeneratorParams) => {
let matrix = new Array(numberOfSellRows).fill(undefined);
let price =
Number(lastTradedPrice) + (numberOfSellRows - Math.ceil(overlap / 2) + 1);
Number(midPrice) + (numberOfSellRows - Math.ceil(overlap / 2) + 1);
const sell: PriceLevelFieldsFragment[] = matrix.map((row, i) => ({
price: (price -= 1).toString(),
volume: (numberOfSellRows - i + 1).toString(),
@@ -153,7 +171,7 @@ export const generateMockData = ({
return {
asks: sell,
bids: buy,
lastTradedPrice,
midPrice,
bestStaticBidPrice: bestStaticBidPrice.toString(),
bestStaticOfferPrice: bestStaticOfferPrice.toString(),
};
+10 -12
View File
@@ -17,7 +17,7 @@ export type OrderbookData = {
interface OrderbookManagerProps {
marketId: string;
onClick: (args: { price?: string; size?: string }) => void;
onClick?: (args: { price?: string; size?: string }) => void;
}
export const OrderbookManager = ({
@@ -61,17 +61,15 @@ export const OrderbookManager = ({
data={data}
reload={reload}
>
{market && marketData && (
<Orderbook
bids={data?.depth.buy ?? []}
asks={data?.depth.sell ?? []}
decimalPlaces={market.decimalPlaces}
positionDecimalPlaces={market.positionDecimalPlaces}
assetSymbol={market.tradableInstrument.instrument.product.quoteName}
onClick={onClick}
lastTradedPrice={marketData.lastTradedPrice}
/>
)}
<Orderbook
bids={data?.depth.buy ?? []}
asks={data?.depth.sell ?? []}
decimalPlaces={market?.decimalPlaces ?? 0}
positionDecimalPlaces={market?.positionDecimalPlaces ?? 0}
assetSymbol={market?.tradableInstrument.instrument.product.quoteName}
onClick={onClick}
midPrice={marketData?.midPrice}
/>
</AsyncRenderer>
);
};
+128 -123
View File
@@ -1,153 +1,158 @@
import type { ReactNode } from 'react';
import { memo } from 'react';
import React, { memo } from 'react';
import { addDecimal, addDecimalsFixedFormatNumber } from '@vegaprotocol/utils';
import { NumericCell } from '@vegaprotocol/datagrid';
import { NumericCell, PriceCell } from '@vegaprotocol/datagrid';
import { VolumeType } from './orderbook-data';
import classNames from 'classnames';
const HIDE_VOL_WIDTH = 190;
const HIDE_CUMULATIVE_VOL_WIDTH = 260;
interface OrderbookRowProps {
volume: number;
cumulativeVolume: number;
value: number;
cumulativeValue?: number;
cumulativeRelativeValue?: number;
decimalPlaces: number;
positionDecimalPlaces: number;
priceFormatDecimalPlaces: number;
price: string;
onClick: (args: { price?: string; size?: string }) => void;
onClick?: (args: { price?: string; size?: string }) => void;
type: VolumeType;
width: number;
maxVol: number;
}
export const OrderbookRow = memo(
({
volume,
cumulativeVolume,
decimalPlaces,
positionDecimalPlaces,
priceFormatDecimalPlaces,
price,
onClick,
type,
width,
maxVol,
}: OrderbookRowProps) => {
const txtId = type === VolumeType.bid ? 'bid' : 'ask';
const cols =
width >= HIDE_CUMULATIVE_VOL_WIDTH ? 3 : width >= HIDE_VOL_WIDTH ? 2 : 1;
return (
<div className="relative px-1">
<CumulationBar
cumulativeVolume={cumulativeVolume}
type={type}
maxVol={maxVol}
/>
<div
data-testid={`${txtId}-rows-container`}
className={classNames('grid gap-1 text-right', `grid-cols-${cols}`)}
>
<OrderBookRowCell
onClick={() => onClick({ price: addDecimal(price, decimalPlaces) })}
>
<NumericCell
testId={`price-${price}`}
value={BigInt(price)}
valueFormatted={addDecimalsFixedFormatNumber(
price,
decimalPlaces,
priceFormatDecimalPlaces
)}
className={classNames({
'text-market-red dark:text-market-red': type === VolumeType.ask,
'text-market-green-600 dark:text-market-green':
type === VolumeType.bid,
})}
/>
</OrderBookRowCell>
{width >= HIDE_VOL_WIDTH && (
<OrderBookRowCell
onClick={() =>
onClick({ size: addDecimal(volume, positionDecimalPlaces) })
}
>
<NumericCell
testId={`${txtId}-vol-${price}`}
value={volume}
valueFormatted={addDecimalsFixedFormatNumber(
volume,
positionDecimalPlaces ?? 0
)}
/>
</OrderBookRowCell>
)}
{width >= HIDE_CUMULATIVE_VOL_WIDTH && (
<OrderBookRowCell
onClick={() =>
onClick({
size: addDecimal(cumulativeVolume, positionDecimalPlaces),
})
}
>
<NumericCell
testId={`cumulative-vol-${price}`}
value={cumulativeVolume}
valueFormatted={addDecimalsFixedFormatNumber(
cumulativeVolume,
positionDecimalPlaces
)}
/>
</OrderBookRowCell>
)}
</div>
</div>
);
}
);
OrderbookRow.displayName = 'OrderbookRow';
const OrderBookRowCell = ({
children,
onClick,
}: {
children: ReactNode;
onClick: () => void;
}) => {
return (
<button
className="overflow-hidden text-right text-ellipsis whitespace-nowrap hover:dark:bg-neutral-800 hover:bg-neutral-200"
onClick={onClick}
>
{children}
</button>
);
};
const HIDE_VOL_WIDTH = 150;
const HIDE_CUMULATIVE_VOL_WIDTH = 220;
const CumulationBar = ({
cumulativeVolume = 0,
cumulativeValue = 0,
type,
maxVol,
}: {
cumulativeVolume: number;
cumulativeValue?: number;
type: VolumeType;
maxVol: number;
}) => {
const width = (cumulativeVolume / maxVol) * 100;
return (
<div
data-testid={`${VolumeType.bid === type ? 'bid' : 'ask'}-bar`}
className={classNames(
'absolute top-0 left-0 h-full',
type === VolumeType.bid
? 'bg-market-green/10 dark:bg-market-green/10'
: 'bg-market-red/10 dark:bg-market-red/10'
? 'bg-market-green-300 dark:bg-market-green/50'
: 'bg-market-red-300 dark:bg-market-red/30'
)}
style={{
width: `${width}%`,
width: `${cumulativeValue}%`,
}}
/>
);
};
const CumulativeVol = memo(
({
testId,
positionDecimalPlaces,
cumulativeValue,
onClick,
}: {
ask?: number;
bid?: number;
cumulativeValue?: number;
testId?: string;
className?: string;
positionDecimalPlaces: number;
onClick?: (size?: string | number) => void;
}) => {
const volume = cumulativeValue ? (
<NumericCell
testId={testId}
value={cumulativeValue}
valueFormatted={addDecimalsFixedFormatNumber(
cumulativeValue,
positionDecimalPlaces ?? 0
)}
/>
) : null;
return onClick && volume ? (
<button
onClick={() => onClick(cumulativeValue)}
className="hover:dark:bg-neutral-800 hover:bg-neutral-200 text-right pr-1"
>
{volume}
</button>
) : (
<div className="pr-1" data-testid={testId}>
{volume}
</div>
);
}
);
CumulativeVol.displayName = 'OrderBookCumulativeVol';
export const OrderbookRow = React.memo(
({
value,
cumulativeValue,
cumulativeRelativeValue,
decimalPlaces,
positionDecimalPlaces,
price,
onClick,
type,
width,
}: OrderbookRowProps) => {
const txtId = type === VolumeType.bid ? 'bid' : 'ask';
const cols =
width >= HIDE_CUMULATIVE_VOL_WIDTH ? 3 : width >= HIDE_VOL_WIDTH ? 2 : 1;
return (
<div className="relative pr-1">
<CumulationBar cumulativeValue={cumulativeRelativeValue} type={type} />
<div
data-testid={`${txtId}-rows-container`}
className={classNames('grid gap-1 text-right', `grid-cols-${cols}`)}
>
<PriceCell
testId={`price-${price}`}
value={BigInt(price)}
onClick={() =>
onClick && onClick({ price: addDecimal(price, decimalPlaces) })
}
valueFormatted={addDecimalsFixedFormatNumber(price, decimalPlaces)}
className={
type === VolumeType.ask
? 'text-market-red dark:text-market-red'
: 'text-market-green-600 dark:text-market-green'
}
/>
{width >= HIDE_VOL_WIDTH && (
<PriceCell
testId={`${txtId}-vol-${price}`}
onClick={(value) =>
onClick &&
value &&
onClick({
size: addDecimal(value, positionDecimalPlaces),
})
}
value={value}
valueFormatted={addDecimalsFixedFormatNumber(
value,
positionDecimalPlaces
)}
/>
)}
{width >= HIDE_CUMULATIVE_VOL_WIDTH && (
<CumulativeVol
testId={`cumulative-vol-${price}`}
onClick={() =>
onClick &&
cumulativeValue &&
onClick({
size: addDecimal(cumulativeValue, positionDecimalPlaces),
})
}
positionDecimalPlaces={positionDecimalPlaces}
cumulativeValue={cumulativeValue}
/>
)}
</div>
</div>
);
}
);
OrderbookRow.displayName = 'OrderbookRow';
+22 -131
View File
@@ -1,9 +1,8 @@
import { render, waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { generateMockData, VolumeType } from './orderbook-data';
import { Orderbook, OrderbookMid } from './orderbook';
import { Orderbook } from './orderbook';
import * as orderbookData from './orderbook-data';
import { createResolutions, formatResolution } from './orderbook-controls';
function mockOffsetSize(width: number, height: number) {
Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', {
@@ -25,7 +24,7 @@ describe('Orderbook', () => {
numberOfSellRows: 100,
numberOfBuyRows: 100,
step: 1,
lastTradedPrice: '122900',
midPrice: '122900',
bestStaticBidPrice: 122905,
bestStaticOfferPrice: 122895,
decimalPlaces: 3,
@@ -38,22 +37,20 @@ describe('Orderbook', () => {
jest.clearAllMocks();
mockOffsetSize(800, 768);
});
it('lastTradedPrice should be in the middle', async () => {
it('markPrice should be in the middle', async () => {
render(
<Orderbook
decimalPlaces={decimalPlaces}
positionDecimalPlaces={0}
{...generateMockData(params)}
assetSymbol="USD"
onClick={jest.fn()}
/>
);
await waitFor(() =>
screen.getByTestId(`last-traded-${params.lastTradedPrice}`)
screen.getByTestId(`middle-mark-price-${params.midPrice}`)
);
expect(
screen.getByTestId(`last-traded-${params.lastTradedPrice}`)
screen.getByTestId(`middle-mark-price-${params.midPrice}`)
).toHaveTextContent('122.90');
});
@@ -71,11 +68,10 @@ describe('Orderbook', () => {
/>
);
expect(
await screen.findByTestId(`last-traded-${params.lastTradedPrice}`)
await screen.findByTestId(`middle-mark-price-${params.midPrice}`)
).toBeInTheDocument();
// Before resolution change the price is 122.934
await userEvent.click(screen.getByTestId('price-122901'));
await userEvent.click(await screen.getByTestId('price-122901'));
expect(onClickSpy).toBeCalledWith({ price: '122.901' });
await userEvent.click(screen.getByTestId('resolution'));
@@ -89,16 +85,15 @@ describe('Orderbook', () => {
expect(orderbookData.compactRows).toHaveBeenCalledWith(
mockedData.bids,
VolumeType.bid,
2
10
);
expect(orderbookData.compactRows).toHaveBeenCalledWith(
mockedData.asks,
VolumeType.ask,
2
10
);
await userEvent.click(screen.getByTestId('price-122938'));
expect(onClickSpy).toBeCalledWith({ price: '122.938' });
await userEvent.click(await screen.getByTestId('price-12294'));
expect(onClickSpy).toBeCalledWith({ price: '122.94' });
});
it('plus - minus buttons should change resolution', async () => {
@@ -118,30 +113,26 @@ describe('Orderbook', () => {
1
);
expect(screen.getByTestId('minus-button')).toBeDisabled();
await userEvent.click(screen.getByTestId('plus-button'));
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
2
);
await userEvent.click(screen.getByTestId('plus-button'));
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
5
);
expect(screen.getByTestId('minus-button')).not.toBeDisabled();
await userEvent.click(screen.getByTestId('minus-button'));
userEvent.click(screen.getByTestId('plus-button'));
await waitFor(() => {
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
2
10
);
});
expect(screen.getByTestId('minus-button')).not.toBeDisabled();
userEvent.click(screen.getByTestId('minus-button'));
await waitFor(() => {
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
1
);
});
expect(screen.getByTestId('minus-button')).toBeDisabled();
await userEvent.click(screen.getByTestId('resolution'));
await waitFor(() => {
expect(screen.getByRole('menu')).toBeInTheDocument();
});
await userEvent.click(screen.getAllByRole('menuitem')[11]);
await userEvent.click(screen.getAllByRole('menuitem')[5]);
await waitFor(() => {
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
100000
@@ -186,103 +177,3 @@ describe('Orderbook', () => {
});
});
});
describe('OrderbookMid', () => {
const props = {
lastTradedPrice: '100',
decimalPlaces: 0,
assetSymbol: 'BTC',
bestAskPrice: '101',
bestBidPrice: '99',
};
it('renders no change until lastTradedPrice changes', () => {
const { rerender } = render(<OrderbookMid {...props} />);
expect(screen.getByTestId(/last-traded/)).toHaveTextContent(
props.lastTradedPrice
);
expect(screen.getByText(props.assetSymbol)).toBeInTheDocument();
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
expect(screen.getByTestId('spread')).toHaveTextContent('(2)');
// rerender with no change should not show the icon
rerender(<OrderbookMid {...props} />);
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
rerender(
<OrderbookMid {...props} lastTradedPrice="101" bestAskPrice="102" />
);
expect(screen.getByTestId('icon-arrow-up')).toBeInTheDocument();
expect(screen.getByTestId('spread')).toHaveTextContent('(3)');
// rerender again with the same price, should still be set to 'up'
rerender(
<OrderbookMid
{...props}
lastTradedPrice="101"
bestAskPrice="102"
bestBidPrice="98"
/>
);
expect(screen.getByTestId('icon-arrow-up')).toBeInTheDocument();
expect(screen.getByTestId('spread')).toHaveTextContent('(4)');
rerender(<OrderbookMid {...props} lastTradedPrice="100" />);
expect(screen.getByTestId('icon-arrow-down')).toBeInTheDocument();
});
});
describe('createResolutions', () => {
it('create resolutions relative to the market', () => {
expect(
createResolutions(
'1', // 0.001
3
)
).toEqual([1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]);
expect(
createResolutions(
'190017', // 1900.17
2
)
).toEqual([1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 10000, 100000]);
expect(
createResolutions(
'123456789', // 1234.56789
5
)
).toEqual([
1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 10000, 100000, 1000000,
10000000, 100000000,
]);
});
it('removes resolutions that arent precise enough for the market', () => {
expect(
createResolutions(
'1', // 0.01
2
)
).toEqual([1, 2, 5, 10, 20, 50, 100]);
});
});
describe('formatResolution', () => {
it('formats less than 1', () => {
expect(formatResolution(1, 2)).toEqual('0.01');
expect(formatResolution(1, 3)).toEqual('0.001');
expect(formatResolution(2, 4)).toEqual('0.0002');
expect(formatResolution(5, 8)).toEqual('0.00000005');
expect(formatResolution(10000, 5)).toEqual('0.1');
});
it('formats greater than 1', () => {
expect(formatResolution(1000, 2)).toEqual('10');
expect(formatResolution(100000, 4)).toEqual('10');
expect(formatResolution(10000000, 2)).toEqual('100,000');
expect(formatResolution(500, 2)).toEqual('5');
expect(formatResolution(500, 1)).toEqual('50');
});
});

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