From 50959b4c50664c8340847985f32d5c0d3d846aa1 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 31 Aug 2023 13:15:02 +0100 Subject: [PATCH 01/16] chore(trading): delete closed market tests (#4660) --- .../src/integration/closed-markets.cy.ts | 475 ------------------ 1 file changed, 475 deletions(-) delete mode 100644 apps/trading-e2e/src/integration/closed-markets.cy.ts diff --git a/apps/trading-e2e/src/integration/closed-markets.cy.ts b/apps/trading-e2e/src/integration/closed-markets.cy.ts deleted file mode 100644 index f0861de49..000000000 --- a/apps/trading-e2e/src/integration/closed-markets.cy.ts +++ /dev/null @@ -1,475 +0,0 @@ -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'); - }); -}); From de4c7926c24137c7d34e70b6ccc185d7c4956f97 Mon Sep 17 00:00:00 2001 From: Art Date: Thu, 31 Aug 2023 15:30:03 +0200 Subject: [PATCH 02/16] chore(deposits): formatting tweaks (#4648) Co-authored-by: Dariusz Majcherczyk --- .../src/integration/market-info.cy.ts | 6 +- .../src/integration/wallet-eth.cy.ts | 3 +- libs/assets/src/lib/asset-details-dialog.tsx | 2 +- libs/assets/src/lib/asset-details-table.tsx | 12 +++- libs/assets/src/lib/asset-option.tsx | 4 +- .../deposits/src/lib/approve-notification.tsx | 17 ++++- libs/deposits/src/lib/deposit-form.spec.tsx | 5 +- libs/deposits/src/lib/deposit-form.tsx | 3 +- libs/deposits/src/lib/deposit-limits.tsx | 14 +++-- libs/react-helpers/src/lib/format/number.tsx | 11 +++- libs/utils/src/lib/format/number.spec.ts | 62 +++++++++++++++++++ libs/utils/src/lib/format/number.ts | 31 ++++++++++ 12 files changed, 147 insertions(+), 23 deletions(-) diff --git a/apps/trading-e2e/src/integration/market-info.cy.ts b/apps/trading-e2e/src/integration/market-info.cy.ts index 9e6fd8c08..35720c4a6 100644 --- a/apps/trading-e2e/src/integration/market-info.cy.ts +++ b/apps/trading-e2e/src/integration/market-info.cy.ts @@ -133,11 +133,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => { validateMarketDataRow(4, 'Decimals', '5'); validateMarketDataRow(5, 'Quantum', '1'); validateMarketDataRow(6, 'Status', 'Enabled'); - validateMarketDataRow( - 7, - 'Contract address', - '0x0158031158Bb4dF2AD02eAA31e8963E84EA978a4' - ); + validateMarketDataRow(7, 'Contract address', '0x0158…78a4'); validateMarketDataRow(8, 'Withdrawal threshold', '0.0005'); validateMarketDataRow(9, 'Lifetime limit', '1,230'); validateMarketDataRow(10, 'Infrastructure fee account balance', '0.00001'); diff --git a/apps/trading-e2e/src/integration/wallet-eth.cy.ts b/apps/trading-e2e/src/integration/wallet-eth.cy.ts index 9859ff4c9..a5a5f89cf 100644 --- a/apps/trading-e2e/src/integration/wallet-eth.cy.ts +++ b/apps/trading-e2e/src/integration/wallet-eth.cy.ts @@ -43,11 +43,10 @@ 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', ethWalletAddress); + cy.getByTestId('ethereum-address').should('have.text', '0xEe7D…d94F'); cy.getByTestId('disconnect-ethereum-wallet') .should('have.text', 'Disconnect') .click(); diff --git a/libs/assets/src/lib/asset-details-dialog.tsx b/libs/assets/src/lib/asset-details-dialog.tsx index f1bf66e39..1fb26fe48 100644 --- a/libs/assets/src/lib/asset-details-dialog.tsx +++ b/libs/assets/src/lib/asset-details-dialog.tsx @@ -97,7 +97,7 @@ export const AssetDetailsDialog = ({ }} > {content} -

+

{t( 'There is 1 unit of the settlement asset (%s) to every 1 quote unit.', [assetSymbol] diff --git a/libs/assets/src/lib/asset-details-table.tsx b/libs/assets/src/lib/asset-details-table.tsx index dcf179748..eca9a30a3 100644 --- a/libs/assets/src/lib/asset-details-table.tsx +++ b/libs/assets/src/lib/asset-details-table.tsx @@ -3,7 +3,11 @@ 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 { CopyWithTooltip, Icon } from '@vegaprotocol/ui-toolkit'; +import { + CopyWithTooltip, + Icon, + truncateMiddle, +} from '@vegaprotocol/ui-toolkit'; import { KeyValueTable, KeyValueTableRow, @@ -56,7 +60,7 @@ export const rows: Rows = [ key: AssetDetail.ID, label: t('ID'), tooltip: '', - value: (asset) => asset.id, + value: (asset) => truncateMiddle(asset.id), }, { key: AssetDetail.TYPE, @@ -109,7 +113,9 @@ export const rows: Rows = [ return ( <> - {' '} + + {truncateMiddle(asset.source.contractAddress)} + {' '} + setOpen(open)} + trigger={ + + + + } + > + + {resolutions.map((r) => ( + setResolution(r)}> + {formatResolution(r)} + + ))} + + + + + ); +}; diff --git a/libs/market-depth/src/lib/orderbook-data.spec.ts b/libs/market-depth/src/lib/orderbook-data.spec.ts index 871ca4145..389cc2919 100644 --- a/libs/market-depth/src/lib/orderbook-data.spec.ts +++ b/libs/market-depth/src/lib/orderbook-data.spec.ts @@ -31,21 +31,12 @@ describe('compactRows', () => { it('counts cumulative vol', () => { const asks = compactRows(sell, VolumeType.ask, 10); const bids = compactRows(buy, VolumeType.bid, 10); - expect(asks[0].cumulativeVol.value).toEqual(4950); - expect(bids[0].cumulativeVol.value).toEqual(579); - expect(asks[10].cumulativeVol.value).toEqual(390); - expect(bids[10].cumulativeVol.value).toEqual(4950); - expect(bids[bids.length - 1].cumulativeVol.value).toEqual(4950); - expect(asks[asks.length - 1].cumulativeVol.value).toEqual(390); - }); - - it('updates relative data', () => { - const 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); + 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); }); }); diff --git a/libs/market-depth/src/lib/orderbook-data.ts b/libs/market-depth/src/lib/orderbook-data.ts index 8d54ae2e2..31be1491a 100644 --- a/libs/market-depth/src/lib/orderbook-data.ts +++ b/libs/market-depth/src/lib/orderbook-data.ts @@ -5,15 +5,11 @@ export enum VolumeType { bid, ask, } -export interface CumulativeVol { - value: number; - relativeValue?: number; -} export interface OrderbookRowData { price: string; - value: number; - cumulativeVol: CumulativeVol; + volume: number; + cumulativeVol: number; } export const getPriceLevel = (price: string | bigint, resolution: number) => { @@ -26,25 +22,6 @@ export const getPriceLevel = (price: string | bigint, 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 @@ -53,14 +30,13 @@ const updateCumulativeVolumeByType = ( const maxIndex = data.length - 1; if (dataType === VolumeType.bid) { for (let i = 0; i <= maxIndex; i++) { - data[i].cumulativeVol.value = - data[i].value + (i !== 0 ? data[i - 1].cumulativeVol.value : 0); + data[i].cumulativeVol = + data[i].volume + (i !== 0 ? data[i - 1].cumulativeVol : 0); } } else { for (let i = maxIndex; i >= 0; i--) { - data[i].cumulativeVol.value = - data[i].value + - (i !== maxIndex ? data[i + 1].cumulativeVol.value : 0); + data[i].cumulativeVol = + data[i].volume + (i !== maxIndex ? data[i + 1].cumulativeVol : 0); } } } @@ -75,6 +51,7 @@ 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); @@ -83,7 +60,11 @@ export const compactRows = ( value += Number(subRow.volume); subRow = groupedByLevel[price].pop(); } - orderbookData.push({ price, value, cumulativeVol: { value: 0 } }); + orderbookData.push({ + price, + volume: value, + cumulativeVol: 0, + }); }); orderbookData.sort((a, b) => { @@ -95,8 +76,9 @@ export const compactRows = ( } return 1; }); + updateCumulativeVolumeByType(orderbookData, dataType); - updateRelativeData(orderbookData); + return orderbookData; }; @@ -140,7 +122,7 @@ export interface MockDataGeneratorParams { numberOfSellRows: number; numberOfBuyRows: number; overlap: number; - midPrice?: string; + lastTradedPrice: string; bestStaticBidPrice: number; bestStaticOfferPrice: number; } @@ -148,14 +130,14 @@ export interface MockDataGeneratorParams { export const generateMockData = ({ numberOfSellRows, numberOfBuyRows, - midPrice, + lastTradedPrice, overlap, bestStaticBidPrice, bestStaticOfferPrice, }: MockDataGeneratorParams) => { let matrix = new Array(numberOfSellRows).fill(undefined); let price = - Number(midPrice) + (numberOfSellRows - Math.ceil(overlap / 2) + 1); + Number(lastTradedPrice) + (numberOfSellRows - Math.ceil(overlap / 2) + 1); const sell: PriceLevelFieldsFragment[] = matrix.map((row, i) => ({ price: (price -= 1).toString(), volume: (numberOfSellRows - i + 1).toString(), @@ -171,7 +153,7 @@ export const generateMockData = ({ return { asks: sell, bids: buy, - midPrice, + lastTradedPrice, bestStaticBidPrice: bestStaticBidPrice.toString(), bestStaticOfferPrice: bestStaticOfferPrice.toString(), }; diff --git a/libs/market-depth/src/lib/orderbook-manager.tsx b/libs/market-depth/src/lib/orderbook-manager.tsx index 2f6a0b003..dea18ba3c 100644 --- a/libs/market-depth/src/lib/orderbook-manager.tsx +++ b/libs/market-depth/src/lib/orderbook-manager.tsx @@ -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,15 +61,17 @@ export const OrderbookManager = ({ data={data} reload={reload} > - + {market && marketData && ( + + )} ); }; diff --git a/libs/market-depth/src/lib/orderbook-row.tsx b/libs/market-depth/src/lib/orderbook-row.tsx index 293b46065..82cc238c1 100644 --- a/libs/market-depth/src/lib/orderbook-row.tsx +++ b/libs/market-depth/src/lib/orderbook-row.tsx @@ -1,158 +1,149 @@ -import React, { memo } from 'react'; +import type { ReactNode } from 'react'; +import { memo } from 'react'; import { addDecimal, addDecimalsFixedFormatNumber } from '@vegaprotocol/utils'; -import { NumericCell, PriceCell } from '@vegaprotocol/datagrid'; +import { NumericCell } 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 { - value: number; - cumulativeValue?: number; - cumulativeRelativeValue?: number; + volume: number; + cumulativeVolume: number; decimalPlaces: number; positionDecimalPlaces: number; price: string; - onClick?: (args: { price?: string; size?: string }) => void; + onClick: (args: { price?: string; size?: string }) => void; type: VolumeType; width: number; + maxVol: number; } -const HIDE_VOL_WIDTH = 150; -const HIDE_CUMULATIVE_VOL_WIDTH = 220; - -const CumulationBar = ({ - cumulativeValue = 0, - type, -}: { - cumulativeValue?: number; - type: VolumeType; -}) => { - return ( -

- ); -}; - -const CumulativeVol = memo( +export const OrderbookRow = 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 ? ( - - ) : null; - - return onClick && volume ? ( - - ) : ( -
- {volume} -
- ); - } -); -CumulativeVol.displayName = 'OrderBookCumulativeVol'; - -export const OrderbookRow = React.memo( - ({ - value, - cumulativeValue, - cumulativeRelativeValue, + volume, + cumulativeVolume, decimalPlaces, positionDecimalPlaces, 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 ( -
- +
+
- - 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 && ( - - onClick && - value && - onClick({ - size: addDecimal(value, positionDecimalPlaces), - }) - } - value={value} + onClick({ price: addDecimal(price, decimalPlaces) })} + > + + + {width >= HIDE_VOL_WIDTH && ( + + onClick({ size: addDecimal(volume, positionDecimalPlaces) }) + } + > + + )} {width >= HIDE_CUMULATIVE_VOL_WIDTH && ( - - onClick && - cumulativeValue && onClick({ - size: addDecimal(cumulativeValue, positionDecimalPlaces), + size: addDecimal(cumulativeVolume, positionDecimalPlaces), }) } - positionDecimalPlaces={positionDecimalPlaces} - cumulativeValue={cumulativeValue} - /> + > + + )}
); } ); + OrderbookRow.displayName = 'OrderbookRow'; + +const OrderBookRowCell = ({ + children, + onClick, +}: { + children: ReactNode; + onClick: () => void; +}) => { + return ( + + ); +}; + +const CumulationBar = ({ + cumulativeVolume = 0, + type, + maxVol, +}: { + cumulativeVolume: number; + type: VolumeType; + maxVol: number; +}) => { + const width = (cumulativeVolume / maxVol) * 100; + return ( +
+ ); +}; diff --git a/libs/market-depth/src/lib/orderbook.spec.tsx b/libs/market-depth/src/lib/orderbook.spec.tsx index 00d9b48e5..68972cd62 100644 --- a/libs/market-depth/src/lib/orderbook.spec.tsx +++ b/libs/market-depth/src/lib/orderbook.spec.tsx @@ -1,7 +1,7 @@ import { render, waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { generateMockData, VolumeType } from './orderbook-data'; -import { Orderbook } from './orderbook'; +import { Orderbook, OrderbookMid } from './orderbook'; import * as orderbookData from './orderbook-data'; function mockOffsetSize(width: number, height: number) { @@ -24,7 +24,7 @@ describe('Orderbook', () => { numberOfSellRows: 100, numberOfBuyRows: 100, step: 1, - midPrice: '122900', + lastTradedPrice: '122900', bestStaticBidPrice: 122905, bestStaticOfferPrice: 122895, decimalPlaces: 3, @@ -44,13 +44,14 @@ describe('Orderbook', () => { positionDecimalPlaces={0} {...generateMockData(params)} assetSymbol="USD" + onClick={jest.fn()} /> ); await waitFor(() => - screen.getByTestId(`middle-mark-price-${params.midPrice}`) + screen.getByTestId(`last-traded-${params.lastTradedPrice}`) ); expect( - screen.getByTestId(`middle-mark-price-${params.midPrice}`) + screen.getByTestId(`last-traded-${params.lastTradedPrice}`) ).toHaveTextContent('122.90'); }); @@ -68,10 +69,10 @@ describe('Orderbook', () => { /> ); expect( - await screen.findByTestId(`middle-mark-price-${params.midPrice}`) + await screen.findByTestId(`last-traded-${params.lastTradedPrice}`) ).toBeInTheDocument(); // Before resolution change the price is 122.934 - await userEvent.click(await screen.getByTestId('price-122901')); + await userEvent.click(screen.getByTestId('price-122901')); expect(onClickSpy).toBeCalledWith({ price: '122.901' }); await userEvent.click(screen.getByTestId('resolution')); @@ -92,7 +93,7 @@ describe('Orderbook', () => { VolumeType.ask, 10 ); - await userEvent.click(await screen.getByTestId('price-12294')); + await userEvent.click(screen.getByTestId('price-12294')); expect(onClickSpy).toBeCalledWith({ price: '122.94' }); }); @@ -177,3 +178,48 @@ 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(); + 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(); + expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument(); + + rerender( + + ); + 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( + + ); + expect(screen.getByTestId('icon-arrow-up')).toBeInTheDocument(); + expect(screen.getByTestId('spread')).toHaveTextContent('(4)'); + + rerender(); + expect(screen.getByTestId('icon-arrow-down')).toBeInTheDocument(); + }); +}); diff --git a/libs/market-depth/src/lib/orderbook.stories.tsx b/libs/market-depth/src/lib/orderbook.stories.tsx index 84400cddf..7346ef6bd 100644 --- a/libs/market-depth/src/lib/orderbook.stories.tsx +++ b/libs/market-depth/src/lib/orderbook.stories.tsx @@ -9,9 +9,9 @@ type Props = Omit & { const OrderbookMockDataProvider = ({ decimalPlaces, ...props }: Props) => { return ( -
+
{ decimalPlaces={decimalPlaces} {...generateMockData({ ...props })} assetSymbol="USD" + onClick={() => undefined} />
diff --git a/libs/market-depth/src/lib/orderbook.tsx b/libs/market-depth/src/lib/orderbook.tsx index 9ccc0a7c0..145ddbb30 100644 --- a/libs/market-depth/src/lib/orderbook.tsx +++ b/libs/market-depth/src/lib/orderbook.tsx @@ -1,25 +1,15 @@ import { useMemo, useRef, useState } from 'react'; import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer'; -import { - addDecimalsFormatNumber, - formatNumberFixed, -} from '@vegaprotocol/utils'; +import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; import { usePrevious } from '@vegaprotocol/react-helpers'; import { OrderbookRow } from './orderbook-row'; import type { OrderbookRowData } from './orderbook-data'; import { compactRows, VolumeType } from './orderbook-data'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - Splash, - VegaIcon, - VegaIconNames, -} from '@vegaprotocol/ui-toolkit'; +import { Splash, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; import classNames from 'classnames'; import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth'; +import { OrderbookControls } from './orderbook-controls'; // Sets row height, will be used to calculate number of rows that can be // displayed each side of the book without overflow @@ -27,20 +17,7 @@ export const rowHeight = 17; const rowGap = 1; const midHeight = 30; -type PriceChange = 'up' | 'down' | 'none'; - -const PRICE_CHANGE_ICON_MAP: Readonly> = { - up: VegaIconNames.ARROW_UP, - down: VegaIconNames.ARROW_DOWN, - none: VegaIconNames.BULLET, -}; -const PRICE_CHANGE_CLASS_MAP: Readonly> = { - up: 'text-market-green-600 dark:text-market-green', - down: 'text-market-red dark:text-market-red', - none: 'text-vega-blue-500', -}; - -const OrderbookTable = ({ +const OrderbookSide = ({ rows, resolution, type, @@ -48,14 +25,16 @@ const OrderbookTable = ({ positionDecimalPlaces, onClick, width, + maxVol, }: { rows: OrderbookRowData[]; resolution: number; decimalPlaces: number; positionDecimalPlaces: number; type: VolumeType; - onClick?: (args: { price?: string; size?: string }) => void; + onClick: (args: { price?: string; size?: string }) => void; width: number; + maxVol: number; }) => { return (
))}
@@ -90,31 +69,88 @@ const OrderbookTable = ({ ); }; +export const OrderbookMid = ({ + lastTradedPrice, + decimalPlaces, + assetSymbol, + bestAskPrice, + bestBidPrice, +}: { + lastTradedPrice: string; + decimalPlaces: number; + assetSymbol: string; + bestAskPrice: string; + bestBidPrice: string; +}) => { + const previousLastTradedPrice = usePrevious(lastTradedPrice); + const priceChangeRef = useRef<'up' | 'down' | 'none'>('none'); + const spread = (BigInt(bestAskPrice) - BigInt(bestBidPrice)).toString(); + + if (previousLastTradedPrice !== lastTradedPrice) { + priceChangeRef.current = + Number(previousLastTradedPrice) > Number(lastTradedPrice) ? 'down' : 'up'; + } + + return ( +
+ {priceChangeRef.current !== 'none' && ( + + + + )} + + {addDecimalsFormatNumber(lastTradedPrice, decimalPlaces)} + + {assetSymbol} + + ({addDecimalsFormatNumber(spread, decimalPlaces)}) + +
+ ); +}; + interface OrderbookProps { decimalPlaces: number; positionDecimalPlaces: number; - onClick?: (args: { price?: string; size?: string }) => void; - midPrice?: string; + onClick: (args: { price?: string; size?: string }) => void; + lastTradedPrice: string; bids: PriceLevelFieldsFragment[]; asks: PriceLevelFieldsFragment[]; - assetSymbol: string | undefined; + assetSymbol: string; } export const Orderbook = ({ decimalPlaces, positionDecimalPlaces, onClick, - midPrice, + lastTradedPrice, asks, bids, assetSymbol, }: OrderbookProps) => { const [resolution, setResolution] = useState(1); - const resolutions = new Array( - Math.max(midPrice?.toString().length ?? 0, decimalPlaces + 1) - ) - .fill(null) - .map((v, i) => Math.pow(10, i)); const groupedAsks = useMemo(() => { return compactRows(asks, VolumeType.ask, resolution); @@ -123,44 +159,11 @@ export const Orderbook = ({ const groupedBids = useMemo(() => { return compactRows(bids, VolumeType.bid, resolution); }, [bids, resolution]); - const [isOpen, setOpen] = useState(false); - const previousMidPrice = usePrevious(midPrice); - const priceChangeRef = useRef<'up' | 'down' | 'none'>('none'); - if (midPrice && previousMidPrice !== midPrice) { - priceChangeRef.current = - (previousMidPrice || '') > midPrice ? 'down' : 'up'; - } - const priceChangeIcon = ( - - - - ); - - const formatResolution = (r: number) => { - return formatNumberFixed( - Math.log10(r) - decimalPlaces > 0 - ? Math.pow(10, Math.log10(r) - decimalPlaces) - : 0, - decimalPlaces - Math.log10(r) - ); - }; - - 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]); - } - }; + // get the best bid/ask, note that we are using the pre aggregated + // values so we can render the most accurate spread in the mid section + const bestAskPrice = asks[0] ? asks[0].price : '0'; + const bestBidPrice = bids[0] ? bids[0].price : '0'; return (
@@ -171,21 +174,30 @@ export const Orderbook = ({ 1, Math.floor((height - midHeight) / 2 / (rowHeight + rowGap)) ); - const askRows = groupedAsks?.slice(limit * -1) ?? []; - const bidRows = groupedBids?.slice(0, limit) ?? []; + const askRows = groupedAsks.slice(limit * -1); + const bidRows = groupedBids.slice(0, limit); + + // this is used for providing a scale to render the volume + // bars based on the visible book + const deepestVisibleAsk = askRows[0]; + const deepestVisibleBid = bidRows[bidRows.length - 1]; + const maxVol = Math.max( + deepestVisibleAsk?.cumulativeVol || 0, + deepestVisibleBid?.cumulativeVol || 0 + ); return (
{askRows.length || bidRows.length ? ( <> - -
- {midPrice && ( - <> - - {addDecimalsFormatNumber(midPrice, decimalPlaces)} - - {assetSymbol} - {priceChangeIcon} - - )} -
- + ) : ( -
+
{t('No data')}
)} @@ -228,59 +235,13 @@ export const Orderbook = ({ }}
-
- - setOpen(open)} - trigger={ - formatResolution(item).length) - ) + 3 - }ch`, - }} - > - -
- {formatResolution(resolution)} -
-
- } - > - - {resolutions.map((r) => ( - setResolution(r)}> - {formatResolution(r)} - - ))} - -
- +
+
); diff --git a/libs/markets/src/lib/__generated__/market-data.ts b/libs/markets/src/lib/__generated__/market-data.ts index 2ce7201b7..385775ebc 100644 --- a/libs/markets/src/lib/__generated__/market-data.ts +++ b/libs/markets/src/lib/__generated__/market-data.ts @@ -3,23 +3,23 @@ import * as Types from '@vegaprotocol/types'; import { gql } from '@apollo/client'; import * as Apollo from '@apollo/client'; const defaultOptions = {} as const; -export type MarketDataUpdateFieldsFragment = { __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }; +export type MarketDataUpdateFieldsFragment = { __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }; export type MarketDataUpdateSubscriptionVariables = Types.Exact<{ marketId: Types.Scalars['ID']; }>; -export type MarketDataUpdateSubscription = { __typename?: 'Subscription', marketsData: Array<{ __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }> }; +export type MarketDataUpdateSubscription = { __typename?: 'Subscription', marketsData: Array<{ __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }> }; -export type MarketDataFieldsFragment = { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }; +export type MarketDataFieldsFragment = { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }; export type MarketDataQueryVariables = Types.Exact<{ marketId: Types.Scalars['ID']; }>; -export type MarketDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null } | null } }> } | null }; +export type MarketDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null } | null } }> } | null }; export const MarketDataUpdateFieldsFragmentDoc = gql` fragment MarketDataUpdateFields on ObservableMarketData { @@ -56,6 +56,7 @@ export const MarketDataUpdateFieldsFragmentDoc = gql` suppliedStake targetStake trigger + lastTradedPrice } `; export const MarketDataFieldsFragmentDoc = gql` @@ -95,6 +96,7 @@ export const MarketDataFieldsFragmentDoc = gql` suppliedStake targetStake trigger + lastTradedPrice } `; export const MarketDataUpdateDocument = gql` diff --git a/libs/markets/src/lib/market-data.graphql b/libs/markets/src/lib/market-data.graphql index 35272316b..fb760b16a 100644 --- a/libs/markets/src/lib/market-data.graphql +++ b/libs/markets/src/lib/market-data.graphql @@ -32,6 +32,7 @@ fragment MarketDataUpdateFields on ObservableMarketData { suppliedStake targetStake trigger + lastTradedPrice } subscription MarketDataUpdate($marketId: ID!) { @@ -76,6 +77,7 @@ fragment MarketDataFields on MarketData { suppliedStake targetStake trigger + lastTradedPrice } query MarketData($marketId: ID!) { diff --git a/libs/markets/src/lib/market-data.mock.ts b/libs/markets/src/lib/market-data.mock.ts index 496000768..d248acb92 100644 --- a/libs/markets/src/lib/market-data.mock.ts +++ b/libs/markets/src/lib/market-data.mock.ts @@ -62,6 +62,7 @@ const marketDataFields: MarketDataFieldsFragment = { markPrice: '4612690058', midPrice: '4612690000', openInterest: '0', + lastTradedPrice: '4612690000', priceMonitoringBounds: [ { minValidPrice: '654701', @@ -99,6 +100,7 @@ const marketDataUpdateFields: MarketDataUpdateFieldsFragment = { marketValueProxy: '', markPrice: '4612690058', midPrice: '0', + lastTradedPrice: '0', openInterest: '0', staticMidPrice: '0', trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED, diff --git a/specs/6003-ORDB-order_book.md b/specs/6003-ORDB-order_book.md index 56acdc4a2..de2a87823 100644 --- a/specs/6003-ORDB-order_book.md +++ b/specs/6003-ORDB-order_book.md @@ -11,4 +11,7 @@ As a market user I want to see information about orders existing in the market. - I **Must** see prices sorted descending (6003-ORDB-007) - I **Must** be able to set a resolution of data (6003-ORDB-008) - When I click specific price, it **Must** be copied to deal ticket form (6003-ORDB-009) -- Order is removed from orderbook if traded away(6003-ORDB-010) +- Order is removed from orderbook if traded away (6003-ORDB-010) +- Spread (bestAsk - bestOffer) is show in the mid secion (6003-ORDB-011) +- Cumulative volume is displayed visually (volume bars) as a proportion of the entire book volume (6003-ORDB-012) +- Mid section shows the last traded price movement using an arrow (6003-ORDB-013) From 105a758e8d31705125146a13159a9de7b2ac53c7 Mon Sep 17 00:00:00 2001 From: daro-maj <119658839+daro-maj@users.noreply.github.com> Date: Fri, 1 Sep 2023 10:48:38 +0200 Subject: [PATCH 07/16] test(trading): add stop order oco spec (#4669) --- specs/0002-WCON-connect_vega_wallet.md | 2 ++ specs/7002-SORD-submit_orders.md | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/specs/0002-WCON-connect_vega_wallet.md b/specs/0002-WCON-connect_vega_wallet.md index d430e34bd..68e110541 100644 --- a/specs/0002-WCON-connect_vega_wallet.md +++ b/specs/0002-WCON-connect_vega_wallet.md @@ -13,6 +13,8 @@ When looking to use Vega via a user interface e.g. Dapp (Decentralized web App), - If there is not running desktop wallet or CLI detected, I can see that I need to open my wallet. (0002-WCON-0013) - I can find out more about supported browsers i.e. there is a link (Issue "List compatible browsers" vegawallet-browser#360 has to be implemented). (0002-WCON-0013) - I can find out more about the Vega Wallet and see what "other" versions there are i.e. there is a link to the page on the website (currently - https://vega.xyz/wallet#overview). (0002-WCON-0014) +- I can install and connect vega metamask snap wallet (0002-WCON-0015) + - Browser wallet: - The browser extension you need is automatically detected if you are using Chrome or Firefox, presenting the specific call to action to install that browser extension in a visible way e.g. with a Chrome or Firefox icon. (0002-WCON-041) diff --git a/specs/7002-SORD-submit_orders.md b/specs/7002-SORD-submit_orders.md index c5e41c202..733388f67 100644 --- a/specs/7002-SORD-submit_orders.md +++ b/specs/7002-SORD-submit_orders.md @@ -174,11 +174,34 @@ When populating a deal ticket I... - **must** see that selecting "Post only" or "reduce only" de-select the other (if selected). As is it not possibly to apply them both to an order (7003-SORD-059) - **must** see an option to select expire for stop order limit and market (7002-SORD-071) + - **must** select strategy for stop order limit and market (7002-SORD-072) - **must** see strategy submit (7002-SORD-073) + if it is oco then submit strategy is not available (7002-SORD-081) - **must** see strategy cancel (7002-SORD-074) - **must** see expiry time/date (7002-SORD-075) +- **must** see an option to select oco for stop order limit and market (7002-SORD-082) + - **must** see a type to select market or limit (7002-SORD-083) + - **must** see for market type + - **must** see rises above (7002-SORD-084) + - **must** see falls below (7002-SORD-085) + - **must** enter a trigger value (7002-SORD-086) + - **must** select a trigger price (7002-SORD-087) + - **must** select a trigger trailing percent offset (7002-SORD-088) + - **must** see for limit type + - **must** enter a price (7002-SORD-089) + - **must** select a trigger direction (7002-SORD-090) + - **must** see rises above (7002-SORD-099) + - **must** see falls below (7002-SORD-091) + - if the user has not set a preference: trigger direction **must** default to `opposite to the one selected in the stop order` (7002-SORD-092) + - **must** enter a trigger value (7002-SORD-093) + - **must** select a trigger type (7002-SORD-094) + - **must** see a trigger price (7002-SORD-095) + - **must** see a trigger trailing percent offset (7002-SORD-096) + - if the user has not set a preference: trigger type **must** default to `price` (7002-SORD-097) + - if the trigger price is already met on submission, **must** warn the user that the stop order will be triggered immediately (7002-SORD-098) + ... so I can control in more detail how my order is executed ## Auto Populating a deal ticket non-manual methods From 6523490d96719b7f364ced475f408048bfc8a708 Mon Sep 17 00:00:00 2001 From: Maciek Date: Fri, 1 Sep 2023 11:00:20 +0200 Subject: [PATCH 08/16] chore(trading): 4349 delayed telemetry opt in (#4642) --- apps/trading/components/settings/settings.tsx | 4 +- .../components/welcome-dialog/get-started.tsx | 5 +- .../telemetry-approval.spec.tsx | 48 +++++---- .../welcome-dialog/telemetry-approval.tsx | 71 +++++++++++--- .../welcome-dialog/welcome-dialog-content.tsx | 6 +- .../welcome-dialog/welcome-dialog.tsx | 98 ++++++++++++++++--- .../lib/hooks/use-telemetry-approval.spec.ts | 84 +++++++++++++--- .../lib/hooks/use-telemetry-approval.ts | 50 +++++++--- apps/trading/stores/global.ts | 2 - .../src/lib/commands/vega-wallet-connect.ts | 4 + .../vega-icons/svg-icons/icon-eye-off.tsx | 7 ++ .../icon/vega-icons/vega-icon-record.ts | 3 + specs/0007-FUGS-first-use-get-started.md | 6 +- 13 files changed, 308 insertions(+), 80 deletions(-) create mode 100644 libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-eye-off.tsx diff --git a/apps/trading/components/settings/settings.tsx b/apps/trading/components/settings/settings.tsx index 4d7fc94b6..0d5f5eb43 100644 --- a/apps/trading/components/settings/settings.tsx +++ b/apps/trading/components/settings/settings.tsx @@ -24,8 +24,8 @@ export const Settings = () => { > setIsApproved(isOn)} - checked={isApproved} + onCheckedChange={(isOn) => setIsApproved(isOn ? 'true' : 'false')} + checked={isApproved === 'true'} /> diff --git a/apps/trading/components/welcome-dialog/get-started.tsx b/apps/trading/components/welcome-dialog/get-started.tsx index 59c0099ee..9b6480ba5 100644 --- a/apps/trading/components/welcome-dialog/get-started.tsx +++ b/apps/trading/components/welcome-dialog/get-started.tsx @@ -23,6 +23,7 @@ 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; @@ -35,7 +36,7 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => { constants.ONBOARDING_VIEWED_KEY ); - const update = useGlobalStore((store) => store.update); + const dismiss = useOnboardingStore((store) => store.dismiss); const marketId = useGlobalStore((store) => store.marketId); const link = marketId ? Links[Routes.MARKET](marketId) : Links[Routes.HOME](); const openVegaWalletDialog = useVegaWalletDialogStore( @@ -61,7 +62,7 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => { onClickHandle = () => { navigate(link); setView({ type: ViewType.Deposit }); - update({ onBoardingDismissed: true }); + dismiss(); }; } else if (step === OnboardingStep.ONBOARDING_ORDER_STEP) { buttonText = t('Dismiss'); diff --git a/apps/trading/components/welcome-dialog/telemetry-approval.spec.tsx b/apps/trading/components/welcome-dialog/telemetry-approval.spec.tsx index ac66dcaff..37fd35258 100644 --- a/apps/trading/components/welcome-dialog/telemetry-approval.spec.tsx +++ b/apps/trading/components/welcome-dialog/telemetry-approval.spec.tsx @@ -2,29 +2,35 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { TelemetryApproval } from './telemetry-approval'; -jest.mock('@vegaprotocol/logger', () => ({ - SentryInit: () => undefined, - SentryClose: () => undefined, -})); - -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(); - expect(screen.getByRole('checkbox')).toHaveAttribute( - 'data-state', - 'unchecked' + it('click on buttons should be properly handled', async () => { + const mockSetTelemetryValue = jest.fn(); + render( + ); - await userEvent.click(screen.getByRole('checkbox')); - expect(screen.getByRole('checkbox')).toHaveAttribute( - 'data-state', - 'checked' + 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'); + }); + + it('confirm button should have proper text', async () => { + const mockSetTelemetryValue = jest.fn(); + render( + ); - expect(screen.getByText('Share usage data')).toBeInTheDocument(); - expect(screen.getByText(helpText)).toBeInTheDocument(); + expect(screen.getByText('Continue sharing data')).toBeInTheDocument(); + await userEvent.click(screen.getByText('Continue sharing data')); + expect(mockSetTelemetryValue).toHaveBeenCalledWith('true'); }); }); diff --git a/apps/trading/components/welcome-dialog/telemetry-approval.tsx b/apps/trading/components/welcome-dialog/telemetry-approval.tsx index 0fd49c983..5b38c2963 100644 --- a/apps/trading/components/welcome-dialog/telemetry-approval.tsx +++ b/apps/trading/components/welcome-dialog/telemetry-approval.tsx @@ -1,21 +1,66 @@ -import { TradingCheckbox } from '@vegaprotocol/ui-toolkit'; +import { + Intent, + TradingButton, + VegaIcon, + VegaIconNames, +} from '@vegaprotocol/ui-toolkit'; import { t } from '@vegaprotocol/i18n'; -import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval'; -export const TelemetryApproval = ({ helpText }: { helpText: string }) => { - const [isApproved, setIsApproved] = useTelemetryApproval(); +interface Props { + telemetryValue: string; + setTelemetryValue: (value: string) => void; +} + +export const TelemetryApproval = ({ + telemetryValue, + setTelemetryValue, +}: Props) => { return (
- {t('Share usage data')}} - checked={isApproved} - name="telemetry-approval" - onCheckedChange={() => setIsApproved(!isApproved)} - /> -
-
- {helpText} +
+ {t( + 'Help us identify bugs and improve Vega Governance by sharing anonymous usage data.' + )} +
+
+ +
+
{t('Anonymous')}
+
{t('Your identity is always anonymous on Vega')}
+
+
+ +
+ +
+
{t('Optional')}
+
{t('You can opt out any time via settings')}
+
+
+
+ setTelemetryValue('false')} + size="small" + intent={Intent.None} + data-testid="do-not-share-data-button" + fill + > + {t('No thanks')} + + + setTelemetryValue('true')} + intent={Intent.Info} + data-testid="share-data-button" + size="small" + fill + > + {telemetryValue === 'true' + ? t('Continue sharing data') + : t('Share data')} + +
); diff --git a/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx b/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx index 218db85bb..7e9564de4 100644 --- a/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx +++ b/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx @@ -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 { useGlobalStore } from '../../stores'; +import { useOnboardingStore } from './welcome-dialog'; export const WelcomeDialogContent = () => { const { VEGA_ENV } = useEnvironment(); - const update = useGlobalStore((store) => store.update); + const dismiss = useOnboardingStore((store) => store.dismiss); const navigate = useNavigate(); const browseMarkets = () => { const link = Links[Routes.MARKETS](); navigate(link); - update({ onBoardingDismissed: true }); + dismiss(); }; const lead = VEGA_ENV === Networks.MAINNET diff --git a/apps/trading/components/welcome-dialog/welcome-dialog.tsx b/apps/trading/components/welcome-dialog/welcome-dialog.tsx index 7739156ee..a3c42cdf1 100644 --- a/apps/trading/components/welcome-dialog/welcome-dialog.tsx +++ b/apps/trading/components/welcome-dialog/welcome-dialog.tsx @@ -1,7 +1,9 @@ -import React from 'react'; import { useNavigate } from 'react-router-dom'; -import { Dialog, Intent } from '@vegaprotocol/ui-toolkit'; +import type { Toast } from '@vegaprotocol/ui-toolkit'; +import { Dialog, Intent, useToasts } 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'; @@ -12,15 +14,41 @@ 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 [onBoardingViewed] = useLocalStorage(constants.ONBOARDING_VIEWED_KEY); - const update = useGlobalStore((store) => store.update); - const dismissed = useGlobalStore((store) => store.onBoardingDismissed); - const currentStep = useGetOnboardingStep(); - 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 currentStep = useGetOnboardingStep(); + const isTelemetryPopupNeeded = + isTelemetryNeeded && + (onBoardingViewed === 'true' || + currentStep > OnboardingStep.ONBOARDING_ORDER_STEP); + const isOnboardingDialogNeeded = onBoardingViewed !== 'true' && currentStep && @@ -29,12 +57,58 @@ export const WelcomeDialog = () => { const marketId = useGlobalStore((store) => store.marketId); const onClose = () => { - const link = marketId - ? Links[Routes.MARKET](marketId) - : Links[Routes.HOME](); - navigate(link); - update({ onBoardingDismissed: true }); + if (isTelemetryPopupNeeded) { + closeTelemetry(); + } else { + const link = marketId + ? Links[Routes.MARKET](marketId) + : Links[Routes.HOME](); + navigate(link); + dismiss(); + } }; + + 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: ( + <> +

+ {t('Improve vega console')} +

+ + + ), + onClose: onApprovalClose, + }; + if (!hasToast(TELEMETRY_APPROVAL_TOAST_ID)) { + setToast(toast); + } + return; + } + const title = ( {t('Console')}{' '} diff --git a/apps/trading/lib/hooks/use-telemetry-approval.spec.ts b/apps/trading/lib/hooks/use-telemetry-approval.spec.ts index bd0be96b9..eccddbcff 100644 --- a/apps/trading/lib/hooks/use-telemetry-approval.spec.ts +++ b/apps/trading/lib/hooks/use-telemetry-approval.spec.ts @@ -1,19 +1,40 @@ import { renderHook, act, waitFor } from '@testing-library/react'; import { useLocalStorage } from '@vegaprotocol/react-helpers'; import { SentryInit, SentryClose } from '@vegaprotocol/logger'; -import { STORAGE_KEY, useTelemetryApproval } from './use-telemetry-approval'; +import { + STORAGE_KEY, + STORAGE_SECOND_KEY, + useTelemetryApproval, +} from './use-telemetry-approval'; +import { Networks } from '@vegaprotocol/environment'; const mockSetValue = jest.fn(); -const mockRemoveValue = jest.fn(); +let mockStorageHookApprovalResult: [string | null, jest.Mock] = [ + null, + mockSetValue, +]; +const mockSetSecondValue = jest.fn(); +let mockStorageHookViewedResult: [string | null, jest.Mock] = [ + null, + mockSetSecondValue, +]; jest.mock('@vegaprotocol/logger'); jest.mock('@vegaprotocol/react-helpers', () => ({ ...jest.requireActual('@vegaprotocol/react-helpers'), - useLocalStorage: jest - .fn() - .mockImplementation(() => [false, mockSetValue, mockRemoveValue]), + useLocalStorage: jest.fn((key: string) => { + if (key === 'vega_telemetry_approval') { + return mockStorageHookApprovalResult; + } + return mockStorageHookViewedResult; + }), })); +let mockVegaEnv = 'test'; jest.mock('@vegaprotocol/environment', () => ({ - useEnvironment: () => ({ VEGA_ENV: 'test', SENTRY_DSN: 'sentry-dsn' }), + ...jest.requireActual('@vegaprotocol/environment'), + useEnvironment: jest.fn(() => ({ + VEGA_ENV: mockVegaEnv, + SENTRY_DSN: 'sentry-dsn', + })), })); describe('useTelemetryApproval', () => { @@ -21,32 +42,71 @@ describe('useTelemetryApproval', () => { jest.clearAllMocks(); }); - it('hook should return proper array', () => { + it('when empty hook should return proper array', () => { const { result } = renderHook(() => useTelemetryApproval()); - expect(result.current[0]).toEqual(false); + expect(result.current[0]).toEqual(''); 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('1'); + expect(mockSetValue).toHaveBeenCalledWith('true'); + expect(mockSetSecondValue).toHaveBeenCalledWith('true'); }); }); 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(mockRemoveValue).toHaveBeenCalledWith(); + expect(mockSetValue).toHaveBeenCalledWith('false'); + expect(mockSetSecondValue).toHaveBeenCalledWith('true'); }); }); }); diff --git a/apps/trading/lib/hooks/use-telemetry-approval.ts b/apps/trading/lib/hooks/use-telemetry-approval.ts index 25e0203b5..d235c9b35 100644 --- a/apps/trading/lib/hooks/use-telemetry-approval.ts +++ b/apps/trading/lib/hooks/use-telemetry-approval.ts @@ -1,25 +1,51 @@ import { useLocalStorage } from '@vegaprotocol/react-helpers'; -import { useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { SentryInit, SentryClose } from '@vegaprotocol/logger'; -import { useEnvironment } from '@vegaprotocol/environment'; +import { Networks, useEnvironment } from '@vegaprotocol/environment'; + export const STORAGE_KEY = 'vega_telemetry_approval'; +export const STORAGE_SECOND_KEY = 'vega_telemetry_viewed'; export const useTelemetryApproval = (): [ - value: boolean, - setValue: (value: boolean) => void + value: string, + setValue: (value: string) => void, + shouldOpen: boolean, + close: () => void ] => { const { VEGA_ENV, SENTRY_DSN } = useEnvironment(); - const [value, setValue, removeValue] = useLocalStorage(STORAGE_KEY); - const setApprove = useCallback( - (value: boolean) => { - if (value && SENTRY_DSN) { + 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) { SentryInit(SENTRY_DSN, VEGA_ENV); - return setValue('1'); + return setValue('true'); } SentryClose(); - removeValue(); + setValue('false'); }, - [setValue, removeValue, SENTRY_DSN, VEGA_ENV] + [setValue, SENTRY_DSN, VEGA_ENV] ); - return [Boolean(value), setApprove]; + 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]; }; diff --git a/apps/trading/stores/global.ts b/apps/trading/stores/global.ts index cbd56fd09..e83687eae 100644 --- a/apps/trading/stores/global.ts +++ b/apps/trading/stores/global.ts @@ -4,7 +4,6 @@ import produce from 'immer'; interface GlobalStore { marketId: string | null; - onBoardingDismissed: boolean; eagerConnecting: boolean; update: (store: Partial>) => void; } @@ -16,7 +15,6 @@ interface PageTitleStore { export const useGlobalStore = create()((set) => ({ marketId: LocalStorage.getItem('marketId') || null, - onBoardingDismissed: false, eagerConnecting: false, update: (newState) => { set( diff --git a/libs/cypress/src/lib/commands/vega-wallet-connect.ts b/libs/cypress/src/lib/commands/vega-wallet-connect.ts index dba138df2..5ca985bdd 100644 --- a/libs/cypress/src/lib/commands/vega-wallet-connect.ts +++ b/libs/cypress/src/lib/commands/vega-wallet-connect.ts @@ -65,6 +65,8 @@ 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({ @@ -81,6 +83,8 @@ 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'); }); }); } diff --git a/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-eye-off.tsx b/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-eye-off.tsx new file mode 100644 index 000000000..31aae4fc8 --- /dev/null +++ b/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-eye-off.tsx @@ -0,0 +1,7 @@ +export const IconEyeOff = ({ size = 16 }: { size: number }) => { + return ( + + + + ); +}; diff --git a/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon-record.ts b/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon-record.ts index e8069b90f..753984075 100644 --- a/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon-record.ts +++ b/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon-record.ts @@ -15,6 +15,7 @@ import { IconDeposit } from './svg-icons/icon-deposit'; import { IconEdit } from './svg-icons/icon-edit'; import { IconExclaimationMark } from './svg-icons/icon-exclaimation-mark'; import { IconEye } from './svg-icons/icon-eye'; +import { IconEyeOff } from './svg-icons/icon-eye-off'; import { IconForum } from './svg-icons/icon-forum'; import { IconGlobe } from './svg-icons/icon-globe'; import { IconInfo } from './svg-icons/icon-info'; @@ -55,6 +56,7 @@ export enum VegaIconNames { EDIT = 'edit', EXCLAIMATION_MARK = 'exclaimation-mark', EYE = 'eye', + EYE_OFF = 'eye-off', FORUM = 'forum', GLOBE = 'globe', INFO = 'info', @@ -90,6 +92,7 @@ export const VegaIconNameMap: Record< 'chevron-down': IconChevronDown, 'chevron-left': IconChevronLeft, 'chevron-up': IconChevronUp, + 'eye-off': IconEyeOff, 'exclaimation-mark': IconExclaimationMark, 'open-external': IconOpenExternal, 'question-mark': IconQuestionMark, diff --git a/specs/0007-FUGS-first-use-get-started.md b/specs/0007-FUGS-first-use-get-started.md index 10c87a581..1cdfbf62d 100644 --- a/specs/0007-FUGS-first-use-get-started.md +++ b/specs/0007-FUGS-first-use-get-started.md @@ -23,7 +23,7 @@ - **Must** There is a link to try out trading on Fairground when I'm on Mainnet (0007-FUGS-008) - **Must** There is a link to trade with real funds on Mainnet when I am on Fairground (0007-FUGS-010) - **Must** When I am on the Fairground version, I can see a warning / call out that this is Fairground meaning I can try out with virtual assets at no risk (0007-FUGS-011) -- If I dismiss the popup, I **must** not see it unless I NOT accomplish full "onboarding" +- If I dismiss the popup, I **must** not see it anymore (0007-FUGS-018) - If I dismiss the popup, I land on the default market (0007-FUGS-012) ## When the popup has been dismissed: @@ -33,3 +33,7 @@ - **Must** We've replaced "connect wallet" in the top right with "get started" (0007-FUGS-015) - **Must** When I press the get started CTA, I see the wallet connect popup (0007-FUGS-016) - **Must** If I have a wallet installed already I don't see this quick start onboarding, and instead call(s) to action in Console revert to connect wallet, not "get started" (button in nav header) (0007-FUGS-017) + +## When onboarding process has been accomplished: + +- I can see telemetry approval toast: on environment other than mainnet telemetry is enabled by default (0007-FUGS-019) From 247927e9390057ad5cc04311a510727a57c663ca Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 1 Sep 2023 10:55:56 +0100 Subject: [PATCH 09/16] chore(trading): get started specs update (#4681) --- specs/0007-FUGS-first-use-get-started.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/specs/0007-FUGS-first-use-get-started.md b/specs/0007-FUGS-first-use-get-started.md index 1cdfbf62d..374782e50 100644 --- a/specs/0007-FUGS-first-use-get-started.md +++ b/specs/0007-FUGS-first-use-get-started.md @@ -29,11 +29,10 @@ ## When the popup has been dismissed: - **Must** I can see the steps to get started with a visible call to action (according to my progress) in the context of the deal ticket, deposit, withdraw, transfer components in the sidebar (0007-FUGS-013) -- **Must** Remove buttons from pane containers that prompt to connect wallet (0007-FUGS-014) -- **Must** We've replaced "connect wallet" in the top right with "get started" (0007-FUGS-015) -- **Must** When I press the get started CTA, I see the wallet connect popup (0007-FUGS-016) -- **Must** If I have a wallet installed already I don't see this quick start onboarding, and instead call(s) to action in Console revert to connect wallet, not "get started" (button in nav header) (0007-FUGS-017) +- **Must** We've replaced "connect wallet" in the top right with "get started" (0007-FUGS-014) +- **Must** When I press the get started CTA, I see the wallet connect popup (0007-FUGS-015) +- **Must** If I have a wallet installed already I don't see this quick start onboarding, and instead call(s) to action in Console revert to connect wallet, not "get started" (button in nav header) (0007-FUGS-016) ## When onboarding process has been accomplished: -- I can see telemetry approval toast: on environment other than mainnet telemetry is enabled by default (0007-FUGS-019) +- I can see telemetry approval toast: on environment other than mainnet telemetry is enabled by default (0007-FUGS-018) From 85a498170041e1ef25db6a70e4a6c09c284834c9 Mon Sep 17 00:00:00 2001 From: Art Date: Fri, 1 Sep 2023 12:30:01 +0200 Subject: [PATCH 10/16] fix(proposals): protocol upgrade notification block querying (#4665) --- ...tocol-upgrade-in-progress-notification.tsx | 25 +++++++++++---- .../use-block-rising.ts | 32 +++++++++---------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/libs/proposals/src/components/protocol-upgrade-in-progress-notification.tsx b/libs/proposals/src/components/protocol-upgrade-in-progress-notification.tsx index a9227289f..3330abb2c 100644 --- a/libs/proposals/src/components/protocol-upgrade-in-progress-notification.tsx +++ b/libs/proposals/src/components/protocol-upgrade-in-progress-notification.tsx @@ -26,22 +26,36 @@ export const ProtocolUpgradeInProgressNotification = () => { const [nextUpgrade] = useLocalStorageSnapshot( NEXT_PROTOCOL_UPGRADE_PROPOSAL_SNAPSHOT ); - const { blocksRising, block } = useBlockRising(); const detailsLink = useProtocolUpgradeProposalLink(); let vegaReleaseTag: string | undefined; let upgradeBlockHeight: string | undefined; - if (error && !data && nextUpgrade && ALLOW_STORED_PROPOSAL_DATA) { + const hasData = data && !error; + const hasStoredData = nextUpgrade && ALLOW_STORED_PROPOSAL_DATA; + + if (hasData) { + // gets tag and height from the data api + vegaReleaseTag = data.vegaReleaseTag; + upgradeBlockHeight = data.upgradeBlockHeight; + } else if (hasStoredData) { + // gets tag and height from stored value if data api is unavailable try { const stored = JSON.parse(nextUpgrade) as StoredNextProtocolUpgradeData; vegaReleaseTag = stored.vegaReleaseTag; upgradeBlockHeight = stored.upgradeBlockHeight; } catch { - // no op + // NOOP - could not parse stored data } } + const hasUpgradeInfo = vegaReleaseTag && upgradeBlockHeight; + + const { blocksRising, block } = useBlockRising( + // skips querying blocks if there's no upgrade information available + !hasUpgradeInfo + ); + /** * If upgrade is in progress then none of the nodes should produce blocks, * same should be with the tendermint block info otherwise it's a network @@ -50,10 +64,7 @@ export const ProtocolUpgradeInProgressNotification = () => { * Once the networks is back then the notification disappears. */ const upgradeInProgress = - vegaReleaseTag && - upgradeBlockHeight && - !blocksRising && - block <= Number(upgradeBlockHeight); + hasUpgradeInfo && !blocksRising && block <= Number(upgradeBlockHeight); if (!upgradeInProgress) return null; diff --git a/libs/proposals/src/lib/protocol-upgrade-proposals/use-block-rising.ts b/libs/proposals/src/lib/protocol-upgrade-proposals/use-block-rising.ts index f02be60aa..60d7bdc9c 100644 --- a/libs/proposals/src/lib/protocol-upgrade-proposals/use-block-rising.ts +++ b/libs/proposals/src/lib/protocol-upgrade-proposals/use-block-rising.ts @@ -15,31 +15,33 @@ const CHECK_INTERVAL = 5000; // ms */ const ALLOW_STALE = 2; // times -> MAX(this, 1) * CHECK_INTERVAL ~> min check time -export const useBlockRising = () => { +export const useBlockRising = (skip = false) => { const [blocksRising, setBlocksRising] = useState(true); const [block, setBlock] = useState(0); const nodes = useEnvironment((state) => state.nodes); const clients = useMemo(() => { - return nodes.map( - (n) => - n && - n.length > 0 && - createClient({ + return nodes.map((n) => { + if (n && n.length > 0) { + const client = createClient({ url: n, cacheConfig: undefined, retry: false, connectToDevTools: false, connectToHeaderStore: true, - }) - ); + }); + return client; + } + return undefined; + }); }, [nodes]); const { refetch: fetchBlockInfo } = useBlockInfo(); useEffect(() => { + if (skip) return; let stale = 0; let prev = 0; const check = async () => { - const queries = clients.map((client, index) => + const queries = clients.map((client) => client ? client .query({ @@ -47,18 +49,16 @@ export const useBlockRising = () => { fetchPolicy: 'network-only', errorPolicy: 'ignore', }) - .catch((err) => - Promise.reject( - `could not retrieve statistics from ${nodes[index]}` - ) - ) + .catch(() => { + // NOOP - could not retrieve statistics for that node (network error) + }) : undefined ); const blockInfo = await fetchBlockInfo(); const results = (await Promise.allSettled(compact(queries))).map( (res) => { - if (res && res.status === 'fulfilled') { + if (res && res.status === 'fulfilled' && res.value) { return res.value.data.statistics; } else { return undefined; @@ -86,7 +86,7 @@ export const useBlockRising = () => { return () => { clearInterval(interval); }; - }, [clients, fetchBlockInfo, blocksRising, nodes]); + }, [clients, fetchBlockInfo, blocksRising, nodes, skip]); return { blocksRising, block }; }; From 39e5836edb2e7c1b65ea75942d72a190c0aa5a47 Mon Sep 17 00:00:00 2001 From: Art Date: Fri, 1 Sep 2023 13:22:13 +0200 Subject: [PATCH 11/16] fix(wallet): check if snaps are supported (#4671) --- .../src/connect-dialog/connect-dialog.tsx | 57 ++++++++++++------- libs/wallet/src/connectors/snap-connector.ts | 12 ++-- libs/wallet/src/use-is-snap-running.ts | 27 --------- libs/wallet/src/use-snap-status.ts | 37 ++++++++++++ 4 files changed, 76 insertions(+), 57 deletions(-) delete mode 100644 libs/wallet/src/use-is-snap-running.ts create mode 100644 libs/wallet/src/use-snap-status.ts diff --git a/libs/wallet/src/connect-dialog/connect-dialog.tsx b/libs/wallet/src/connect-dialog/connect-dialog.tsx index 7aa5c32e4..e3c0ed4dc 100644 --- a/libs/wallet/src/connect-dialog/connect-dialog.tsx +++ b/libs/wallet/src/connect-dialog/connect-dialog.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames'; import { Dialog, + ExternalLink, Intent, Pill, TradingButton, @@ -39,7 +40,7 @@ import { useVegaWallet } from '../use-vega-wallet'; import { InjectedConnectorForm } from './injected-connector-form'; import { isBrowserWalletInstalled } from '../utils'; import { useIsWalletServiceRunning } from '../use-is-wallet-service-running'; -import { useIsSnapRunning } from '../use-is-snap-running'; +import { SnapStatus, useSnapStatus } from '../use-snap-status'; import { useVegaWalletDialogStore } from './vega-wallet-dialog-store'; export const CLOSE_DELAY = 1700; @@ -158,7 +159,7 @@ const ConnectDialogContainer = ({ appChainId ); - const isSnapRunning = useIsSnapRunning( + const snapStatus = useSnapStatus( DEFAULT_SNAP_ID, Boolean(connectors['snap']) ); @@ -183,7 +184,7 @@ const ConnectDialogContainer = ({ setWalletUrl={setWalletUrl} onSelect={handleSelect} isDesktopWalletRunning={isDesktopWalletRunning} - isSnapRunning={isSnapRunning} + snapStatus={snapStatus} /> )} @@ -198,14 +199,14 @@ const ConnectorList = ({ walletUrl, setWalletUrl, isDesktopWalletRunning, - isSnapRunning, + snapStatus, }: { connectors: Connectors; onSelect: (type: WalletType) => void; walletUrl: string; setWalletUrl: (value: string) => void; isDesktopWalletRunning: boolean | null; - isSnapRunning: boolean | null; + snapStatus: SnapStatus; }) => { const { pubKey, links } = useVegaWallet(); const title = isBrowserWalletInstalled() @@ -249,7 +250,7 @@ const ConnectorList = ({
{connectors['snap'] !== undefined ? (
- {isSnapRunning ? ( + {snapStatus === SnapStatus.INSTALLED ? ( ) : ( - -
- {t('Install Vega MetaMask Snap')} -
-
- -
- - } - onClick={() => { - requestSnap(DEFAULT_SNAP_ID); - }} - /> + <> + +
+ {t('Install Vega MetaMask Snap')} +
+
+ +
+ + } + onClick={() => { + requestSnap(DEFAULT_SNAP_ID); + }} + /> + {snapStatus === SnapStatus.NOT_SUPPORTED ? ( +

+ {t('No MetaMask version that supports snaps detected.')}{' '} + {t('Learn more about')}{' '} + + MetaMask Snaps + +

+ ) : null} + )}
) : null} diff --git a/libs/wallet/src/connectors/snap-connector.ts b/libs/wallet/src/connectors/snap-connector.ts index ede6fa70e..861967a09 100644 --- a/libs/wallet/src/connectors/snap-connector.ts +++ b/libs/wallet/src/connectors/snap-connector.ts @@ -118,14 +118,10 @@ export const getSnap = async ( snapId: string, version?: string ): Promise => { - try { - const snaps = await getSnaps(); - return Object.values(snaps).find( - (snap) => snap.id === snapId && (!version || snap.version === version) - ); - } catch (e) { - return undefined; - } + const snaps = await getSnaps(); + return Object.values(snaps).find( + (snap) => snap.id === snapId && (!version || snap.version === version) + ); }; export const invokeSnap = async ( diff --git a/libs/wallet/src/use-is-snap-running.ts b/libs/wallet/src/use-is-snap-running.ts deleted file mode 100644 index ec44e2a36..000000000 --- a/libs/wallet/src/use-is-snap-running.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useState } from 'react'; -import { getSnap } from './connectors'; - -const INTERVAL = 2_000; - -export const useIsSnapRunning = (snapId: string, shouldCheck: boolean) => { - const [running, setRunning] = useState(false); - useEffect(() => { - if (!shouldCheck) return; - - const checkState = async () => { - const snap = await getSnap(snapId); - setRunning(!!snap); - }; - - const i = setInterval(() => { - checkState(); - }, INTERVAL); - - checkState(); - - return () => { - clearInterval(i); - }; - }, [snapId, shouldCheck]); - return running; -}; diff --git a/libs/wallet/src/use-snap-status.ts b/libs/wallet/src/use-snap-status.ts new file mode 100644 index 000000000..87c267aa9 --- /dev/null +++ b/libs/wallet/src/use-snap-status.ts @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; +import { getSnap } from './connectors'; + +const INTERVAL = 2_000; + +export enum SnapStatus { + NOT_SUPPORTED, + INSTALLED, + NOT_INSTALLED, +} + +export const useSnapStatus = (snapId: string, shouldCheck: boolean) => { + const [status, setStatus] = useState(SnapStatus.NOT_INSTALLED); + useEffect(() => { + if (!shouldCheck) return; + + const checkState = async () => { + try { + const snap = await getSnap(snapId); + setStatus(snap ? SnapStatus.INSTALLED : SnapStatus.NOT_INSTALLED); + } catch (err) { + setStatus(SnapStatus.NOT_SUPPORTED); + } + }; + + const i = setInterval(() => { + checkState(); + }, INTERVAL); + + checkState(); + + return () => { + clearInterval(i); + }; + }, [snapId, shouldCheck]); + return status; +}; From 92090743326fdfe63b10dff83cfd7958b7b4b5ac Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Fri, 1 Sep 2023 17:00:11 -0700 Subject: [PATCH 12/16] chore(trading): change pane context buttons color (#4680) --- .../accounts-menu/accounts-menu.tsx | 4 +- .../deposits-menu/deposits-menu.tsx | 3 +- .../positions-menu/positions-menu.tsx | 3 +- .../withdrawals-menu/withdrawals-menu.tsx | 3 +- .../src/lib/candles-menu.spec.tsx | 4 +- libs/candles-chart/src/lib/candles-menu.tsx | 112 ++++++++++-------- .../open-orders-menu/open-orders-menu.tsx | 9 +- 7 files changed, 69 insertions(+), 69 deletions(-) diff --git a/apps/trading/components/accounts-menu/accounts-menu.tsx b/apps/trading/components/accounts-menu/accounts-menu.tsx index 8fe5ae993..86c80e91c 100644 --- a/apps/trading/components/accounts-menu/accounts-menu.tsx +++ b/apps/trading/components/accounts-menu/accounts-menu.tsx @@ -1,5 +1,5 @@ import { t } from '@vegaprotocol/i18n'; -import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit'; +import { TradingButton } from '@vegaprotocol/ui-toolkit'; import { ViewType, useSidebar } from '../sidebar'; export const AccountsMenu = () => { @@ -8,7 +8,6 @@ export const AccountsMenu = () => { return ( <> setView({ type: ViewType.Transfer })} @@ -16,7 +15,6 @@ export const AccountsMenu = () => { {t('Transfer')} setView({ type: ViewType.Deposit })} > diff --git a/apps/trading/components/deposits-menu/deposits-menu.tsx b/apps/trading/components/deposits-menu/deposits-menu.tsx index 28c751e84..81b6edf60 100644 --- a/apps/trading/components/deposits-menu/deposits-menu.tsx +++ b/apps/trading/components/deposits-menu/deposits-menu.tsx @@ -1,5 +1,5 @@ import { t } from '@vegaprotocol/i18n'; -import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit'; +import { TradingButton } from '@vegaprotocol/ui-toolkit'; import { ViewType, useSidebar } from '../sidebar'; export const DepositsMenu = () => { @@ -7,7 +7,6 @@ export const DepositsMenu = () => { return ( setView({ type: ViewType.Deposit })} data-testid="deposit-button" diff --git a/apps/trading/components/positions-menu/positions-menu.tsx b/apps/trading/components/positions-menu/positions-menu.tsx index 58dddee7b..428bb82d7 100644 --- a/apps/trading/components/positions-menu/positions-menu.tsx +++ b/apps/trading/components/positions-menu/positions-menu.tsx @@ -1,5 +1,5 @@ import { t } from '@vegaprotocol/i18n'; -import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit'; +import { TradingButton } from '@vegaprotocol/ui-toolkit'; import { usePositionsStore } from '../positions-container'; export const PositionsMenu = () => { @@ -7,7 +7,6 @@ export const PositionsMenu = () => { const toggle = usePositionsStore((store) => store.toggleClosedMarkets); return ( { @@ -7,7 +7,6 @@ export const WithdrawalsMenu = () => { return ( setView({ type: ViewType.Withdraw })} data-testid="withdraw-dialog-button" diff --git a/libs/candles-chart/src/lib/candles-menu.spec.tsx b/libs/candles-chart/src/lib/candles-menu.spec.tsx index 7a9ed6f52..85f8b5b24 100644 --- a/libs/candles-chart/src/lib/candles-menu.spec.tsx +++ b/libs/candles-chart/src/lib/candles-menu.spec.tsx @@ -7,8 +7,8 @@ describe('CandlesMenu', () => { render(); await userEvent.click( - screen.getByText('Studies', { - selector: '[type="button"]', + screen.getByRole('button', { + name: 'Studies', }) ); expect(await screen.findByRole('menu')).toBeInTheDocument(); diff --git a/libs/candles-chart/src/lib/candles-menu.tsx b/libs/candles-chart/src/lib/candles-menu.tsx index 2b3209e0a..cc5b62e40 100644 --- a/libs/candles-chart/src/lib/candles-menu.tsx +++ b/libs/candles-chart/src/lib/candles-menu.tsx @@ -10,13 +10,14 @@ import { studyLabels, } from 'pennant'; import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuItemIndicator, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuTrigger, + TradingButton, + TradingDropdown, + TradingDropdownCheckboxItem, + TradingDropdownContent, + TradingDropdownItemIndicator, + TradingDropdownRadioGroup, + TradingDropdownRadioItem, + TradingDropdownTrigger, Icon, } from '@vegaprotocol/ui-toolkit'; import type { IconName } from '@blueprintjs/icons'; @@ -44,69 +45,76 @@ export const CandlesMenu = () => { } = useCandlesChartSettings(); const triggerClasses = 'text-xs'; const contentAlign = 'end'; + const triggerButtonProps = { size: 'extra-small' } as const; return ( <> - - {t(`Interval: ${intervalLabels[interval]}`)} - + + + {t(`Interval: ${intervalLabels[interval]}`)} + + } > - - + { setInterval(value as Interval); }} > {Object.values(Interval).map((timeInterval) => ( - {intervalLabels[timeInterval]} - - + + ))} - - - - + + + - - + + + + + } > - - + { setType(value as ChartType); }} > {Object.values(ChartType).map((type) => ( - + {chartTypeLabels[type]} - - + + ))} - - - - + + + - {t('Overlays')} - + + + {t('Overlays')} + + } > - + {Object.values(Overlay).map((overlay) => ( - { @@ -121,21 +129,23 @@ export const CandlesMenu = () => { }} > {overlayLabels[overlay]} - - + + ))} - - - + + - {t('Studies')} - + + + {t('Studies')} + + } > - + {Object.values(Study).map((study) => ( - { @@ -150,11 +160,11 @@ export const CandlesMenu = () => { }} > {studyLabels[study]} - - + + ))} - - + + ); }; diff --git a/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx b/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx index 4ada22fb6..1f3879aa1 100644 --- a/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx +++ b/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx @@ -1,5 +1,5 @@ import { t } from '@vegaprotocol/i18n'; -import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit'; +import { TradingButton } from '@vegaprotocol/ui-toolkit'; import { useVegaTransactionStore, useVegaWallet } from '@vegaprotocol/wallet'; import { useHasAmendableOrder } from '../../order-hooks'; @@ -28,12 +28,7 @@ export const OpenOrdersMenu = ({ marketId }: { marketId: string }) => { }; const CancelAllOrdersButton = ({ onClick }: { onClick: () => void }) => ( - + {t('Cancel all')} ); From e41aff88b16a110d4b365129dd68976751b53e96 Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Sun, 3 Sep 2023 11:15:41 -0700 Subject: [PATCH 13/16] chore(trading): make top traded default sort for market selector (#4637) --- .../market-selector/market-selector.spec.tsx | 4 +- .../market-selector/market-selector.tsx | 13 ++-- .../market-selector/sort-dropdown.tsx | 46 ++++++------- .../use-market-selector-list.spec.tsx | 68 ++++++++++++------- .../use-market-selector-list.ts | 22 +----- 5 files changed, 70 insertions(+), 83 deletions(-) diff --git a/apps/trading/components/market-selector/market-selector.spec.tsx b/apps/trading/components/market-selector/market-selector.spec.tsx index faf20a8ff..f4cfadda6 100644 --- a/apps/trading/components/market-selector/market-selector.spec.tsx +++ b/apps/trading/components/market-selector/market-selector.spec.tsx @@ -262,9 +262,7 @@ 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) - .filter(([key]) => key !== Sort.None) - .map(([key]) => SortTypeMapping[key as SortType]) + Object.entries(Sort).map(([key]) => SortTypeMapping[key as SortType]) ); await userEvent.click(screen.getByTestId('sort-item-Gained')); expect( diff --git a/apps/trading/components/market-selector/market-selector.tsx b/apps/trading/components/market-selector/market-selector.tsx index 7750ea33d..673eb43e1 100644 --- a/apps/trading/components/market-selector/market-selector.tsx +++ b/apps/trading/components/market-selector/market-selector.tsx @@ -40,7 +40,7 @@ export const MarketSelector = ({ const [filter, setFilter] = useState({ searchTerm: '', product: Product.All, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); const allProducts = filter.product === Product.All; @@ -53,7 +53,7 @@ export const MarketSelector = ({ return (
-
+
{ @@ -106,9 +106,6 @@ export const MarketSelector = ({ currentSort={filter.sort} onSelect={(sort) => { setFilter((curr) => { - if (curr.sort === sort) { - return { ...curr, sort: Sort.None }; - } return { ...curr, sort, @@ -294,9 +291,9 @@ const List = ({ const Skeleton = () => { return ( -
-
-
+
+
+
diff --git a/apps/trading/components/market-selector/sort-dropdown.tsx b/apps/trading/components/market-selector/sort-dropdown.tsx index 82e6382ea..285d6d566 100644 --- a/apps/trading/components/market-selector/sort-dropdown.tsx +++ b/apps/trading/components/market-selector/sort-dropdown.tsx @@ -1,4 +1,3 @@ -import { t } from '@vegaprotocol/i18n'; import { DropdownMenu, DropdownMenuContent, @@ -11,7 +10,6 @@ import { } from '@vegaprotocol/ui-toolkit'; export const Sort = { - None: 'None', Gained: 'Gained', Lost: 'Lost', New: 'New', @@ -23,17 +21,15 @@ export type SortType = keyof typeof Sort; export const SortTypeMapping: { [key in SortType]: string; } = { - [Sort.None]: 'None', + [Sort.TopTraded]: 'Top traded', [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, @@ -51,10 +47,8 @@ export const SortDropdown = ({ - - {currentSort === SortTypeMapping.None - ? t('Sort') - : SortTypeMapping[currentSort]}{' '} + + {SortTypeMapping[currentSort]} @@ -65,24 +59,22 @@ export const SortDropdown = ({ value={currentSort} onValueChange={(value) => onSelect(value as SortType)} > - {Object.keys(Sort) - .filter((s) => s !== Sort.None) - .map((key) => { - return ( - - - {' '} - {SortTypeMapping[key as SortType]} - - - - ); - })} + {Object.keys(Sort).map((key) => { + return ( + + + {' '} + {SortTypeMapping[key as SortType]} + + + + ); + })} diff --git a/apps/trading/components/market-selector/use-market-selector-list.spec.tsx b/apps/trading/components/market-selector/use-market-selector-list.spec.tsx index 0741aa889..5297dafc4 100644 --- a/apps/trading/components/market-selector/use-market-selector-list.spec.tsx +++ b/apps/trading/components/market-selector/use-market-selector-list.spec.tsx @@ -12,7 +12,10 @@ import { useMarketList } from '@vegaprotocol/markets'; import type { Filter } from '../../components/market-selector'; import { subDays } from 'date-fns'; -jest.mock('@vegaprotocol/markets'); +jest.mock('@vegaprotocol/markets', () => ({ + ...jest.requireActual('@vegaprotocol/markets'), + useMarketList: jest.fn(), +})); const mockUseMarketList = useMarketList as jest.Mock; describe('useMarketSelectorList', () => { @@ -20,7 +23,7 @@ describe('useMarketSelectorList', () => { const defaultArgs: Filter = { searchTerm: '', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }; return renderHook((args) => useMarketSelectorList(args), { @@ -109,21 +112,21 @@ describe('useMarketSelectorList', () => { rerender({ searchTerm: '', product: Product.Spot as 'Future', - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([markets[1]]); rerender({ searchTerm: '', product: Product.Perpetual as 'Future', - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([markets[2]]); rerender({ searchTerm: '', product: Product.All, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual(markets); @@ -189,7 +192,7 @@ describe('useMarketSelectorList', () => { const { result, rerender } = setup({ searchTerm: '', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: ['asset-0'], }); expect(result.current.markets).toEqual([markets[0], markets[1]]); @@ -197,7 +200,7 @@ describe('useMarketSelectorList', () => { rerender({ searchTerm: '', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: ['asset-0', 'asset-1'], }); @@ -210,7 +213,7 @@ describe('useMarketSelectorList', () => { rerender({ searchTerm: '', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: ['asset-0', 'asset-1', 'asset-2'], }); @@ -220,7 +223,7 @@ describe('useMarketSelectorList', () => { rerender({ searchTerm: '', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: ['asset-invalid'], }); @@ -275,28 +278,28 @@ describe('useMarketSelectorList', () => { const { result, rerender } = setup({ searchTerm: 'abc', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([markets[0]]); rerender({ searchTerm: 'def', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([markets[1], markets[2]]); rerender({ searchTerm: 'defg', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([markets[2]]); rerender({ searchTerm: 'zzz', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([]); @@ -305,14 +308,14 @@ describe('useMarketSelectorList', () => { rerender({ searchTerm: 'aaa', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([markets[0]]); rerender({ searchTerm: 'ggg', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([ @@ -322,11 +325,15 @@ describe('useMarketSelectorList', () => { ]); }); - it('sorts by state and volume by default', () => { + it('sorts by top traded by default', () => { const markets = [ createMarketFragment({ id: 'market-0', - state: MarketState.STATE_PENDING, + state: MarketState.STATE_ACTIVE, + // @ts-ignore data not on fragment + data: { + markPrice: '1', + }, // @ts-ignore candles not on fragment candles: [ { @@ -337,30 +344,42 @@ 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', + volume: '100', }, ], }), 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: '100', + 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: '100', + volume: '400', }, ], }), @@ -375,14 +394,15 @@ describe('useMarketSelectorList', () => { const { result } = setup({ searchTerm: '', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); + expect(result.current.markets).toEqual([ - markets[1], + markets[3], markets[2], markets[0], - markets[3], + markets[1], ]); }); diff --git a/apps/trading/components/market-selector/use-market-selector-list.ts b/apps/trading/components/market-selector/use-market-selector-list.ts index 72bef8f54..47f68e4c8 100644 --- a/apps/trading/components/market-selector/use-market-selector-list.ts +++ b/apps/trading/components/market-selector/use-market-selector-list.ts @@ -1,11 +1,7 @@ import { useMemo } from 'react'; import orderBy from 'lodash/orderBy'; import { MarketState } from '@vegaprotocol/types'; -import { - calcCandleVolume, - calcTradedFactor, - useMarketList, -} from '@vegaprotocol/markets'; +import { calcTradedFactor, useMarketList } from '@vegaprotocol/markets'; import { priceChangePercentage } from '@vegaprotocol/utils'; import type { Filter } from '../../components/market-selector/market-selector'; import { Sort } from './sort-dropdown'; @@ -60,22 +56,6 @@ 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( From 9e5bc9c8d1cc335e795987579182be524085aae0 Mon Sep 17 00:00:00 2001 From: Maciek Date: Mon, 4 Sep 2023 09:46:02 +0200 Subject: [PATCH 14/16] chore(trading): handle negative decimals (#4659) --- .../deal-ticket/deal-ticket.spec.tsx | 33 +++++++++++++++++-- libs/fills/src/lib/fills-table.spec.tsx | 22 ++++++++++++- .../components/order-list/order-list.spec.tsx | 20 ++++++++++- .../src/lib/positions-table.spec.tsx | 13 ++++++++ libs/utils/src/lib/format/number.spec.ts | 28 ++++++++++++++++ libs/utils/src/lib/format/number.ts | 8 +++-- .../utils/src/lib/validate/validate-amount.ts | 6 ++++ 7 files changed, 122 insertions(+), 8 deletions(-) diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx index 5008c2b59..c672e93bc 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx @@ -21,6 +21,8 @@ import { 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', () => ({ @@ -36,12 +38,19 @@ const market = generateMarket(); const marketData = generateMarketData(); const submit = jest.fn(); -function generateJsx(mocks: MockedResponse[] = []) { +function generateJsx( + mocks: MockedResponse[] = [], + marketOverrides: PartialDeep = {} +) { + const joinedMarket: Market = { + ...market, + ...marketOverrides, + } as Market; return ( { 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()); @@ -533,6 +541,25 @@ describe('DealTicket', () => { 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'; diff --git a/libs/fills/src/lib/fills-table.spec.tsx b/libs/fills/src/lib/fills-table.spec.tsx index b65fe4dea..f68e124ff 100644 --- a/libs/fills/src/lib/fills-table.spec.tsx +++ b/libs/fills/src/lib/fills-table.spec.tsx @@ -4,7 +4,6 @@ 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'; @@ -215,6 +214,27 @@ 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( + + ); + }); + + 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 = { diff --git a/libs/orders/src/lib/components/order-list/order-list.spec.tsx b/libs/orders/src/lib/components/order-list/order-list.spec.tsx index 8dde7f8a9..5cfa49863 100644 --- a/libs/orders/src/lib/components/order-list/order-list.spec.tsx +++ b/libs/orders/src/lib/components/order-list/order-list.spec.tsx @@ -6,7 +6,7 @@ import type { PartialDeep } from 'type-fest'; import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet'; import { MockedProvider } from '@apollo/client/testing'; -import type { OrderFieldsFragment, OrderListTableProps } from '../'; +import type { Order, OrderFieldsFragment, OrderListTableProps } from '../'; import { OrderListTable } from '../'; import { generateOrder, @@ -164,6 +164,24 @@ describe('OrderListTable', () => { ); }); + it('negative positionDecimalPoints should be properly rendered in size column', async () => { + const localMarketOrder = { + ...marketOrder, + size: '3000', + market: { + ...marketOrder.market, + positionDecimalPlaces: -4, + }, + } as Order; + + await act(async () => { + render(generateJsx({ rowData: [localMarketOrder] })); + }); + + const cells = screen.getAllByRole('gridcell'); + expect(cells[2]).toHaveTextContent('+30,000,000'); + }); + describe('amend cell', () => { it('allows cancelling and editing for permitted orders', async () => { const mockEdit = jest.fn(); diff --git a/libs/positions/src/lib/positions-table.spec.tsx b/libs/positions/src/lib/positions-table.spec.tsx index 79fc39b6d..7dc85c5d1 100644 --- a/libs/positions/src/lib/positions-table.spec.tsx +++ b/libs/positions/src/lib/positions-table.spec.tsx @@ -201,6 +201,19 @@ describe('Positions', () => { ).not.toBeInTheDocument(); }); + it('handle negative positionDecimalPlaces', async () => { + await renderComponent({ + ...singleRow, + openVolume: '-2000', + positionDecimalPlaces: -4, + }); + const cells = screen.getAllByRole('gridcell'); + const cell = cells[1]; + expect(within(cell).getByTestId('stack-cell-primary')).toHaveTextContent( + '-20,000,000' + ); + }); + describe('PNLCell', () => { const props = { data: undefined, diff --git a/libs/utils/src/lib/format/number.spec.ts b/libs/utils/src/lib/format/number.spec.ts index d36dfb719..119087fca 100644 --- a/libs/utils/src/lib/format/number.spec.ts +++ b/libs/utils/src/lib/format/number.spec.ts @@ -7,6 +7,8 @@ import { formatNumberPercentage, getUnlimitedThreshold, isNumeric, + removeDecimal, + toBigNum, quantumDecimalPlaces, toDecimal, toNumberParts, @@ -153,10 +155,36 @@ describe('number utils', () => { { v: 7, o: '0.0000001' }, { v: 8, o: '0.00000001' }, { v: 9, o: '0.000000001' }, + { v: -1, o: '10' }, + { v: -2, o: '100' }, + { v: -3, o: '1000' }, ])('formats with toNumber given number correctly', ({ v, o }) => { expect(toDecimal(v)).toStrictEqual(o); }); }); + + describe('positive and negative decimals should be handled correctly', () => { + const baseNum = '2000'; + const methods = [removeDecimal, toBigNum]; + it.each([ + { decimals: 0, result: ['2000', '2000'] }, + { decimals: 1, result: ['20000', '200'] }, + { decimals: -1, result: ['200', '20000'] }, + { decimals: 2, result: ['200000', '20'] }, + { decimals: -2, result: ['20', '200000'] }, + { decimals: 3, result: ['2000000', '2'] }, + { decimals: -3, result: ['2', '2000000'] }, + { decimals: 4, result: ['20000000', '0.2'] }, + { decimals: -4, result: ['0', '20000000'] }, // removeDecimal has toFixed(0) at the end + ])( + 'number methods should handle negative decimals', + ({ decimals, result }) => { + methods.forEach((method, i) => { + expect(method(baseNum, decimals).toString()).toEqual(result[i]); + }); + } + ); + }); }); describe('quantumDecimalPlaces', () => { diff --git a/libs/utils/src/lib/format/number.ts b/libs/utils/src/lib/format/number.ts index e0d035a50..bf5a93ff4 100644 --- a/libs/utils/src/lib/format/number.ts +++ b/libs/utils/src/lib/format/number.ts @@ -21,7 +21,7 @@ const MAX_FRACTION_DIGITS = 20; export function toDecimal(numberOfDecimals: number) { return new BigNumber(1) - .dividedBy(Math.pow(10, numberOfDecimals)) + .dividedBy(new BigNumber(10).exponentiatedBy(numberOfDecimals)) .toString(10); } @@ -29,7 +29,8 @@ export function toBigNum( rawValue: string | number, decimals: number ): BigNumber { - return new BigNumber(rawValue || 0).dividedBy(Math.pow(10, decimals)); + const divides = new BigNumber(10).exponentiatedBy(decimals); + return new BigNumber(rawValue || 0).dividedBy(divides); } export function addDecimal( @@ -48,7 +49,8 @@ export function removeDecimal( value: string | BigNumber, decimals: number ): string { - return new BigNumber(value || 0).times(Math.pow(10, decimals)).toFixed(0); + const times = new BigNumber(10).exponentiatedBy(decimals); + return new BigNumber(value || 0).times(times).toFixed(0); } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat diff --git a/libs/utils/src/lib/validate/validate-amount.ts b/libs/utils/src/lib/validate/validate-amount.ts index ef45d3bba..95549ab1f 100644 --- a/libs/utils/src/lib/validate/validate-amount.ts +++ b/libs/utils/src/lib/validate/validate-amount.ts @@ -4,6 +4,12 @@ export const validateAmount = (step: number | string, field: string) => { const [, stepDecimals = ''] = String(step).split('.'); return (value?: string) => { + if (Number(step) > 1) { + if (Number(value) % Number(step) > 0) { + return t(`${field} must be a multiple of ${step} for this market`); + } + return true; + } const [, valueDecimals = ''] = (value || '').split('.'); if (stepDecimals.length < valueDecimals.length) { if (stepDecimals === '') { From 4afd4694049c2710b3c882b631b6862870d91243 Mon Sep 17 00:00:00 2001 From: Edd Date: Mon, 4 Sep 2023 10:36:13 +0100 Subject: [PATCH 15/16] chore(explorer,trading,governance): reconfigure hooks (#4655) --- .husky/commit-msg | 3 --- .husky/pre-commit | 5 +++++ .husky/pre-push | 8 ++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100755 .husky/pre-commit create mode 100755 .husky/pre-push diff --git a/.husky/commit-msg b/.husky/commit-msg index b0dca9dee..047fa8eb6 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -3,6 +3,3 @@ # Lint commit messages to ensure they follow conventional commit standards yarn commitlint --edit "${1}" - -# Lint all staged files -yarn lint-staged diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..c18cf4092 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +# Lint all staged files +yarn lint-staged diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 000000000..30b469329 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,8 @@ +#!/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 From 66f25603d3107211d5cde6a49bff59d7225c71e2 Mon Sep 17 00:00:00 2001 From: Joe Tsang <30622993+jtsang586@users.noreply.github.com> Date: Mon, 4 Sep 2023 10:41:04 +0100 Subject: [PATCH 16/16] test(explorer): add e2e test for explorer oracles (#4657) --- .../src/integration/oracles.cy.js | 68 +++++++++++++++++++ .../src/app/routes/oracles/home/index.tsx | 7 +- 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 apps/explorer-e2e/src/integration/oracles.cy.js diff --git a/apps/explorer-e2e/src/integration/oracles.cy.js b/apps/explorer-e2e/src/integration/oracles.cy.js new file mode 100644 index 000000000..06c4466af --- /dev/null +++ b/apps/explorer-e2e/src/integration/oracles.cy.js @@ -0,0 +1,68 @@ +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'); + }); + }); + }); + }); +}); diff --git a/apps/explorer/src/app/routes/oracles/home/index.tsx b/apps/explorer/src/app/routes/oracles/home/index.tsx index 0ee9abc0b..d088a0a47 100644 --- a/apps/explorer/src/app/routes/oracles/home/index.tsx +++ b/apps/explorer/src/app/routes/oracles/home/index.tsx @@ -38,7 +38,12 @@ const Oracles = () => { const dataConnection = o?.node.dataConnection; return ( -
+