From 011e4e4cec649a155559219af5f87b38c6d6b0ac Mon Sep 17 00:00:00 2001 From: "m.ray" <16125548+MadalinaRaicu@users.noreply.github.com> Date: Tue, 6 Jun 2023 05:00:29 +0300 Subject: [PATCH 01/49] fix(trading): market info refactor (#3985) Co-authored-by: Matthew Russell --- .../app/components/markets/market-details.tsx | 181 +++------- .../src/integration/market-info.cy.ts | 7 +- .../market-info/info-key-value-table.tsx | 2 +- .../market-info/market-info-accordion.tsx | 337 +++++++++--------- .../market-info/market-info-panels.tsx | 223 ++++-------- .../oracle-basic-profile.tsx | 34 +- .../components/accordion/accordion.spec.tsx | 26 +- .../accordion/accordion.stories.tsx | 31 +- .../src/components/accordion/accordion.tsx | 53 +-- libs/ui-toolkit/src/components/link/link.tsx | 4 +- .../src/components/tooltip/tooltip.tsx | 4 +- 11 files changed, 381 insertions(+), 521 deletions(-) diff --git a/apps/explorer/src/app/components/markets/market-details.tsx b/apps/explorer/src/app/components/markets/market-details.tsx index 29a9b7722..48409bb60 100644 --- a/apps/explorer/src/app/components/markets/market-details.tsx +++ b/apps/explorer/src/app/components/markets/market-details.tsx @@ -1,5 +1,6 @@ import { t } from '@vegaprotocol/i18n'; import type { MarketInfoWithData } from '@vegaprotocol/markets'; +import { PriceMonitoringBoundsInfoPanel } from '@vegaprotocol/markets'; import { LiquidityInfoPanel, LiquidityMonitoringParametersInfoPanel, @@ -39,133 +40,69 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => { return []; }; - const oraclePanels = isEqual( + const showTwoOracles = isEqual( getSigners(settlementData), getSigners(terminationData) - ) - ? [ - { - title: t('Settlement Oracle'), - content: ( - - ), - }, - { - title: t('Termination Oracle'), - content: ( - - ), - }, - ] - : [ - { - title: t('Oracle'), - content: ( - - ), - }, - ]; + ); - const panels = [ - { - title: t('Key details'), - content: , - }, - { - title: t('Instrument'), - content: , - }, - { - title: t('Settlement asset'), - content: , - }, - { - title: t('Metadata'), - content: , - }, - { - title: t('Risk model'), - content: , - }, - { - title: t('Risk parameters'), - content: , - }, - { - title: t('Risk factors'), - content: , - }, - ...(market.priceMonitoringSettings?.parameters?.triggers || []).map( - (trigger, i) => ({ - title: t(`Price monitoring trigger ${i + 1}`), - content: , - }) - ), - ...(market.data?.priceMonitoringBounds || []).map((trigger, i) => ({ - title: t(`Price monitoring bound ${i + 1}`), - content: ( - <> - - - - ), - })), - { - title: t('Liquidity monitoring parameters'), - content: ( - - ), - }, - { - title: t('Liquidity'), - content: , - }, - { - title: t('Liquidity price range'), - content: ( - - ), - }, - ...oraclePanels, - ]; + const headerClassName = 'font-alpha calt text-xl mt-4 border-b-2 pb-2'; return ( - <> - {panels.map((p) => ( -
-

{p.title}

- {p.content} -
+
+

{t('Key details')}

+ +

{t('Instrument')}

+ +

{t('Settlement asset')}

+ +

{t('Metadata')}

+ +

{t('Risk model')}

+ +

{t('Risk parameters')}

+ +

{t('Risk factors')}

+ + {(market.data?.priceMonitoringBounds || []).map((trigger, i) => ( + <> +

+ {t('Price monitoring bounds %s', [(i + 1).toString()])} +

+ + ))} - + {(market.priceMonitoringSettings?.parameters?.triggers || []).map( + (trigger, i) => ( + <> +

+ {t('Price monitoring settings %s', [(i + 1).toString()])} +

+ + + ) + )} +

{t('Liquidity monitoring')}

+ +

{t('Liquidity')}

+ +

{t('Liquidity price range')}

+ + {showTwoOracles ? ( + <> +

{t('Settlement oracle')}

+ +

{t('Termination oracle')}

+ + + ) : ( + <> +

{t('Oracle')}

+ + + )} +
); }; diff --git a/apps/trading-e2e/src/integration/market-info.cy.ts b/apps/trading-e2e/src/integration/market-info.cy.ts index 6e1c0fe9b..849e557cc 100644 --- a/apps/trading-e2e/src/integration/market-info.cy.ts +++ b/apps/trading-e2e/src/integration/market-info.cy.ts @@ -55,12 +55,9 @@ describe('market info is displayed', { tags: '@smoke' }, () => { validateMarketDataRow(3, 'Quote Unit', 'BTC'); }); - // TODO: fix this test - // New volume check logic, added by https://github.com/vegaprotocol/frontend-monorepo/pull/3870 has caused the - // 24hr volume assertion to fail as it now reads 'Unknown' - it.skip('market volume displayed', () => { + it('market volume displayed', () => { cy.getByTestId(marketTitle).contains('Market volume').click(); - validateMarketDataRow(0, '24 Hour Volume', '1'); + validateMarketDataRow(0, '24 Hour Volume', 'Unknown'); validateMarketDataRow(1, 'Open Interest', '-'); validateMarketDataRow(2, 'Best Bid Volume', '1'); validateMarketDataRow(3, 'Best Offer Volume', '3'); diff --git a/libs/markets/src/lib/components/market-info/info-key-value-table.tsx b/libs/markets/src/lib/components/market-info/info-key-value-table.tsx index e2c38818f..bbbc04795 100644 --- a/libs/markets/src/lib/components/market-info/info-key-value-table.tsx +++ b/libs/markets/src/lib/components/market-info/info-key-value-table.tsx @@ -34,7 +34,7 @@ const Row = ({ assetSymbol = '', noBorder = true, }: RowProps) => { - const className = 'text-black dark:text-white text-sm !px-0'; + const className = 'text-sm'; const getFormattedValue = (value: ReactNode) => { if (typeof value !== 'string' && typeof value !== 'number') return value; diff --git a/libs/markets/src/lib/components/market-info/market-info-accordion.tsx b/libs/markets/src/lib/components/market-info/market-info-accordion.tsx index 0835819d3..da773ee9d 100644 --- a/libs/markets/src/lib/components/market-info/market-info-accordion.tsx +++ b/libs/markets/src/lib/components/market-info/market-info-accordion.tsx @@ -10,6 +10,7 @@ import { Link as UILink, Splash, TinyScroll, + AccordionItem, } from '@vegaprotocol/ui-toolkit'; import { generatePath, Link } from 'react-router-dom'; @@ -85,26 +86,6 @@ export const MarketInfoAccordion = ({ market.accountsConnection?.edges ); - const marketDataPanels = [ - { - title: t('Current fees'), - content: , - }, - { - title: t('Market price'), - content: , - }, - { - title: t('Market volume'), - content: , - }, - ...marketAccounts - .filter((a) => a.type === Schema.AccountType.ACCOUNT_TYPE_INSURANCE) - .map((a) => ({ - title: t(`Insurance pool`), - content: , - })), - ]; const settlementData = market.tradableInstrument.instrument.product .dataSourceSpecForSettlementData.data as DataSourceDefinition; const terminationData = market.tradableInstrument.instrument.product @@ -123,165 +104,187 @@ export const MarketInfoAccordion = ({ } return []; }; - const oraclePanels = isEqual( - getSigners(settlementData), - getSigners(terminationData) - ) - ? [ - { - title: t('Oracle'), - content: ( - - ), - }, - ] - : [ - { - title: t('Settlement Oracle'), - content: ( - - ), - }, - { - title: t('Termination Oracle'), - content: ( - - ), - }, - ]; - const marketSpecPanels = [ - { - title: t('Key details'), - content: , - }, - { - title: t('Instrument'), - content: , - }, - ...oraclePanels, - { - title: t('Settlement asset'), - content: , - }, - { - title: t('Metadata'), - content: , - }, - { - title: t('Risk model'), - content: , - }, - { - title: t('Risk parameters'), - content: , - }, - { - title: t('Risk factors'), - content: , - }, - ...(market.priceMonitoringSettings?.parameters?.triggers || []).map( - (_, triggerIndex) => ({ - title: t(`Price monitoring bounds ${triggerIndex + 1}`), - content: ( - - ), - }) - ), - { - title: t('Liquidity monitoring parameters'), - content: , - }, - { - title: t('Liquidity'), - content: ( - - onSelect?.(market.id, ev.metaKey || ev.ctrlKey)} - data-testid="view-liquidity-link" - > - {t('View liquidity provision table')} - - - ), - }, - { - title: t('Liquidity price range'), - content: , - }, - ]; - - const marketGovPanels = [ - { - title: t('Proposal'), - content: ( -
- {VEGA_TOKEN_URL && ( - - {t('View governance proposal')} - - )} - {VEGA_TOKEN_URL && ( - - {t('Propose a change to market')} - - )} -
- ), - }, - ]; return (

{t('Market data')}

- + + } + /> + } + /> + } + /> + {marketAccounts + .filter((a) => a.type === Schema.AccountType.ACCOUNT_TYPE_INSURANCE) + .map((a) => ( + } + /> + ))} +

{t('Market specification')}

- + + } + /> + } + /> + {isEqual(getSigners(settlementData), getSigners(terminationData)) ? ( + + } + /> + ) : ( + <> + + } + /> + + } + /> + + )} + } + /> + } + /> + } + /> + } + /> + } + /> + {(market.priceMonitoringSettings?.parameters?.triggers || []).map( + (_, triggerIndex) => ( + + } + /> + ) + )} + } + /> + +
+ + onSelect?.(market.id, ev.metaKey || ev.ctrlKey) + } + data-testid="view-liquidity-link" + > + {t('View liquidity provision table')} + +
+ + } + /> + } + /> +
- {VEGA_TOKEN_URL && marketGovPanels && market.proposal?.id && ( + {VEGA_TOKEN_URL && market.proposal?.id && (

{t('Market governance')}

- + + + + {t('View governance proposal')} + + + {t('Propose a change to market')} + + + } + /> +
)}
diff --git a/libs/markets/src/lib/components/market-info/market-info-panels.tsx b/libs/markets/src/lib/components/market-info/market-info-panels.tsx index da470ecbf..6fb4af889 100644 --- a/libs/markets/src/lib/components/market-info/market-info-panels.tsx +++ b/libs/markets/src/lib/components/market-info/market-info-panels.tsx @@ -1,4 +1,4 @@ -import type { ComponentProps } from 'react'; +import type { ReactNode } from 'react'; import { useState } from 'react'; import { useMemo } from 'react'; import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets'; @@ -30,19 +30,12 @@ import { useOracleProofs } from '../../hooks'; import { OracleDialog } from '../oracle-dialog/oracle-dialog'; import { useDataProvider } from '@vegaprotocol/data-provider'; -type PanelProps = Pick< - ComponentProps, - 'children' | 'noBorder' ->; - type MarketInfoProps = { market: MarketInfo; + children?: ReactNode; }; -export const CurrentFeesInfoPanel = ({ - market, - ...props -}: MarketInfoProps & PanelProps) => ( +export const CurrentFeesInfoPanel = ({ market }: MarketInfoProps) => ( <>

{t( @@ -62,10 +54,7 @@ export const CurrentFeesInfoPanel = ({ ); -export const MarketPriceInfoPanel = ({ - market, - ...props -}: MarketInfoProps & PanelProps) => { +export const MarketPriceInfoPanel = ({ market }: MarketInfoProps) => { const assetSymbol = market?.tradableInstrument.instrument.product?.settlementAsset.symbol || ''; const quoteUnit = @@ -84,9 +73,8 @@ export const MarketPriceInfoPanel = ({ quoteUnit: market.tradableInstrument.instrument.product.quoteName, }} decimalPlaces={market.decimalPlaces} - {...props} /> -

+

{t( 'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).', [assetSymbol, quoteUnit] @@ -96,10 +84,7 @@ export const MarketPriceInfoPanel = ({ ); }; -export const MarketVolumeInfoPanel = ({ - market, - ...props -}: MarketInfoProps & PanelProps) => { +export const MarketVolumeInfoPanel = ({ market }: MarketInfoProps) => { const { data } = useDataProvider({ dataProvider: marketDataProvider, variables: { marketId: market.id }, @@ -124,7 +109,6 @@ export const MarketVolumeInfoPanel = ({ bestStaticOfferVolume: dash(data?.bestStaticOfferVolume), }} decimalPlaces={market.positionDecimalPlaces} - {...props} /> ); }; @@ -132,13 +116,11 @@ export const MarketVolumeInfoPanel = ({ export const InsurancePoolInfoPanel = ({ market, account, - ...props }: { account: NonNullable< Get >; -} & MarketInfoProps & - PanelProps) => { +} & MarketInfoProps) => { const assetSymbol = market?.tradableInstrument.instrument.product?.settlementAsset.symbol || ''; return ( @@ -150,14 +132,11 @@ export const InsurancePoolInfoPanel = ({ decimalPlaces={ market.tradableInstrument.instrument.product.settlementAsset.decimals } - {...props} /> ); }; -export const KeyDetailsInfoPanel = ({ - market, -}: MarketInfoProps & PanelProps) => { +export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => { const assetDecimals = market.tradableInstrument.instrument.product.settlementAsset.decimals; return ( @@ -175,10 +154,7 @@ export const KeyDetailsInfoPanel = ({ ); }; -export const InstrumentInfoPanel = ({ - market, - ...props -}: MarketInfoProps & PanelProps) => ( +export const InstrumentInfoPanel = ({ market }: MarketInfoProps) => ( ); -export const SettlementAssetInfoPanel = ({ - market, - noBorder = true, -}: MarketInfoProps & PanelProps) => { +export const SettlementAssetInfoPanel = ({ market }: MarketInfoProps) => { const assetSymbol = market?.tradableInstrument.instrument.product?.settlementAsset.symbol || ''; const quoteUnit = @@ -208,7 +180,7 @@ export const SettlementAssetInfoPanel = ({ @@ -224,10 +196,7 @@ export const SettlementAssetInfoPanel = ({ ); }; -export const MetadataInfoPanel = ({ - market, - ...props -}: MarketInfoProps & PanelProps) => ( +export const MetadataInfoPanel = ({ market }: MarketInfoProps) => ( ({ ...acc, ...curr }), {}), }} - {...props} /> ); -export const RiskModelInfoPanel = ({ - market, - ...props -}: MarketInfoProps & PanelProps) => { +export const RiskModelInfoPanel = ({ market }: MarketInfoProps) => { if (market.tradableInstrument.riskModel.__typename !== 'LogNormalRiskModel') { return null; } const { tau, riskAversionParameter } = market.tradableInstrument.riskModel; - return ( - - ); + return ; }; -export const RiskParametersInfoPanel = ({ - market, - ...props -}: MarketInfoProps & PanelProps) => { +export const RiskParametersInfoPanel = ({ market }: MarketInfoProps) => { if (market.tradableInstrument.riskModel.__typename === 'LogNormalRiskModel') { const { r, sigma, mu } = market.tradableInstrument.riskModel.params; - return ; + return ; } if (market.tradableInstrument.riskModel.__typename === 'SimpleRiskModel') { const { factorLong, factorShort } = market.tradableInstrument.riskModel.params; - return ( - - ); + return ; } return null; }; -export const RiskFactorsInfoPanel = ({ - market, - ...props -}: MarketInfoProps & PanelProps) => { +export const RiskFactorsInfoPanel = ({ market }: MarketInfoProps) => { if (!market.riskFactors) { return null; } const { short, long } = market.riskFactors; - return ; + return ; }; export const PriceMonitoringBoundsInfoPanel = ({ market, triggerIndex, - ...props -}: { +}: MarketInfoProps & { triggerIndex: number; -} & MarketInfoProps & - PanelProps) => { +}) => { const { data } = useDataProvider({ dataProvider: marketDataProvider, variables: { marketId: market.id }, @@ -318,8 +263,8 @@ export const PriceMonitoringBoundsInfoPanel = ({ return null; } return ( -

-
+ <> +

{t('%s probability price bounds', [ formatNumberPercentage( @@ -331,32 +276,28 @@ export const PriceMonitoringBoundsInfoPanel = ({ {t('Within %s seconds', [formatNumber(trigger.horizonSecs)])}

-
- {bounds && ( - - )} -
-

+ {bounds && ( + + )} +

{t('Results in %s seconds auction if breached', [ trigger.auctionExtensionSecs.toString(), ])}

-
+ ); }; export const LiquidityMonitoringParametersInfoPanel = ({ market, - ...props -}: MarketInfoProps & PanelProps) => ( +}: MarketInfoProps) => ( ); -export const LiquidityInfoPanel = ({ - market, - ...props -}: MarketInfoProps & PanelProps) => { +export const LiquidityInfoPanel = ({ market, children }: MarketInfoProps) => { const assetDecimals = market.tradableInstrument.instrument.product.settlementAsset.decimals; const assetSymbol = @@ -383,23 +320,22 @@ export const LiquidityInfoPanel = ({ variables: { marketId: market.id }, }); return ( - + <> + + {children} + ); }; -export const LiquidityPriceRangeInfoPanel = ({ - market, - ...props -}: MarketInfoProps & PanelProps) => { +export const LiquidityPriceRangeInfoPanel = ({ market }: MarketInfoProps) => { const quoteUnit = market?.tradableInstrument.instrument.product?.quoteName || ''; const liquidityPriceRange = formatNumberPercentage( @@ -411,39 +347,37 @@ export const LiquidityPriceRangeInfoPanel = ({ }); return ( <> -

+

{`For liquidity orders to count towards a commitment, they must be within the liquidity monitoring bounds.`}

-

+

{`The liquidity price range is a ${liquidityPriceRange} difference from the mid price.`}

-
- -
+ ); }; @@ -451,8 +385,7 @@ export const LiquidityPriceRangeInfoPanel = ({ export const OracleInfoPanel = ({ market, type, -}: MarketInfoProps & - PanelProps & { type: 'settlementData' | 'termination' }) => { +}: MarketInfoProps & { type: 'settlementData' | 'termination' }) => { const product = market.tradableInstrument.instrument.product; const { VEGA_EXPLORER_URL, ORACLE_PROOFS_URL } = useEnvironment(); const { data } = useOracleProofs(ORACLE_PROOFS_URL); @@ -469,7 +402,7 @@ export const OracleInfoPanel = ({ ) as DataSourceDefinition; return ( -
+
+
{signerProviders.map((provider) => ( -

{message}

-

- {oracleMarkets && - t('Involved in %s %s', [ +

+ {message} +

+ {oracleMarkets && ( +

+ {t('Involved in %s %s', [ oracleMarkets.length.toString(), oracleMarkets.length !== 1 ? t('markets') : t('market'), ])} -

+

+ )} {links.length > 0 && ( -
+
{links.map((link) => ( - - + + - - - {link.type} + {link.type} diff --git a/libs/ui-toolkit/src/components/accordion/accordion.spec.tsx b/libs/ui-toolkit/src/components/accordion/accordion.spec.tsx index b6f2f9c20..a3c82b9f5 100644 --- a/libs/ui-toolkit/src/components/accordion/accordion.spec.tsx +++ b/libs/ui-toolkit/src/components/accordion/accordion.spec.tsx @@ -1,15 +1,17 @@ import { fireEvent, render, screen } from '@testing-library/react'; -import { Accordion } from './accordion'; +import { Accordion, AccordionItem } from './accordion'; describe('Accordion', () => { it('should render successfully', () => { render( - + + + ); expect(screen.queryByTestId('accordion-title')).toHaveTextContent( 'Lorem ipsum title' @@ -18,11 +20,13 @@ describe('Accordion', () => { it('should toggle and open expansion panel', () => { render( - + + + ); fireEvent.click(screen.getByTestId('accordion-toggle')); expect(screen.queryByTestId('accordion-title')).toHaveTextContent( diff --git a/libs/ui-toolkit/src/components/accordion/accordion.stories.tsx b/libs/ui-toolkit/src/components/accordion/accordion.stories.tsx index 775d7d807..99b542e20 100644 --- a/libs/ui-toolkit/src/components/accordion/accordion.stories.tsx +++ b/libs/ui-toolkit/src/components/accordion/accordion.stories.tsx @@ -1,5 +1,5 @@ import type { Story, Meta } from '@storybook/react'; -import { Accordion } from './accordion'; +import { Accordion, AccordionItem } from './accordion'; export default { component: Accordion, @@ -10,18 +10,21 @@ const Template: Story = (args) => ; export const Default = Template.bind({}); Default.args = { - panels: [ - { - title: 'Title of expansion panel', - content: 'Lorem ipsum', - }, - { - title: 'Title of expansion panel', - content: 'Lorem ipsum', - }, - { - title: 'Title of expansion panel', - content: 'Lorem ipsum', - }, + children: [ + , + , + , ], }; diff --git a/libs/ui-toolkit/src/components/accordion/accordion.tsx b/libs/ui-toolkit/src/components/accordion/accordion.tsx index 41f9525bc..9e52f8250 100644 --- a/libs/ui-toolkit/src/components/accordion/accordion.tsx +++ b/libs/ui-toolkit/src/components/accordion/accordion.tsx @@ -1,7 +1,6 @@ -import React, { useState } from 'react'; import * as AccordionPrimitive from '@radix-ui/react-accordion'; import classNames from 'classnames'; -import { Icon } from '../icon'; +import { VegaIcon, VegaIconNames } from '../icon'; export interface AccordionItemProps { title: React.ReactNode; @@ -10,7 +9,6 @@ export interface AccordionItemProps { export interface AccordionPanelProps extends AccordionItemProps { itemId: string; - active: boolean; } export interface AccordionProps { @@ -19,23 +17,8 @@ export interface AccordionProps { } export const Accordion = ({ panels, children }: AccordionProps) => { - const [values, setValues] = useState([]); - return ( - - {panels?.map(({ title, content }, i) => ( - - ))} + {children} ); @@ -45,11 +28,11 @@ export const AccordionItem = ({ title, content, itemId, - active, }: AccordionPanelProps) => { const triggerClassNames = classNames( 'w-full py-2', - 'flex items-center justify-between border-b border-neutral-500 text-sm' + 'flex items-center justify-between border-b border-vega-light-200 dark:border-vega-dark-200 text-sm', + 'group' ); return ( @@ -59,26 +42,28 @@ export const AccordionItem = ({ className={triggerClassNames} > {title} - + - -
- {content} -
+ + {content}
); }; -export const AccordionChevron = ({ active }: { active: boolean }) => { +export const AccordionChevron = () => { return ( - + + + ); }; diff --git a/libs/ui-toolkit/src/components/link/link.tsx b/libs/ui-toolkit/src/components/link/link.tsx index 2059a5e62..524ba3fd6 100644 --- a/libs/ui-toolkit/src/components/link/link.tsx +++ b/libs/ui-toolkit/src/components/link/link.tsx @@ -43,8 +43,8 @@ Link.displayName = 'Link'; export const ExternalLink = ({ children, className, ...props }: LinkProps) => ( -
{description}
+
+ {description} +
)} From 0d96c487d9f2fcf557279c43e2fb3b0a2d2ddee6 Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Tue, 6 Jun 2023 00:08:19 -0700 Subject: [PATCH 02/49] fix(trading): use ag grid to handle error,loading and no data message (#4012) --- .../src/integration/trading-positions.cy.ts | 13 +- .../client-pages/liquidity/liquidity.tsx | 42 +------ apps/trading/client-pages/markets/closed.tsx | 25 ++-- .../portfolio/deposits-container.tsx | 17 +-- .../portfolio/withdrawals-container.tsx | 17 +-- .../src/lib/accounts-manager.spec.tsx | 13 +- libs/accounts/src/lib/accounts-manager.tsx | 35 +----- .../src/lib/ag-grid/ag-grid-lazy-themed.tsx | 3 + libs/fills/src/lib/fills-manager.tsx | 71 ++--------- libs/ledger/src/lib/ledger-manager.tsx | 29 +---- .../markets-container/markets-container.tsx | 41 +------ .../order-list-manager.spec.tsx | 51 -------- .../order-list-manager/order-list-manager.tsx | 31 +---- .../components/order-list/order-list.spec.tsx | 9 -- .../src/lib/positions-data-providers.ts | 11 +- libs/positions/src/lib/positions-manager.tsx | 24 +--- .../proposals-list/proposal-list.spec.tsx | 24 +++- .../proposals-list/proposals-list.tsx | 27 +---- libs/trades/src/lib/trades-container.tsx | 112 +++--------------- libs/trades/src/lib/trades-data-provider.ts | 51 ++++---- libs/trades/src/lib/trades-table.tsx | 1 - 21 files changed, 121 insertions(+), 526 deletions(-) diff --git a/apps/trading-e2e/src/integration/trading-positions.cy.ts b/apps/trading-e2e/src/integration/trading-positions.cy.ts index 3d8effb6b..4e9ba75d7 100644 --- a/apps/trading-e2e/src/integration/trading-positions.cy.ts +++ b/apps/trading-e2e/src/integration/trading-positions.cy.ts @@ -98,23 +98,14 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => { }, }, ]; - const marketData = marketsDataQuery(); - const edges = marketData.marketsConnection?.edges.map((market) => { - const replace = - market.node.data?.market.id === 'market-2' ? null : market.node.data; - return { ...market, node: { ...market.node, data: replace } }; - }); const overrides = { - ...marketData, - marketsConnection: { ...marketData.marketsConnection, edges }, + marketsConnection: { edges: [] }, }; cy.mockGQL((req) => { aliasGQLQuery(req, 'MarketsData', overrides, errors); }); cy.visit('/#/markets/market-0'); - cy.get('.pointer-events-none.absolute.inset-0').contains( - 'Something went wrong:' - ); + cy.get('[data-testid="tab-positions"]').contains('no market data'); }); }); diff --git a/apps/trading/client-pages/liquidity/liquidity.tsx b/apps/trading/client-pages/liquidity/liquidity.tsx index f37f16652..71924b40f 100644 --- a/apps/trading/client-pages/liquidity/liquidity.tsx +++ b/apps/trading/client-pages/liquidity/liquidity.tsx @@ -11,14 +11,12 @@ import { formatNumberPercentage, } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; -import { updateGridData } from '@vegaprotocol/datagrid'; import { NetworkParams, useNetworkParams, } from '@vegaprotocol/network-parameters'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { - AsyncRenderer, Tab, Tabs, Link as UiToolkitLink, @@ -26,14 +24,13 @@ import { ExternalLink, } from '@vegaprotocol/ui-toolkit'; import { useVegaWallet } from '@vegaprotocol/wallet'; -import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import { memo, useEffect, useRef, useState } from 'react'; import { Header, HeaderStat, HeaderTitle } from '../../components/header'; import type { AgGridReact } from 'ag-grid-react'; -import type { IGetRowsParams } from 'ag-grid-community'; -import type { LiquidityProvisionData, Filter } from '@vegaprotocol/liquidity'; +import type { Filter } from '@vegaprotocol/liquidity'; import { Link, useParams } from 'react-router-dom'; import { Links, Routes } from '../../pages/client-router'; @@ -74,21 +71,12 @@ export const LiquidityContainer = ({ }) => { const gridRef = useRef(null); const { data: market } = useMarket(marketId); - const dataRef = useRef(null); // To be removed when liquidityProvision subscriptions are working useReloadLiquidityData(marketId); - const update = useCallback( - ({ data }: { data: LiquidityProvisionData[] | null }) => { - return updateGridData(dataRef, data, gridRef); - }, - [gridRef] - ); - - const { data, loading, error } = useDataProvider({ + const { data, error } = useDataProvider({ dataProvider: lpAggregatedDataProvider, - update, variables: { marketId: marketId || '', filter }, skip: !marketId, }); @@ -103,36 +91,16 @@ export const LiquidityContainer = ({ ]); const stakeToCcyVolume = params.market_liquidity_stakeToCcyVolume; - const getRows = useCallback( - async ({ successCallback, startRow, endRow }: IGetRowsParams) => { - const rowsThisBlock = dataRef.current - ? dataRef.current.slice(startRow, endRow) - : []; - const lastRow = dataRef.current ? dataRef.current.length : 0; - successCallback(rowsThisBlock, lastRow); - }, - [] - ); - return (
-
- !data?.length} - /> -
); }; diff --git a/apps/trading/client-pages/markets/closed.tsx b/apps/trading/client-pages/markets/closed.tsx index c081f9553..26094faa8 100644 --- a/apps/trading/client-pages/markets/closed.tsx +++ b/apps/trading/client-pages/markets/closed.tsx @@ -27,7 +27,6 @@ import type { ColDef } from 'ag-grid-community'; import { SettlementDateCell } from './settlement-date-cell'; import { SettlementPriceCell } from './settlement-price-cell'; import { useDataProvider } from '@vegaprotocol/data-provider'; -import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; type SettlementAsset = MarketMaybeWithData['tradableInstrument']['instrument']['product']['settlementAsset']; @@ -55,7 +54,6 @@ export const Closed = () => { const { pubKey } = useVegaWallet(); const { data: marketData, - loading, error, reload, } = useDataProvider({ @@ -117,21 +115,20 @@ export const Closed = () => { }); return (
- -
- -
+
); }; -const ClosedMarketsDataGrid = ({ rowData }: { rowData: Row[] }) => { +const ClosedMarketsDataGrid = ({ + rowData, + error, + reload, +}: { + rowData: Row[]; + error: Error | undefined; + reload: () => void; +}) => { const openAssetDialog = useAssetDetailsDialogStore((store) => store.open); const colDefs = useMemo(() => { const cols: ColDef[] = [ @@ -315,7 +312,7 @@ const ClosedMarketsDataGrid = ({ rowData }: { rowData: Row[] }) => { resizable: true, minWidth: 100, }} - overlayNoRowsTemplate="No data" + overlayNoRowsTemplate={error ? error.message : t('No markets')} storeKey="closedMarkets" /> ); diff --git a/apps/trading/client-pages/portfolio/deposits-container.tsx b/apps/trading/client-pages/portfolio/deposits-container.tsx index f79beee88..58fb25004 100644 --- a/apps/trading/client-pages/portfolio/deposits-container.tsx +++ b/apps/trading/client-pages/portfolio/deposits-container.tsx @@ -1,4 +1,4 @@ -import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit'; +import { Button } from '@vegaprotocol/ui-toolkit'; import { useDepositDialog, DepositsTable } from '@vegaprotocol/deposits'; import { depositsProvider } from '@vegaprotocol/deposits'; import { t } from '@vegaprotocol/i18n'; @@ -11,7 +11,7 @@ import type { AgGridReact } from 'ag-grid-react'; export const DepositsContainer = () => { const gridRef = useRef(null); const { pubKey, isReadOnly } = useVegaWallet(); - const { data, loading, error, reload } = useDataProvider({ + const { data, error } = useDataProvider({ dataProvider: depositsProvider, variables: { partyId: pubKey || '' }, skip: !pubKey, @@ -23,21 +23,10 @@ export const DepositsContainer = () => {
-
- !(data && data.length)} - noDataMessage={t('No deposits')} - reload={reload} - /> -
{!isReadOnly && (
diff --git a/apps/trading/client-pages/portfolio/withdrawals-container.tsx b/apps/trading/client-pages/portfolio/withdrawals-container.tsx index 8edfc0da1..0809dd911 100644 --- a/apps/trading/client-pages/portfolio/withdrawals-container.tsx +++ b/apps/trading/client-pages/portfolio/withdrawals-container.tsx @@ -1,4 +1,4 @@ -import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit'; +import { Button } from '@vegaprotocol/ui-toolkit'; import { withdrawalProvider, useWithdrawalDialog, @@ -11,7 +11,7 @@ import { VegaWalletContainer } from '../../components/vega-wallet-container'; export const WithdrawalsContainer = () => { const { pubKey, isReadOnly } = useVegaWallet(); - const { data, loading, error, reload } = useDataProvider({ + const { data, error } = useDataProvider({ dataProvider: withdrawalProvider, variables: { partyId: pubKey || '' }, skip: !pubKey, @@ -24,19 +24,8 @@ export const WithdrawalsContainer = () => { -
- !(data && data.length)} - noDataMessage={t('No withdrawals')} - reload={reload} - /> -
{!isReadOnly && (
diff --git a/libs/accounts/src/lib/accounts-manager.spec.tsx b/libs/accounts/src/lib/accounts-manager.spec.tsx index bfbc02308..0278ff546 100644 --- a/libs/accounts/src/lib/accounts-manager.spec.tsx +++ b/libs/accounts/src/lib/accounts-manager.spec.tsx @@ -97,7 +97,7 @@ describe('AccountManager', () => { }); }); - it('splash loading should be displayed', async () => { + it('loading should be displayed', async () => { mockedUseDataProvider.mockImplementation((args) => { return { loading: true, @@ -113,15 +113,6 @@ describe('AccountManager', () => { /> ); }); - await waitFor(() => { - expect( - screen.getByText( - (content, element) => - Boolean( - element?.className.endsWith('flex items-center justify-center') - ) && content.startsWith('Loading') - ) - ).toBeInTheDocument(); - }); + expect(await screen.findByText('Loading...')).toBeInTheDocument(); }); }); diff --git a/libs/accounts/src/lib/accounts-manager.tsx b/libs/accounts/src/lib/accounts-manager.tsx index f8cf912df..8f9ee3517 100644 --- a/libs/accounts/src/lib/accounts-manager.tsx +++ b/libs/accounts/src/lib/accounts-manager.tsx @@ -1,11 +1,9 @@ -import { useRef, memo, useCallback, useState, useEffect } from 'react'; +import { useRef, memo, useCallback, useState } from 'react'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; import { useBottomPlaceholder } from '@vegaprotocol/datagrid'; import { useDataProvider } from '@vegaprotocol/data-provider'; -import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import type { AgGridReact } from 'ag-grid-react'; -import type { RowDataUpdatedEvent } from 'ag-grid-community'; import type { AccountFields } from './accounts-data-provider'; import { aggregatedAccountsDataProvider, @@ -71,7 +69,6 @@ export const AccountManager = ({ storeKey, }: AccountManagerProps) => { const gridRef = useRef(null); - const [hasData, setHasData] = useState(Boolean(pinnedAsset)); const [breakdownAssetId, setBreakdownAssetId] = useState(); const update = useCallback( ({ data }: { data: AccountFields[] | null }) => { @@ -102,7 +99,7 @@ export const AccountManager = ({ }, [gridRef, pinnedAsset] ); - const { data, loading, error, reload } = useDataProvider({ + const { data, error } = useDataProvider({ dataProvider: aggregatedAccountsDataProvider, variables: { partyId }, update, @@ -112,45 +109,21 @@ export const AccountManager = ({ disabled: noBottomPlaceholder, }); - useEffect( - () => setHasData(Boolean(pinnedAsset || data?.length)), - [data, pinnedAsset] - ); - - const onRowDataUpdated = useCallback( - (event: RowDataUpdatedEvent) => { - setHasData(Boolean(pinnedAsset || event.api?.getModel().getRowCount())); - }, - [pinnedAsset] - ); - return (
-
- !hasData} - error={error} - loading={loading} - noDataMessage={pinnedAsset ? ' ' : t('No accounts')} - reload={reload} - /> -
{ const gridRef = useRef(null); const scrolledToTop = useRef(true); - const { - data, - error, - loading, - addNewRows, - getRows, - reload, - makeBottomPlaceholders, - } = useFillsList({ + const { data, error } = useFillsList({ partyId, marketId, gridRef, scrolledToTop, }); - const checkBottomPlaceholder = useCallback(() => { - const rowCont = gridRef.current?.api?.getModel().getRowCount() ?? 0; - const lastRowIndex = gridRef.current?.api?.getLastDisplayedRow(); - if (lastRowIndex && rowCont - 1 === lastRowIndex) { - const lastrow = gridRef.current?.api.getDisplayedRowAtIndex(lastRowIndex); - lastrow?.setRowHeight(50); - makeBottomPlaceholders(lastrow?.data); - gridRef.current?.api.onRowHeightChanged(); - gridRef.current?.api.refreshInfiniteCache(); - } - }, [makeBottomPlaceholders]); + const bottomPlaceholderProps = useBottomPlaceholder({ + gridRef, + }); - const onBodyScrollEnd = useCallback( - (event: BodyScrollEndEvent) => { - if (event.top === 0) { - addNewRows(); - } - checkBottomPlaceholder(); - }, - [addNewRows, checkBottomPlaceholder] - ); - - const onBodyScroll = useCallback((event: BodyScrollEvent) => { - scrolledToTop.current = event.top <= 0; - }, []); - - const { isFullWidthRow, fullWidthCellRenderer, rowClassRules, getRowHeight } = - useBottomPlaceholder({ - gridRef, - }); + const fills = compact(data).map((e) => e.node); return (
-
- !(data && data.length)} - reload={reload} - /> -
); }; diff --git a/libs/ledger/src/lib/ledger-manager.tsx b/libs/ledger/src/lib/ledger-manager.tsx index a4ca28c2f..5da40809f 100644 --- a/libs/ledger/src/lib/ledger-manager.tsx +++ b/libs/ledger/src/lib/ledger-manager.tsx @@ -1,11 +1,9 @@ import { t } from '@vegaprotocol/i18n'; import type * as Schema from '@vegaprotocol/types'; -import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import type { FilterChangedEvent } from 'ag-grid-community'; import type { AgGridReact } from 'ag-grid-react'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { subDays, formatRFC3339 } from 'date-fns'; -import type { AggregatedLedgerEntriesNode } from './ledger-entries-data-provider'; import { useLedgerEntriesDataProvider } from './ledger-entries-data-provider'; import { LedgerTable } from './ledger-table'; import type * as Types from '@vegaprotocol/types'; @@ -27,9 +25,8 @@ const defaultFilter = { export const LedgerManager = ({ partyId }: { partyId: string }) => { const gridRef = useRef(null); const [filter, setFilter] = useState(defaultFilter); - const [dataCount, setDataCount] = useState(0); - const { data, error, loading, reload } = useLedgerEntriesDataProvider({ + const { data, error } = useLedgerEntriesDataProvider({ partyId, filter, gridRef, @@ -39,16 +36,9 @@ export const LedgerManager = ({ partyId }: { partyId: string }) => { const updatedFilter = { ...defaultFilter, ...event.api.getFilterModel() }; setFilter(updatedFilter); }, []); - const extractNodesDecorator = useCallback( - (data: AggregatedLedgerEntriesNode[] | null, loading: boolean) => - data && !loading ? data.map((item) => item.node) : null, - [] - ); - const extractedData = extractNodesDecorator(data, loading); - useEffect(() => { - setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0); - }, [extractedData]); + // allow passing undefined to grid so that loading state is shown + const extractedData = data?.map((item) => item.node); return (
@@ -56,20 +46,11 @@ export const LedgerManager = ({ partyId }: { partyId: string }) => { ref={gridRef} rowData={extractedData} onFilterChanged={onFilterChanged} + overlayNoRowsTemplate={error ? error.message : t('No entries')} /> {extractedData && ( )} -
- !dataCount} - reload={reload} - /> -
); }; diff --git a/libs/markets/src/lib/components/markets-container/markets-container.tsx b/libs/markets/src/lib/components/markets-container/markets-container.tsx index 054946995..76a05c5b0 100644 --- a/libs/markets/src/lib/components/markets-container/markets-container.tsx +++ b/libs/markets/src/lib/components/markets-container/markets-container.tsx @@ -1,9 +1,8 @@ import type { MouseEvent } from 'react'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useEffect, useRef } from 'react'; import type { AgGridReact } from 'ag-grid-react'; import type { CellClickedEvent } from 'ag-grid-community'; import { t } from '@vegaprotocol/i18n'; -import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { MarketListTable } from './market-list-table'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { marketsWithDataProvider as dataProvider } from '../../markets-provider'; @@ -16,25 +15,10 @@ interface MarketsContainerProps { export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => { const gridRef = useRef(null); - const dataRef = useRef(null); - const [dataCount, setDataCount] = useState(1); - const handleDataCount = useCallback(() => { - setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0); - }, []); - const update = useCallback( - ({ data }: { data: MarketMaybeWithData[] | null }) => { - data && gridRef.current?.api?.setRowData(data); - dataRef.current = data; - handleDataCount(); - return true; - }, - [handleDataCount] - ); - const { error, loading, reload } = useDataProvider({ + const { data, error, reload } = useDataProvider({ dataProvider, variables: undefined, - update, }); useEffect(() => { @@ -46,17 +30,11 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => { }; }, [reload]); - const handleOnGridReady = useCallback(() => { - dataRef?.current && update({ data: dataRef.current }); - handleDataCount(); - }, [handleDataCount, update]); - return (
{ const { data, column, event } = cellEvent; // prevent navigating to the market page if any of the below cells are clicked @@ -79,19 +57,8 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => { ); }} onMarketClick={onSelect} - onFilterChanged={handleDataCount} - onGridReady={handleOnGridReady} + overlayNoRowsTemplate={error ? error.message : t('No markets')} /> -
- !dataCount} - reload={reload} - /> -
); }; diff --git a/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx b/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx index 88e151a59..666070b2a 100644 --- a/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx +++ b/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx @@ -24,41 +24,6 @@ const generateJsx = () => { }; describe('OrderListManager', () => { - it('should render a loading state while awaiting orders', async () => { - jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({ - data: [], - loading: true, - error: undefined, - flush: jest.fn(), - reload: jest.fn(), - load: jest.fn(), - totalCount: 0, - }); - await act(async () => { - render(generateJsx()); - }); - expect(screen.getByText('Loading...')).toBeInTheDocument(); - }); - - it('should render an error state', async () => { - const errorMsg = 'Oops! An Error'; - jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({ - data: null, - loading: false, - error: new Error(errorMsg), - flush: jest.fn(), - reload: jest.fn(), - load: jest.fn(), - totalCount: undefined, - }); - await act(async () => { - render(generateJsx()); - }); - expect( - screen.getByText(`Something went wrong: ${errorMsg}`) - ).toBeInTheDocument(); - }); - it('should render the order list if orders provided', async () => { jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({ data: [{ id: '1' } as OrderFieldsFragment], @@ -74,20 +39,4 @@ describe('OrderListManager', () => { }); expect(await screen.findByText('OrderList')).toBeInTheDocument(); }); - - it('should show no orders message', async () => { - jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({ - data: [], - loading: false, - error: undefined, - flush: jest.fn(), - reload: jest.fn(), - load: jest.fn(), - totalCount: undefined, - }); - await act(async () => { - render(generateJsx()); - }); - expect(screen.getByText('No orders')).toBeInTheDocument(); - }); }); diff --git a/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx b/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx index f9e07b6e7..c4833c828 100644 --- a/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx +++ b/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx @@ -1,6 +1,5 @@ -import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { t } from '@vegaprotocol/i18n'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { Button } from '@vegaprotocol/ui-toolkit'; import type { AgGridReact } from 'ag-grid-react'; import type { GridReadyEvent, FilterChangedEvent } from 'ag-grid-community'; @@ -72,7 +71,6 @@ export const OrderListManager = ({ storeKey, }: OrderListManagerProps) => { const gridRef = useRef(null); - const [hasData, setHasData] = useState(false); const [editOrder, setEditOrder] = useState(null); const create = useVegaTransactionStore((state) => state.create); const hasAmendableOrder = useHasAmendableOrder(marketId); @@ -81,7 +79,7 @@ export const OrderListManager = ({ ? { partyId, filter: { liveOnly: true } } : { partyId }; - const { data, error, loading, reload } = useDataProvider({ + const { data, error } = useDataProvider({ dataProvider: ordersWithMarketProvider, variables, update: ({ data }) => { @@ -129,22 +127,11 @@ export const OrderListManager = ({ const onFilterChanged = useCallback( (event: FilterChangedEvent) => { - const rowCount = gridRef.current?.api?.getModel().getRowCount(); - setHasData((rowCount ?? 0) > 0); bottomPlaceholderOnFilterChanged?.(); }, [bottomPlaceholderOnFilterChanged] ); - const onRowDataChanged = useCallback(() => { - const rowCount = gridRef.current?.api?.getModel().getRowCount(); - setHasData((rowCount ?? 0) > 0); - }, []); - - useEffect(() => { - setHasData(Boolean(data?.length)); - }, [data]); - const cancelAll = useCallback(() => { create({ orderCancellation: {}, @@ -164,24 +151,12 @@ export const OrderListManager = ({ onMarketClick={onMarketClick} onOrderTypeClick={onOrderTypeClick} onFilterChanged={onFilterChanged} - onRowDataChanged={onRowDataChanged} isReadOnly={isReadOnly} storeKey={storeKey} - suppressLoadingOverlay - suppressNoRowsOverlay suppressAutoSize + overlayNoRowsTemplate={error ? error.message : t('No orders')} {...bottomPlaceholderProps} /> -
- !hasData} - reload={reload} - /> -
{!isReadOnly && hasAmendableOrder && ( 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 a69417e86..6af806a86 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 @@ -44,15 +44,6 @@ const generateJsx = ( }; describe('OrderListTable', () => { - it('should show no orders message', async () => { - await act(async () => { - render(generateJsx({ rowData: [] })); - }); - expect(() => screen.getByText('No orders')).toThrow( - 'Unable to find an element' - ); - }); - it('should render correct columns', async () => { await act(async () => { render(generateJsx({ rowData: [marketOrder, limitOrder] })); diff --git a/libs/positions/src/lib/positions-data-providers.ts b/libs/positions/src/lib/positions-data-providers.ts index 4edf6cf88..c13ba32d7 100644 --- a/libs/positions/src/lib/positions-data-providers.ts +++ b/libs/positions/src/lib/positions-data-providers.ts @@ -210,10 +210,13 @@ const positionDataProvider = makeDerivedDataProvider< partyIds: variables.partyIds, }), ], - (data, variables) => - (data[0] as PositionFieldsFragment[] | null)?.find( - (p) => p.market.id === variables?.marketId - ) || null + (data, variables) => { + return ( + (data[0] as PositionFieldsFragment[] | null)?.find( + (p) => p.market.id === variables?.marketId + ) || null + ); + } ); export const openVolumeDataProvider = makeDerivedDataProvider< diff --git a/libs/positions/src/lib/positions-manager.tsx b/libs/positions/src/lib/positions-manager.tsx index 5ca5c1e46..9899c235d 100644 --- a/libs/positions/src/lib/positions-manager.tsx +++ b/libs/positions/src/lib/positions-manager.tsx @@ -1,5 +1,4 @@ -import { useCallback, useRef, useState } from 'react'; -import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; +import { useRef } from 'react'; import { usePositionsData } from './use-positions-data'; import { PositionsTable } from './positions-table'; import type { AgGridReact } from 'ag-grid-react'; @@ -26,8 +25,7 @@ export const PositionsManager = ({ }: PositionsManagerProps) => { const { pubKeys, pubKey } = useVegaWallet(); const gridRef = useRef(null); - const { data, error, loading, reload } = usePositionsData(partyIds, gridRef); - const [dataCount, setDataCount] = useState(data?.length ?? 0); + const { data, error } = usePositionsData(partyIds, gridRef); const create = useVegaTransactionStore((store) => store.create); const onClose = ({ marketId, @@ -63,9 +61,6 @@ export const PositionsManager = ({ gridRef, disabled: noBottomPlaceholder, }); - const updateRowCount = useCallback(() => { - setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0); - }, []); return (
@@ -76,25 +71,12 @@ export const PositionsManager = ({ ref={gridRef} onMarketClick={onMarketClick} onClose={onClose} - suppressLoadingOverlay - suppressNoRowsOverlay isReadOnly={isReadOnly} - onFilterChanged={updateRowCount} - onRowDataUpdated={updateRowCount} {...bottomPlaceholderProps} storeKey={storeKey} multipleKeys={partyIds.length > 1} + overlayNoRowsTemplate={error ? error.message : t('No positions')} /> -
- !dataCount} - reload={reload} - /> -
); }; diff --git a/libs/proposals/src/components/proposals-list/proposal-list.spec.tsx b/libs/proposals/src/components/proposals-list/proposal-list.spec.tsx index 5bef72d3c..0a5d5052e 100644 --- a/libs/proposals/src/components/proposals-list/proposal-list.spec.tsx +++ b/libs/proposals/src/components/proposals-list/proposal-list.spec.tsx @@ -110,17 +110,29 @@ describe('ProposalsList', () => { }); it('empty response should causes no data message display', async () => { + const mock: MockedResponse = { + request: { + query: ProposalsListDocument, + variables: { + proposalType: Types.ProposalType.TYPE_NEW_MARKET, + }, + }, + result: { + data: { + proposalsConnection: { + __typename: 'ProposalsConnection', + edges: [], + }, + }, + }, + }; await act(() => { render( - + ); }); - const container = document.querySelector('.ag-center-cols-container'); - await waitFor(() => { - expect(container).toBeInTheDocument(); - }); - expect(screen.getByText('No markets')).toBeInTheDocument(); + expect(await screen.findByText('No markets')).toBeInTheDocument(); }); }); diff --git a/libs/proposals/src/components/proposals-list/proposals-list.tsx b/libs/proposals/src/components/proposals-list/proposals-list.tsx index 3c5c94b9e..392d5974a 100644 --- a/libs/proposals/src/components/proposals-list/proposals-list.tsx +++ b/libs/proposals/src/components/proposals-list/proposals-list.tsx @@ -1,5 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; +import { useRef } from 'react'; import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid'; import { t } from '@vegaprotocol/i18n'; import * as Types from '@vegaprotocol/types'; @@ -20,8 +19,7 @@ export const getNewMarketProposals = (data: ProposalListFieldsFragment[]) => export const ProposalsList = () => { const gridRef = useRef(null); - const [dataCount, setDataCount] = useState(0); - const { data, loading, error, refetch } = useProposalsListQuery({ + const { data, error } = useProposalsListQuery({ variables: { proposalType: Types.ProposalType.TYPE_NEW_MARKET, }, @@ -31,12 +29,6 @@ export const ProposalsList = () => { removePaginationWrapper(data?.proposalsConnection?.edges) ); const { columnDefs, defaultColDef } = useColumnDefs(); - const handleDataCount = useCallback(() => { - setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0); - }, []); - useEffect(() => { - handleDataCount(); - }, [filteredData, handleDataCount]); return (
@@ -46,24 +38,11 @@ export const ProposalsList = () => { columnDefs={columnDefs} rowData={filteredData} defaultColDef={defaultColDef} - suppressLoadingOverlay - suppressNoRowsOverlay - onFilterChanged={handleDataCount} storeKey="proposedMarkets" getRowId={({ data }) => data.id} style={{ width: '100%', height: '100%' }} - onGridReady={handleDataCount} + overlayNoRowsTemplate={error ? error.message : t('No markets')} /> -
- !dataCount} - reload={refetch} - /> -
); }; diff --git a/libs/trades/src/lib/trades-container.tsx b/libs/trades/src/lib/trades-container.tsx index 1779426e0..3396728d7 100644 --- a/libs/trades/src/lib/trades-container.tsx +++ b/libs/trades/src/lib/trades-container.tsx @@ -1,13 +1,11 @@ -import { makeInfiniteScrollGetRows } from '@vegaprotocol/data-provider'; +import compact from 'lodash/compact'; import { useDataProvider } from '@vegaprotocol/data-provider'; -import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import type { AgGridReact } from 'ag-grid-react'; -import { useCallback, useRef } from 'react'; -import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community'; +import { useRef } from 'react'; import { tradesWithMarketProvider } from './trades-data-provider'; import { TradesTable } from './trades-table'; -import type { Trade, TradeEdge } from './trades-data-provider'; import { useOrderStore } from '@vegaprotocol/orders'; +import { t } from '@vegaprotocol/i18n'; interface TradesContainerProps { marketId: string; @@ -15,104 +13,24 @@ interface TradesContainerProps { export const TradesContainer = ({ marketId }: TradesContainerProps) => { const gridRef = useRef(null); - const dataRef = useRef<(TradeEdge | null)[] | null>(null); - const totalCountRef = useRef(undefined); - const newRows = useRef(0); - const scrolledToTop = useRef(true); const updateOrder = useOrderStore((store) => store.update); - const addNewRows = useCallback(() => { - if (newRows.current === 0) { - return; - } - if (totalCountRef.current !== undefined) { - totalCountRef.current += newRows.current; - } - newRows.current = 0; - gridRef.current?.api?.refreshInfiniteCache(); - }, []); - - const update = useCallback( - ({ - data, - delta, - }: { - data: (TradeEdge | null)[] | null; - delta?: Trade[]; - }) => { - if (dataRef.current?.length) { - if (!scrolledToTop.current) { - const createdAt = dataRef.current?.[0]?.node.createdAt; - if (createdAt) { - newRows.current += (delta || []).filter( - (trade) => trade.createdAt > createdAt - ).length; - } - } - dataRef.current = data; - gridRef.current?.api?.refreshInfiniteCache(); - return true; - } - dataRef.current = data; - return false; - }, - [] - ); - - const insert = useCallback( - ({ - data, - totalCount, - }: { - data: (TradeEdge | null)[] | null; - totalCount?: number; - }) => { - dataRef.current = data; - totalCountRef.current = totalCount; - return true; - }, - [] - ); - - const { data, error, loading, load, totalCount, reload } = useDataProvider({ + const { data, error } = useDataProvider({ dataProvider: tradesWithMarketProvider, - update, - insert, variables: { marketId }, }); - totalCountRef.current = totalCount; - const getRows = makeInfiniteScrollGetRows( - dataRef, - totalCountRef, - load, - newRows - ); - - const onBodyScrollEnd = (event: BodyScrollEndEvent) => { - if (event.top === 0) { - addNewRows(); - } - }; - - const onBodyScroll = (event: BodyScrollEvent) => { - scrolledToTop.current = event.top <= 0; - }; + const trades = compact(data).map((d) => d.node); return ( - - { - if (price) { - updateOrder(marketId, { price }); - } - }} - /> - + { + if (price) { + updateOrder(marketId, { price }); + } + }} + overlayNoRowsTemplate={error ? error.message : t('No trades')} + /> ); }; diff --git a/libs/trades/src/lib/trades-data-provider.ts b/libs/trades/src/lib/trades-data-provider.ts index 601a0a66a..47102c68c 100644 --- a/libs/trades/src/lib/trades-data-provider.ts +++ b/libs/trades/src/lib/trades-data-provider.ts @@ -2,8 +2,6 @@ import { makeDataProvider, makeDerivedDataProvider, defaultAppend as append, - paginatedCombineDelta as combineDelta, - paginatedCombineInsertionData as combineInsertionData, } from '@vegaprotocol/data-provider'; import type { PageInfo, Edge } from '@vegaprotocol/data-provider'; import type { Market } from '@vegaprotocol/markets'; @@ -18,7 +16,7 @@ import { TradesDocument, TradesUpdateDocument } from './__generated__/Trades'; import orderBy from 'lodash/orderBy'; import produce from 'immer'; -export const MAX_TRADES = 50; +export const MAX_TRADES = 500; const getData = ( responseData: TradesQuery | null @@ -34,32 +32,25 @@ const update = ( data: ReturnType | null, delta: ReturnType ) => { + if (!data) return data; return produce(data, (draft) => { + // for each incoming trade add it to the beginning and remove oldest trade orderBy(delta, 'createdAt', 'desc').forEach((node) => { - if (!draft) { - return; - } - const index = draft.findIndex((edge) => edge?.node.id === node.id); - if (index !== -1) { - if (draft?.[index]?.node) { - Object.assign(draft[index]?.node as TradeFieldsFragment, node); - } - } else { - const firstNode = draft[0]?.node; - if (firstNode && node.createdAt >= firstNode.createdAt) { - const { marketId, ...nodeData } = node; - draft.unshift({ - node: { - ...nodeData, - __typename: 'Trade', - market: { - __typename: 'Market', - id: marketId, - }, - }, - cursor: '', - }); - } + const { marketId, ...nodeData } = node; + draft.unshift({ + node: { + ...nodeData, + __typename: 'Trade', + market: { + __typename: 'Market', + id: marketId, + }, + }, + cursor: '', + }); + + if (draft.length > MAX_TRADES) { + draft.pop(); } }); }); @@ -86,7 +77,7 @@ export const tradesProvider = makeDataProvider< pagination: { getPageInfo, append, - first: 100, + first: MAX_TRADES, }, }); @@ -117,7 +108,5 @@ export const tradesWithMarketProvider = makeDerivedDataProvider< node, }; }); - }, - combineDelta['0']>, - combineInsertionData + } ); diff --git a/libs/trades/src/lib/trades-table.tsx b/libs/trades/src/lib/trades-table.tsx index 1c06bdea0..1482dc9b1 100644 --- a/libs/trades/src/lib/trades-table.tsx +++ b/libs/trades/src/lib/trades-table.tsx @@ -51,7 +51,6 @@ export const TradesTable = forwardRef((props, ref) => { return ( data.id} ref={ref} defaultColDef={{ From a01e48d508380f4d400793386ab206888a20244d Mon Sep 17 00:00:00 2001 From: Joe Tsang <30622993+jtsang586@users.noreply.github.com> Date: Tue, 6 Jun 2023 12:12:59 +0100 Subject: [PATCH 03/49] chore(governance): nightly e2e test fixes (#4038) --- .github/workflows/cypress-run.yml | 2 +- .../integration/flow/proposal-details.cy.ts | 2 - .../integration/flow/proposal-enacted.cy.ts | 2 + .../src/integration/flow/proposal-forms.cy.ts | 26 ++- .../src/integration/flow/proposal-list.cy.ts | 7 +- .../src/integration/flow/rewards-flow.cy.ts | 14 +- .../src/integration/flow/staking-flow.cy.ts | 3 + .../flow/token-association-flow.cy.ts | 150 +++++------------- .../src/support/common.functions.ts | 26 +++ .../src/support/governance.functions.ts | 38 ++--- .../src/support/staking.functions.ts | 4 +- .../src/support/wallet-teardown.functions.ts | 59 +++---- 12 files changed, 157 insertions(+), 176 deletions(-) diff --git a/.github/workflows/cypress-run.yml b/.github/workflows/cypress-run.yml index abe346284..31039e427 100644 --- a/.github/workflows/cypress-run.yml +++ b/.github/workflows/cypress-run.yml @@ -20,7 +20,7 @@ jobs: project: ${{ fromJSON(inputs.projects) }} name: ${{ matrix.project }} runs-on: self-hosted-runner - timeout-minutes: 60 + timeout-minutes: 100 steps: # Checks if skip cache was requested - name: Set skip-nx-cache flag diff --git a/apps/governance-e2e/src/integration/flow/proposal-details.cy.ts b/apps/governance-e2e/src/integration/flow/proposal-details.cy.ts index c0ebdd4fa..995288645 100644 --- a/apps/governance-e2e/src/integration/flow/proposal-details.cy.ts +++ b/apps/governance-e2e/src/integration/flow/proposal-details.cy.ts @@ -103,8 +103,6 @@ describe( it('Newly created freeform proposal details - shows proposed and closing dates', function () { const proposalTitle = generateFreeFormProposalTitle(); const proposalTimeStamp = createTenDigitUnixTimeStampForSpecifiedDays(3); - // const currentDate = new Date(createTenDigitUnixTimeStampForSpecifiedDays(0) * 1000) - // const proposedDate = new Date(currentDate.getTime() + 60000) submitUniqueRawProposal({ proposalTitle: proposalTitle, diff --git a/apps/governance-e2e/src/integration/flow/proposal-enacted.cy.ts b/apps/governance-e2e/src/integration/flow/proposal-enacted.cy.ts index 022dbf90d..b03a33056 100644 --- a/apps/governance-e2e/src/integration/flow/proposal-enacted.cy.ts +++ b/apps/governance-e2e/src/integration/flow/proposal-enacted.cy.ts @@ -14,6 +14,7 @@ import { createUpdateNetworkProposalTxBody, createFreeFormProposalTxBody, } from '../../support/proposal.functions'; +import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions'; import { ethereumWalletConnect } from '../../support/wallet-eth.functions'; import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions'; @@ -43,6 +44,7 @@ context( waitForSpinner(); cy.connectVegaWallet(); ethereumWalletConnect(); + ensureSpecifiedUnstakedTokensAreAssociated('1'); navigateTo(navigation.proposals); }); diff --git a/apps/governance-e2e/src/integration/flow/proposal-forms.cy.ts b/apps/governance-e2e/src/integration/flow/proposal-forms.cy.ts index 6cbabacf9..a649fe862 100644 --- a/apps/governance-e2e/src/integration/flow/proposal-forms.cy.ts +++ b/apps/governance-e2e/src/integration/flow/proposal-forms.cy.ts @@ -1,5 +1,6 @@ import { closeDialog, + dissociateFromSecondWalletKey, navigateTo, navigation, turnTelemetryOff, @@ -15,7 +16,11 @@ import { governanceProposalType, voteForProposal, } from '../../support/governance.functions'; -import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions'; +import { + ensureSpecifiedUnstakedTokensAreAssociated, + stakingPageAssociateTokens, + stakingPageDisassociateAllTokens, +} from '../../support/staking.functions'; import { ethereumWalletConnect } from '../../support/wallet-eth.functions'; import { vegaWalletSetSpecifiedApprovalAmount, @@ -197,6 +202,9 @@ context( // 3003-PMAN-001 it('Able to submit valid new market proposal', function () { + // Wait needed for test to pass in CI because of report name discrepancy when time passes + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(5000); goToMakeNewProposal(governanceProposalType.NEW_MARKET); cy.get(newProposalTitle).type('Test new market proposal'); cy.get(newProposalDescription).type('E2E test for proposals'); @@ -248,6 +256,9 @@ context( // Will fail if run after 'Able to submit update market proposal and vote for proposal' // 3002-PROP-022 it('Unable to submit update market proposal without equity-like share in the market', function () { + cy.get('[data-testid="manage-vega-wallet"]:visible').click(); + cy.get('[data-testid="select-keypair-button"]').eq(0).click(); // switch to second wallet pub key + stakingPageAssociateTokens('1'); goToMakeNewProposal(governanceProposalType.UPDATE_MARKET); cy.get(newProposalTitle).type('Test update market proposal - rejected'); cy.get(newProposalDescription).type('E2E test for proposals'); @@ -269,7 +280,11 @@ context( cy.get(newProposalSubmitButton).should('be.visible').click(); cy.contains('Proposal rejected', proposalTimeout).should('be.visible'); validateDialogContentMsg('PROPOSAL_ERROR_INSUFFICIENT_EQUITY_LIKE_SHARE'); - ensureSpecifiedUnstakedTokensAreAssociated('1'); + closeDialog(); + ethereumWalletConnect(); + stakingPageDisassociateAllTokens(); + cy.get('[data-testid="manage-vega-wallet"]:visible').click(); + cy.get('[data-testid="select-keypair-button"]').eq(0).click(); }); // 3002-PROP-020 @@ -525,6 +540,13 @@ context( }); }); + after('Disassociate from second wallet key if present', function () { + cy.reload(); + waitForSpinner(); + ethereumWalletConnect(); + dissociateFromSecondWalletKey(); + }); + function validateDialogContentMsg(expectedMsg: string) { cy.getByTestId('dialog-content') .last() diff --git a/apps/governance-e2e/src/integration/flow/proposal-list.cy.ts b/apps/governance-e2e/src/integration/flow/proposal-list.cy.ts index 4a47ebfdc..a2e2153e4 100644 --- a/apps/governance-e2e/src/integration/flow/proposal-list.cy.ts +++ b/apps/governance-e2e/src/integration/flow/proposal-list.cy.ts @@ -86,11 +86,12 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () { }); it('Newly created proposals list - shows title and portion of summary', function () { - const proposalPath = '/proposals/new-market-raw.json'; - const enactmentTimestamp = createTenDigitUnixTimeStampForSpecifiedDays(3); + const proposalPath = 'src/fixtures/proposals/new-market-raw.json'; + const proposalTimestamp = createTenDigitUnixTimeStampForSpecifiedDays(3); submitUniqueRawProposal({ proposalBody: proposalPath, - enactmentTimestamp: enactmentTimestamp, + enactmentTimestamp: proposalTimestamp, + closingTimestamp: proposalTimestamp, }); // 3001-VOTE-052 // 3001-VOTE-008 // 3001-VOTE-034 diff --git a/apps/governance-e2e/src/integration/flow/rewards-flow.cy.ts b/apps/governance-e2e/src/integration/flow/rewards-flow.cy.ts index a8aee38be..939a700ff 100644 --- a/apps/governance-e2e/src/integration/flow/rewards-flow.cy.ts +++ b/apps/governance-e2e/src/integration/flow/rewards-flow.cy.ts @@ -20,8 +20,10 @@ const vegaAssetAddress = '0x67175Da1D5e966e40D11c4B2519392B2058373de'; const vegaWalletUnstakedBalance = '[data-testid="vega-wallet-balance-unstaked"]'; const rewardsTable = 'epoch-total-rewards-table'; +const rewardsStartEpoch = 380; // Use 30 running locally +const rewardsEndEpoch = 500; // Change to 200 running locally const txTimeout = Cypress.env('txTimeout'); -const rewardsTimeOut = { timeout: 60000 }; +const rewardsTimeOut = { timeout: 5 * 60 * 1000 }; context('rewards - flow', { tags: '@slow' }, function () { before('set up environment to allow rewards', function () { @@ -29,18 +31,22 @@ context('rewards - flow', { tags: '@slow' }, function () { turnTelemetryOff(); cy.visit('/'); waitForSpinner(); - depositAsset(vegaAssetAddress, '1000', 18); ethereumWalletConnect(); cy.connectVegaWallet(); + depositAsset(vegaAssetAddress, '1000', 18); + cy.getByTestId('currency-title', txTimeout).should( + 'contain.text', + 'Collateral' + ); vegaWalletTeardown(); cy.associateTokensToVegaWallet('6000'); - cy.VegaWalletTopUpRewardsPool(30, 200); - navigateTo(navigation.validators); + cy.VegaWalletTopUpRewardsPool(rewardsStartEpoch, rewardsEndEpoch); cy.get(vegaWalletUnstakedBalance, txTimeout).should( 'contain', '6,000.0', txTimeout ); + navigateTo(navigation.validators); clickOnValidatorFromList(0); stakingValidatorPageAddStake('3000'); closeStakingDialog(); diff --git a/apps/governance-e2e/src/integration/flow/staking-flow.cy.ts b/apps/governance-e2e/src/integration/flow/staking-flow.cy.ts index f3aee5c79..dfa8b01bd 100644 --- a/apps/governance-e2e/src/integration/flow/staking-flow.cy.ts +++ b/apps/governance-e2e/src/integration/flow/staking-flow.cy.ts @@ -109,6 +109,7 @@ context( cy.getByTestId(userStake, epochTimeout) .first() .should('have.text', '2.00'); + waitForBeginningOfEpoch(); cy.getByTestId('total-stake').first().realHover(); cy.getByTestId('staked-by-user-tooltip') .first() @@ -379,6 +380,7 @@ context( }); it('Disassociating some tokens - prioritizes unstaked tokens', function () { + vegaWalletSetSpecifiedApprovalAmount('1000'); stakingPageAssociateTokens('3'); verifyUnstakedBalance(3.0); cy.get('button').contains('Select a validator to nominate').click(); @@ -485,6 +487,7 @@ context( }); afterEach('Teardown Wallet', function () { + navigateTo(navigation.home); vegaWalletTeardown(); }); diff --git a/apps/governance-e2e/src/integration/flow/token-association-flow.cy.ts b/apps/governance-e2e/src/integration/flow/token-association-flow.cy.ts index 3727ca52f..9c95d90cc 100644 --- a/apps/governance-e2e/src/integration/flow/token-association-flow.cy.ts +++ b/apps/governance-e2e/src/integration/flow/token-association-flow.cy.ts @@ -24,7 +24,6 @@ const ethWalletContainer = '[data-testid="ethereum-wallet"]'; const vegaWalletAssociatedBalance = '[data-testid="currency-value"]'; const vegaWalletUnstakedBalance = '[data-testid="vega-wallet-balance-unstaked"]'; -const currencyTitle = '[data-testid="currency-title"]:visible'; const txTimeout = Cypress.env('txTimeout'); const vegaWalletPublicKeyShort = Cypress.env('vegaWalletPublicKeyShort'); const ethWalletAssociateButton = '[data-testid="associate-btn"]:visible'; @@ -39,7 +38,7 @@ const associatedKey = '[data-testid="associated-key"]'; const associatedAmount = '[data-testid="associated-amount"]'; const associateCompleteText = '[data-testid="transaction-complete-body"]'; const disassociationWarning = '[data-testid="disassociation-warning"]'; -const vegaWallet = '[data-testid="vega-wallet"]'; +const vegaWallet = 'aside [data-testid="vega-wallet"]'; context( 'Token association flow - with eth and vega wallets connected', @@ -79,27 +78,15 @@ context( //0005-ETXN-003 //0005-ETXN-005 stakingPageAssociateTokens('2', { skipConfirmation: true }); - - cy.get(currencyTitle, txTimeout).should('have.length.above', 4); validateWalletCurrency('Associated', '0.00'); validateWalletCurrency('Pending association', '2.00'); validateWalletCurrency('Total associated after pending', '2.00'); - cy.get(currencyTitle, txTimeout).should('have.length.at.least', 6); - // 0005-ETXN-002 verifyEthWalletAssociatedBalance('2.0'); - verifyEthWalletTotalAssociatedBalance('2.0'); - - cy.get(vegaWallet) - .first() - .within(() => { - cy.get(vegaWalletAssociatedBalance, txTimeout).should( - 'contain', - 2.0 - ); - }); - + cy.get(vegaWallet).within(() => { + cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0); + }); cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', 2.0); }); @@ -114,12 +101,11 @@ context( verifyEthWalletAssociatedBalance('2.0'); verifyEthWalletTotalAssociatedBalance('6,002.00'); cy.get('button').contains('Select a validator to nominate').click(); + cy.getByTestId('epoch-countdown').should('be.visible'); stakingPageDisassociateTokens('2'); - cy.get(currencyTitle, txTimeout).should('have.length.above', 4); validateWalletCurrency('Associated', '2.00'); validateWalletCurrency('Pending association', '2.00'); validateWalletCurrency('Total associated after pending', '0.00'); - cy.get(currencyTitle, txTimeout).should('have.length.at.least', 6); cy.get( '[data-testid="eth-wallet-associated-balances"]:visible', txTimeout @@ -132,38 +118,26 @@ context( stakingPageAssociateTokens('1001', { approve: true }); verifyEthWalletAssociatedBalance('1,001.00'); verifyEthWalletTotalAssociatedBalance('7,001.00'); - cy.get(vegaWallet) - .last() - .within(() => { - cy.get(vegaWalletAssociatedBalance, txTimeout).should( - 'contain', - '1,001.00' - ); - }); + cy.get(vegaWallet).within(() => { + cy.get(vegaWalletAssociatedBalance, txTimeout).should( + 'contain', + '1,001.00' + ); + }); }); it('Able to disassociate a partial amount of tokens currently associated', function () { stakingPageAssociateTokens('2'); - cy.get(vegaWallet) - .first() - .within(() => { - cy.get(vegaWalletAssociatedBalance, txTimeout).should( - 'contain', - 2.0 - ); - }); - + cy.get(vegaWallet).within(() => { + cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0); + }); cy.get('button').contains('Select a validator to nominate').click(); + cy.getByTestId('epoch-countdown').should('be.visible'); stakingPageDisassociateTokens('1'); verifyEthWalletAssociatedBalance('1.0'); - cy.get(vegaWallet) - .first() - .within(() => { - cy.get(vegaWalletAssociatedBalance, txTimeout).should( - 'contain', - 1.0 - ); - }); + cy.get(vegaWallet).within(() => { + cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 1.0); + }); }); it('Able to disassociate all tokens - using max', function () { @@ -171,15 +145,11 @@ context( const warningText = 'Warning: Any tokens that have been nominated to a node will sacrifice rewards they are due for the current epoch. If you do not wish to sacrifice these, you should remove stake from a node at the end of an epoch before disassociation.'; stakingPageAssociateTokens('2'); - cy.get(vegaWallet) - .first() - .within(() => { - cy.get(vegaWalletAssociatedBalance, txTimeout).should( - 'contain', - 2.0 - ); - }); + cy.get(vegaWallet).within(() => { + cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0); + }); cy.get('button').contains('Select a validator to nominate').click(); + cy.getByTestId('epoch-countdown').should('be.visible'); cy.get(ethWalletDissociateButton).click(); cy.get(disassociationWarning).should('contain', warningText); stakingPageDisassociateAllTokens(); @@ -197,14 +167,9 @@ context( 'not.exist' ); }); - cy.get(vegaWallet) - .first() - .within(() => { - cy.get(vegaWalletAssociatedBalance, txTimeout).should( - 'contain', - 0.0 - ); - }); + cy.get(vegaWallet).within(() => { + cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 0.0); + }); }); it('Able to associate and disassociate vesting contract tokens', function () { @@ -219,32 +184,22 @@ context( type: 'contract', skipConfirmation: true, }); - - cy.get(currencyTitle, txTimeout).should('have.length.above', 4); validateWalletCurrency('Associated', '0.00'); validateWalletCurrency('Pending association', '2.00'); validateWalletCurrency('Total associated after pending', '2.00'); - cy.get(currencyTitle, txTimeout).should('have.length.at.least', 6); verifyEthWalletAssociatedBalance('2.0'); verifyEthWalletTotalAssociatedBalance('2.0'); - cy.get(vegaWallet) - .first() - .within(() => { - cy.get(vegaWalletAssociatedBalance, txTimeout).should( - 'contain', - 2.0 - ); - }); + cy.get(vegaWallet).within(() => { + cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0); + }); cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', 2.0); stakingPageDisassociateTokens('1', { type: 'contract', skipConfirmation: true, }); - cy.get(currencyTitle, txTimeout).should('have.length.above', 4); validateWalletCurrency('Associated', '2.00'); validateWalletCurrency('Pending association', '1.00'); validateWalletCurrency('Total associated after pending', '1.00'); - cy.get(currencyTitle, txTimeout).should('have.length.at.least', 6); verifyEthWalletAssociatedBalance('1.0'); verifyEthWalletTotalAssociatedBalance('1.0'); }); @@ -256,6 +211,7 @@ context( // 1004-ASSO-022 stakingPageAssociateTokens('21', { type: 'wallet' }); cy.get('button').contains('Select a validator to nominate').click(); + cy.getByTestId('epoch-countdown').should('be.visible'); stakingPageAssociateTokens('37', { type: 'contract' }); cy.get(vestingContractSection) .first() @@ -275,28 +231,18 @@ context( ); cy.get(associatedAmount, txTimeout).should('contain', 21); }); - cy.get(vegaWallet) - .first() - .within(() => { - cy.get(vegaWalletAssociatedBalance, txTimeout).should( - 'contain', - 58 - ); - }); + cy.get(vegaWallet).within(() => { + cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 58); + }); stakingPageDisassociateTokens('6', { type: 'contract' }); cy.get(vestingContractSection) .first() .within(() => { cy.get(associatedAmount, txTimeout).should('contain', 31); }); - cy.get(vegaWallet) - .first() - .within(() => { - cy.get(vegaWalletAssociatedBalance, txTimeout).should( - 'contain', - 52 - ); - }); + cy.get(vegaWallet).within(() => { + cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 52); + }); navigateTo(navigation.validators); stakingPageDisassociateTokens('9', { type: 'wallet' }); cy.get(vegaInWalletSection) @@ -304,14 +250,9 @@ context( .within(() => { cy.get(associatedAmount, txTimeout).should('contain', 12); }); - cy.get(vegaWallet) - .first() - .within(() => { - cy.get(vegaWalletAssociatedBalance, txTimeout).should( - 'contain', - 43 - ); - }); + cy.get(vegaWallet).within(() => { + cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 43); + }); }); it('Not able to associate more tokens than owned', function () { @@ -328,11 +269,9 @@ context( // 1004-ASSO-004 it('Pending association outside of app is shown', function () { vegaWalletAssociate('2'); - cy.get(currencyTitle, txTimeout).should('have.length.above', 4); validateWalletCurrency('Associated', '0.00'); validateWalletCurrency('Pending association', '2.00'); validateWalletCurrency('Total associated after pending', '2.00'); - cy.get(currencyTitle, txTimeout).should('have.length.at.least', 6); validateWalletCurrency('Associated', '2.00'); }); @@ -341,11 +280,9 @@ context( cy.wrap(validateWalletCurrency('Associated', '2.00')).then(() => { vegaWalletDisassociate('2'); }); - cy.get(currencyTitle, txTimeout).should('have.length.above', 4); validateWalletCurrency('Associated', '2.00'); validateWalletCurrency('Pending association', '2.00'); validateWalletCurrency('Total associated after pending', '0.00'); - cy.get(currencyTitle, txTimeout).should('have.length.at.least', 6); validateWalletCurrency('Associated', '0.00'); }); @@ -364,14 +301,9 @@ context( Cypress.env('vegaWalletPublicKey2') ); stakingPageAssociateTokens('2'); - cy.get(vegaWallet) - .first() - .within(() => { - cy.get(vegaWalletAssociatedBalance, txTimeout).should( - 'contain', - 2.0 - ); - }); + cy.get(vegaWallet).within(() => { + cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0); + }); cy.get(associateCompleteText).should( 'have.text', `Vega key ${Cypress.env( diff --git a/apps/governance-e2e/src/support/common.functions.ts b/apps/governance-e2e/src/support/common.functions.ts index 94a5e3afb..39fc20f6b 100644 --- a/apps/governance-e2e/src/support/common.functions.ts +++ b/apps/governance-e2e/src/support/common.functions.ts @@ -1,8 +1,11 @@ +import { stakingPageDisassociateAllTokens } from './staking.functions'; + const tokenDropDown = 'state-trigger'; const txTimeout = Cypress.env('txTimeout'); export enum navigation { section = 'nav', + home = '[href="/"]', vesting = '[href="/token/redeem"]', validators = '[href="/validators"]', rewards = '[href="/rewards"]', @@ -18,6 +21,7 @@ export function convertTokenValueToNumber(subject: string) { } const topLevelRoutes = [ + navigation.home, navigation.proposals, navigation.validators, navigation.rewards, @@ -97,3 +101,25 @@ export function turnTelemetryOff() { win.localStorage.setItem('vega_telemetry_on', 'false') ); } + +export function dissociateFromSecondWalletKey() { + const secondWalletKey = Cypress.env('vegaWalletPublicKey2Short'); + cy.getByTestId('vega-in-wallet') + .first() + .within(() => { + cy.getByTestId('eth-wallet-associated-balances') + .last() + .within(() => { + cy.getByTestId('associated-key') + .invoke('text') + .as('associatedPubKey'); + }); + }); + cy.get('@associatedPubKey').then((associatedPubKey) => { + if (associatedPubKey == secondWalletKey) { + cy.get('[data-testid="manage-vega-wallet"]:visible').click(); + cy.get('[data-testid="select-keypair-button"]').eq(0).click(); + stakingPageDisassociateAllTokens(); + } + }); +} diff --git a/apps/governance-e2e/src/support/governance.functions.ts b/apps/governance-e2e/src/support/governance.functions.ts index 5809a1e39..dcd7dd7eb 100644 --- a/apps/governance-e2e/src/support/governance.functions.ts +++ b/apps/governance-e2e/src/support/governance.functions.ts @@ -59,29 +59,29 @@ export function submitUniqueRawProposal(proposalFields: { submit?: boolean; }) { goToMakeNewProposal(governanceProposalType.RAW); - let proposalBodyPath = '/proposals/raw.json'; + let proposalBodyPath = 'src/fixtures/proposals/raw.json'; if (proposalFields.proposalBody) { proposalBodyPath = proposalFields.proposalBody; } cy.readFile(proposalBodyPath).then((rawProposal) => { - if (!proposalFields.proposalBody) { - if (proposalFields.proposalTitle) { - rawProposal.rationale.title = proposalFields.proposalTitle; - cy.wrap(proposalFields.proposalTitle).as('proposalTitle'); - } - if (proposalFields.proposalDescription) { - rawProposal.rationale.description = proposalFields.proposalDescription; - } - if (proposalFields.closingTimestamp) { - rawProposal.terms.closingTimestamp = proposalFields.closingTimestamp; - } else { - const minTimeStamp = createTenDigitUnixTimeStampForSpecifiedDays(2); - rawProposal.terms.closingTimestamp = minTimeStamp; - } - if (proposalFields.enactmentTimestamp) { - rawProposal.terms.enactmentTimestamp = - proposalFields.enactmentTimestamp; - } + if (proposalFields.proposalTitle) { + rawProposal.rationale.title = proposalFields.proposalTitle; + cy.wrap(proposalFields.proposalTitle).as('proposalTitle'); + } + if (proposalFields.proposalDescription) { + rawProposal.rationale.description = proposalFields.proposalDescription; + } + if (proposalFields.closingTimestamp) { + rawProposal.terms.closingTimestamp = proposalFields.closingTimestamp; + } else if ( + !proposalFields.closingTimestamp && + !proposalFields.proposalBody + ) { + const minTimeStamp = createTenDigitUnixTimeStampForSpecifiedDays(2); + rawProposal.terms.closingTimestamp = minTimeStamp; + } + if (proposalFields.enactmentTimestamp) { + rawProposal.terms.enactmentTimestamp = proposalFields.enactmentTimestamp; } const proposalPayload = JSON.stringify(rawProposal); diff --git a/apps/governance-e2e/src/support/staking.functions.ts b/apps/governance-e2e/src/support/staking.functions.ts index c016a37e0..c7b88e5af 100644 --- a/apps/governance-e2e/src/support/staking.functions.ts +++ b/apps/governance-e2e/src/support/staking.functions.ts @@ -236,8 +236,8 @@ export function validateWalletCurrency( currencyTitle: string, expectedAmount: string ) { - cy.get("[data-testid='currency-title']") - .contains(currencyTitle) + cy.get("[data-testid='currency-title']", txTimeout) + .contains(currencyTitle, txTimeout) .parent() .parent() .within(() => { diff --git a/apps/governance-e2e/src/support/wallet-teardown.functions.ts b/apps/governance-e2e/src/support/wallet-teardown.functions.ts index 13883849a..1753958ce 100644 --- a/apps/governance-e2e/src/support/wallet-teardown.functions.ts +++ b/apps/governance-e2e/src/support/wallet-teardown.functions.ts @@ -19,7 +19,7 @@ const ethStakingBridgeContractAddress = Cypress.env( ); const ethProviderUrl = Cypress.env('ethProviderUrl'); const getAccount = (number = 0) => `m/44'/60'/0'/0/${number}`; -const transactionTimeout = 100000; +const transactionTimeout = { timeout: 100000, log: false }; const Erc20BridgeAddress = '0x9708FF7510D4A7B9541e1699d15b53Ecb1AFDc54'; const provider = new ethers.providers.JsonRpcProvider({ url: ethProviderUrl }); @@ -43,10 +43,7 @@ export async function depositAsset( const faucet = new Token(assetEthAddress, signer); cy.wrap( faucet.approve(Erc20BridgeAddress, amount + '0'.repeat(decimalPlaces + 1)), - { - timeout: transactionTimeout, - log: false, - } + transactionTimeout ).then(() => { const collateralBridge = new CollateralBridge(Erc20BridgeAddress, signer); cy.wrap( @@ -55,7 +52,7 @@ export async function depositAsset( amount + '0'.repeat(decimalPlaces), '0x' + vegaWalletPubKey ), - { timeout: transactionTimeout, log: false } + transactionTimeout ); }); } @@ -79,13 +76,13 @@ export async function vegaWalletTeardown() { } }); cy.get(vegaWalletContainer).within(() => { - cy.get(associatedAmountInWallet, { - timeout: transactionTimeout, - }) - .should('have.length', 1, { timeout: transactionTimeout }) - .contains('0.00', { - timeout: transactionTimeout, - }); + cy.get(associatedAmountInWallet, transactionTimeout).should( + 'have.length', + 1 + ); + cy.get(associatedAmountInWallet) + .first(transactionTimeout) + .should('have.text', '0.00'); }); }); } @@ -109,7 +106,7 @@ async function vegaWalletTeardownStaking(stakingBridgeContract: StakingBridge) { cy.highlight('Tearing down staking tokens from vega wallet if present'); cy.wrap( stakingBridgeContract.stake_balance(ethWalletPubKey, vegaWalletPubKey), - { timeout: transactionTimeout } + transactionTimeout ).then((stakeBalance) => { if (Number(stakeBalance) != 0) { cy.get(vegaWalletContainer).within(() => { @@ -122,31 +119,25 @@ async function vegaWalletTeardownStaking(stakingBridgeContract: StakingBridge) { String(stakeBalance), vegaWalletPubKey ), - { timeout: transactionTimeout } + transactionTimeout ); cy.wrap( vestingContract.stake_balance(ethWalletPubKey, vegaWalletPubKey), - { - timeout: transactionTimeout, - log: false, - } + transactionTimeout ).then((vestingAmount) => { if (Number(vestingAmount) != 0) { - cy.contains('Associated', { - timeout: transactionTimeout, - }) + cy.contains('Associated', transactionTimeout) .parent() .parent() .within(() => { - cy.getByTestId('currency-value', { - timeout: transactionTimeout, - }) - .should('have.length', 1) + cy.getByTestId('currency-value', transactionTimeout) + .first() .invoke('text') .as('displayedAmount'); - cy.get('@displayedAmount', { - timeout: transactionTimeout, - }).should('not.eq', $associatedAmount); + cy.get('@displayedAmount', transactionTimeout).should( + 'not.eq', + $associatedAmount + ); }); } }); @@ -158,14 +149,14 @@ async function vegaWalletTeardownStaking(stakingBridgeContract: StakingBridge) { async function vegaWalletTeardownVesting(vestingContract: TokenVesting) { cy.highlight('Tearing down vesting tokens from vega wallet if present'); - cy.wrap(vestingContract.stake_balance(ethWalletPubKey, vegaWalletPubKey), { - timeout: transactionTimeout, - log: false, - }).then((vestingAmount) => { + cy.wrap( + vestingContract.stake_balance(ethWalletPubKey, vegaWalletPubKey), + transactionTimeout + ).then((vestingAmount) => { if (Number(vestingAmount) != 0) { cy.wrap( vestingContract.remove_stake(String(vestingAmount), vegaWalletPubKey), - { timeout: transactionTimeout } + transactionTimeout ); } }); From e054db39b5ea5233726eb2a90eb50c9b6a69018c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20M=C5=82odzikowski?= Date: Tue, 6 Jun 2023 13:47:42 +0200 Subject: [PATCH 04/49] feat(ci): add deploys for multisig-signer (#4039) --- .github/workflows/ci-cd-trigger.yml | 23 ++++++++++++++++++++--- .github/workflows/publish-dist.yml | 4 ++++ docker/nginx.conf | 1 + 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-cd-trigger.yml b/.github/workflows/ci-cd-trigger.yml index 8d222b90c..67116dfdf 100644 --- a/.github/workflows/ci-cd-trigger.yml +++ b/.github/workflows/ci-cd-trigger.yml @@ -113,15 +113,16 @@ jobs: preview_governance="not deployed" preview_trading="not deployed" preview_explorer="not deployed" - if [[ $affected == *"governance"* ]]; then + preview_tools="not deployed" + if [[ $affected =~ *"governance"* ]]; then projects_e2e+='"governance-e2e" ' preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug") fi - if [[ $affected == *"trading"* ]]; then + if [[ $affected =~ *"trading"* ]]; then projects_e2e+='"trading-e2e" ' preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug") fi - if [[ $affected == *"explorer"* ]]; then + if [[ $affected =~ *"explorer"* ]]; then projects_e2e+='"explorer-e2e" ' preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug") fi @@ -135,9 +136,17 @@ jobs: projects_e2e=[${projects_e2e// /,}] echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV echo PROJECTS=$(echo $projects_e2e | sed 's|-e2e||g') >> $GITHUB_ENV + if [[ $affected =~ *"mutlisig-signer"* ]]; then + # tools are only applicable to check previews or deploy from develop to mainnet + if [[ "${{ github.event_name }}" = "pull_request" ]] || [[ "${{ github.ref_name }}" = "develop" ]]; then + preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug") + PROJECTS+='"multisig-signer" ' + fi + fi echo PREVIEW_GOVERNANCE=$preview_governance >> $GITHUB_ENV echo PREVIEW_TRADING=$preview_trading >> $GITHUB_ENV echo PREVIEW_EXPLORER=$preview_explorer >> $GITHUB_ENV + echo PREVIEW_TOOLS=$preview_tools >> $GITHUB_ENV outputs: projects: ${{ env.PROJECTS }} @@ -145,6 +154,7 @@ jobs: preview_governance: ${{ env.PREVIEW_GOVERNANCE }} preview_trading: ${{ env.PREVIEW_TRADING }} preview_explorer: ${{ env.PREVIEW_EXPLORER }} + preview_tools: ${{ env.PREVIEW_TOOLS }} cypress: needs: lint-test-build @@ -203,6 +213,12 @@ jobs: sleep 5 done fi + if [[ "${{ needs.lint-test-build.outputs.preview_tools }}" =~ $regex ]]; then + until curl -L --fail "${{ needs.lint-test-build.outputs.preview_tools }}"; do + echo "waiting for tools preview" + sleep 5 + done + fi - name: Create comment uses: peter-evans/create-or-update-comment@v3 @@ -214,6 +230,7 @@ jobs: * governance: ${{ needs.lint-test-build.outputs.preview_governance }} * explorer: ${{ needs.lint-test-build.outputs.preview_explorer }} * trading: ${{ needs.lint-test-build.outputs.preview_trading }} + * tools: ${{ needs.lint-test-build.outputs.preview_tools }} # Report single result at the end, to avoid mess with required checks in PR cypress-check: diff --git a/.github/workflows/publish-dist.yml b/.github/workflows/publish-dist.yml index c7183f286..1b4a98038 100644 --- a/.github/workflows/publish-dist.yml +++ b/.github/workflows/publish-dist.yml @@ -70,6 +70,10 @@ jobs: envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)" elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then envName="stagnet1" + if [[ "${matrix.app}" = "multisig-signer" ]]; then + envName="mainnet" + bucketName="tools.vega.xyz" + fi elif [[ "${{ github.ref }}" =~ .*main$ ]]; then envName="mainnet" elif [[ "${{ matrix.app}}" = "trading" ]] && [[ "${{ startsWith(github.ref, 'refs/tags/v') && 'true' || 'false' }}" = "true" ]]; then diff --git a/docker/nginx.conf b/docker/nginx.conf index 40a878bce..fd15608db 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -3,6 +3,7 @@ server { listen 80; location / { + add_header 'Cache-Control' 'max-age=60'; root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; From fdcd24847c857733ce935a934d34748e861ca13f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20M=C5=82odzikowski?= Date: Tue, 6 Jun 2023 14:07:56 +0200 Subject: [PATCH 05/49] fix(ci): syntax for publishing dist --- .github/workflows/publish-dist.yml | 2 +- apps/multisig-signer/.env.mainnet | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-dist.yml b/.github/workflows/publish-dist.yml index 1b4a98038..3defc8474 100644 --- a/.github/workflows/publish-dist.yml +++ b/.github/workflows/publish-dist.yml @@ -70,7 +70,7 @@ jobs: envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)" elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then envName="stagnet1" - if [[ "${matrix.app}" = "multisig-signer" ]]; then + if [[ "${{ matrix.app }}" = "multisig-signer" ]]; then envName="mainnet" bucketName="tools.vega.xyz" fi diff --git a/apps/multisig-signer/.env.mainnet b/apps/multisig-signer/.env.mainnet index 3c2646f18..3ee5b5286 100644 --- a/apps/multisig-signer/.env.mainnet +++ b/apps/multisig-signer/.env.mainnet @@ -3,3 +3,5 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/maste NX_VEGA_URL=https://api.vega.community/graphql NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}' NX_VEGA_ENV=MAINNET + + From e8ae085c06b4fee3740cd2cea02cc6e484be6202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20M=C5=82odzikowski?= Date: Tue, 6 Jun 2023 14:18:47 +0200 Subject: [PATCH 06/49] fix(ci): typo in app name --- .github/workflows/ci-cd-trigger.yml | 2 +- apps/multisig-signer/.env.mainnet | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci-cd-trigger.yml b/.github/workflows/ci-cd-trigger.yml index 67116dfdf..0d8b17511 100644 --- a/.github/workflows/ci-cd-trigger.yml +++ b/.github/workflows/ci-cd-trigger.yml @@ -136,7 +136,7 @@ jobs: projects_e2e=[${projects_e2e// /,}] echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV echo PROJECTS=$(echo $projects_e2e | sed 's|-e2e||g') >> $GITHUB_ENV - if [[ $affected =~ *"mutlisig-signer"* ]]; then + if [[ $affected =~ *"multisig-signer"* ]]; then # tools are only applicable to check previews or deploy from develop to mainnet if [[ "${{ github.event_name }}" = "pull_request" ]] || [[ "${{ github.ref_name }}" = "develop" ]]; then preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug") diff --git a/apps/multisig-signer/.env.mainnet b/apps/multisig-signer/.env.mainnet index 3ee5b5286..3c2646f18 100644 --- a/apps/multisig-signer/.env.mainnet +++ b/apps/multisig-signer/.env.mainnet @@ -3,5 +3,3 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/maste NX_VEGA_URL=https://api.vega.community/graphql NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}' NX_VEGA_ENV=MAINNET - - From b8309a76e7704226fe90da1c56370f1c97442651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20M=C5=82odzikowski?= Date: Tue, 6 Jun 2023 14:29:46 +0200 Subject: [PATCH 07/49] fix(ci): way of resolving affected --- .github/workflows/ci-cd-trigger.yml | 12 ++++++++---- apps/multisig-signer/.env.mainnet | 3 +++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-cd-trigger.yml b/.github/workflows/ci-cd-trigger.yml index 0d8b17511..e16a4cc68 100644 --- a/.github/workflows/ci-cd-trigger.yml +++ b/.github/workflows/ci-cd-trigger.yml @@ -114,15 +114,18 @@ jobs: preview_trading="not deployed" preview_explorer="not deployed" preview_tools="not deployed" - if [[ $affected =~ *"governance"* ]]; then + if echo "$affected" | grep -q governance; then + echo "Governance is affected" projects_e2e+='"governance-e2e" ' preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug") fi - if [[ $affected =~ *"trading"* ]]; then + if echo "$affected" | grep -q trading; then + echo "Trading is affected" projects_e2e+='"trading-e2e" ' preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug") fi - if [[ $affected =~ *"explorer"* ]]; then + if echo "$affected" | grep -q explorer; then + echo "Explorer is affected" projects_e2e+='"explorer-e2e" ' preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug") fi @@ -136,7 +139,8 @@ jobs: projects_e2e=[${projects_e2e// /,}] echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV echo PROJECTS=$(echo $projects_e2e | sed 's|-e2e||g') >> $GITHUB_ENV - if [[ $affected =~ *"multisig-signer"* ]]; then + if echo "$affected" | grep -q multisig-signer; then + echo "Tools are affected" # tools are only applicable to check previews or deploy from develop to mainnet if [[ "${{ github.event_name }}" = "pull_request" ]] || [[ "${{ github.ref_name }}" = "develop" ]]; then preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug") diff --git a/apps/multisig-signer/.env.mainnet b/apps/multisig-signer/.env.mainnet index 3c2646f18..2a65459fa 100644 --- a/apps/multisig-signer/.env.mainnet +++ b/apps/multisig-signer/.env.mainnet @@ -3,3 +3,6 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/maste NX_VEGA_URL=https://api.vega.community/graphql NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}' NX_VEGA_ENV=MAINNET + + + From 5b02fd5d54796f8a604585868d9f219216856779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20M=C5=82odzikowski?= Date: Tue, 6 Jun 2023 14:39:28 +0200 Subject: [PATCH 08/49] fix(ci): way of resolving affected --- .github/workflows/ci-cd-trigger.yml | 7 ++++++- apps/multisig-signer/.env.mainnet | 3 --- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-cd-trigger.yml b/.github/workflows/ci-cd-trigger.yml index e16a4cc68..f2115f946 100644 --- a/.github/workflows/ci-cd-trigger.yml +++ b/.github/workflows/ci-cd-trigger.yml @@ -142,10 +142,15 @@ jobs: if echo "$affected" | grep -q multisig-signer; then echo "Tools are affected" # tools are only applicable to check previews or deploy from develop to mainnet - if [[ "${{ github.event_name }}" = "pull_request" ]] || [[ "${{ github.ref_name }}" = "develop" ]]; then + if [[ "${{ github.event_name }}" = "pull_request" ]]; then + echo "Deploying tools on preview" preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug") PROJECTS+='"multisig-signer" ' fi + if [[ "${{ github.ref }}" =~ .*develop$ ]]; then + echo "Deploying tools on s3" + PROJECTS+='"multisig-signer" ' + fi fi echo PREVIEW_GOVERNANCE=$preview_governance >> $GITHUB_ENV echo PREVIEW_TRADING=$preview_trading >> $GITHUB_ENV diff --git a/apps/multisig-signer/.env.mainnet b/apps/multisig-signer/.env.mainnet index 2a65459fa..3c2646f18 100644 --- a/apps/multisig-signer/.env.mainnet +++ b/apps/multisig-signer/.env.mainnet @@ -3,6 +3,3 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/maste NX_VEGA_URL=https://api.vega.community/graphql NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}' NX_VEGA_ENV=MAINNET - - - From 429a5a23d3f237bffcbb5e3aed8e16f356323a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20M=C5=82odzikowski?= Date: Tue, 6 Jun 2023 14:48:49 +0200 Subject: [PATCH 09/49] fix(ci): way of resolving affected --- .github/workflows/ci-cd-trigger.yml | 11 ++++++----- apps/multisig-signer/.env.mainnet | 2 ++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-cd-trigger.yml b/.github/workflows/ci-cd-trigger.yml index f2115f946..f69b7dbc0 100644 --- a/.github/workflows/ci-cd-trigger.yml +++ b/.github/workflows/ci-cd-trigger.yml @@ -137,21 +137,22 @@ jobs: fi projects_e2e=${projects_e2e%?} projects_e2e=[${projects_e2e// /,}] - echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV - echo PROJECTS=$(echo $projects_e2e | sed 's|-e2e||g') >> $GITHUB_ENV + projects=$(echo $projects_e2e | sed 's|-e2e||g') if echo "$affected" | grep -q multisig-signer; then echo "Tools are affected" # tools are only applicable to check previews or deploy from develop to mainnet if [[ "${{ github.event_name }}" = "pull_request" ]]; then echo "Deploying tools on preview" preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug") - PROJECTS+='"multisig-signer" ' + projects+='"multisig-signer" ' fi if [[ "${{ github.ref }}" =~ .*develop$ ]]; then echo "Deploying tools on s3" - PROJECTS+='"multisig-signer" ' + projects+='"multisig-signer" ' fi fi + echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV + echo PROJECTS=$projects >> $GITHUB_ENV echo PREVIEW_GOVERNANCE=$preview_governance >> $GITHUB_ENV echo PREVIEW_TRADING=$preview_trading >> $GITHUB_ENV echo PREVIEW_EXPLORER=$preview_explorer >> $GITHUB_ENV @@ -168,7 +169,7 @@ jobs: cypress: needs: lint-test-build name: '(CI) cypress' - if: ${{ needs.lint-test-build.outputs.projects != '[]' }} + if: ${{ needs.lint-test-build.outputs.projects-e2e != '[]' }} uses: ./.github/workflows/cypress-run.yml secrets: inherit with: diff --git a/apps/multisig-signer/.env.mainnet b/apps/multisig-signer/.env.mainnet index 3c2646f18..3ee5b5286 100644 --- a/apps/multisig-signer/.env.mainnet +++ b/apps/multisig-signer/.env.mainnet @@ -3,3 +3,5 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/maste NX_VEGA_URL=https://api.vega.community/graphql NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}' NX_VEGA_ENV=MAINNET + + From b3036d520f7217a3238e7f88a6638c681ff966ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20M=C5=82odzikowski?= Date: Tue, 6 Jun 2023 15:03:18 +0200 Subject: [PATCH 10/49] fix(ci): way of resolving affected --- .github/workflows/ci-cd-trigger.yml | 3 ++- apps/multisig-signer/.env.mainnet | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-cd-trigger.yml b/.github/workflows/ci-cd-trigger.yml index f69b7dbc0..b319d66ab 100644 --- a/.github/workflows/ci-cd-trigger.yml +++ b/.github/workflows/ci-cd-trigger.yml @@ -136,7 +136,6 @@ jobs: preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug") fi projects_e2e=${projects_e2e%?} - projects_e2e=[${projects_e2e// /,}] projects=$(echo $projects_e2e | sed 's|-e2e||g') if echo "$affected" | grep -q multisig-signer; then echo "Tools are affected" @@ -151,6 +150,8 @@ jobs: projects+='"multisig-signer" ' fi fi + projects_e2e=[${projects_e2e// /,}] + projects=[${projects// /,}] echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV echo PROJECTS=$projects >> $GITHUB_ENV echo PREVIEW_GOVERNANCE=$preview_governance >> $GITHUB_ENV diff --git a/apps/multisig-signer/.env.mainnet b/apps/multisig-signer/.env.mainnet index 3ee5b5286..3c2646f18 100644 --- a/apps/multisig-signer/.env.mainnet +++ b/apps/multisig-signer/.env.mainnet @@ -3,5 +3,3 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/maste NX_VEGA_URL=https://api.vega.community/graphql NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}' NX_VEGA_ENV=MAINNET - - From 1521bab4c47ff64a3eabf101c491c69c0a3cb7c7 Mon Sep 17 00:00:00 2001 From: Ciaran McGhie Date: Tue, 6 Jun 2023 14:10:03 +0100 Subject: [PATCH 11/49] chore(react-helpers,utils,logger): create logger lib and move sentry/logger utils there (#3990) --- .../lib/hooks/use-telemetry-approval.spec.ts | 4 +-- .../lib/hooks/use-telemetry-approval.ts | 2 +- apps/trading/sentry.client.config.js | 2 +- libs/apollo-client/src/lib/apollo-client.ts | 2 +- libs/deposits/src/lib/use-deposit-balances.ts | 3 +- libs/deposits/src/lib/use-get-allowance.ts | 3 +- .../src/lib/use-get-deposit-maximum.ts | 3 +- .../src/lib/use-get-deposited-amount.ts | 3 +- libs/environment/src/hooks/use-releases.ts | 2 +- libs/logger/.babelrc | 12 +++++++ libs/logger/.eslintrc.json | 18 ++++++++++ libs/logger/README.md | 7 ++++ libs/logger/jest.config.ts | 10 ++++++ libs/logger/package.json | 5 +++ libs/logger/project.json | 34 +++++++++++++++++++ libs/logger/src/hooks/index.ts | 1 + .../src/hooks/use-logger.ts | 5 +-- libs/logger/src/index.ts | 2 ++ libs/logger/src/lib/index.ts | 2 ++ .../src/lib/local-logger.spec.ts | 0 .../{utils => logger}/src/lib/local-logger.ts | 0 .../src/lib/sentry-utils.spec.ts | 0 .../{utils => logger}/src/lib/sentry-utils.ts | 0 libs/logger/tsconfig.json | 23 +++++++++++++ libs/logger/tsconfig.lib.json | 10 ++++++ libs/logger/tsconfig.spec.json | 20 +++++++++++ libs/react-helpers/.eslintrc.json | 24 ++++++++----- libs/react-helpers/src/hooks/index.ts | 1 - .../src/hooks/use-mutation-observer.ts | 7 +--- .../src/hooks/use-resize-observer.ts | 7 +--- libs/ui-toolkit/.eslintrc.json | 10 +++++- libs/utils/.eslintrc.json | 24 ++++++++----- libs/utils/src/index.ts | 2 -- libs/web3/src/lib/use-eager-connect.ts | 7 ++-- ...se-ethereum-withdraw-approvals-manager.tsx | 2 +- libs/web3/src/lib/use-get-withdraw-delay.ts | 2 +- .../src/lib/use-complete-withdraw.ts | 2 +- .../src/lib/use-verify-withdrawal.ts | 3 +- libs/withdraws/src/lib/use-withdraw-asset.tsx | 3 +- tsconfig.base.json | 1 + workspace.json | 1 + 41 files changed, 215 insertions(+), 54 deletions(-) create mode 100644 libs/logger/.babelrc create mode 100644 libs/logger/.eslintrc.json create mode 100644 libs/logger/README.md create mode 100644 libs/logger/jest.config.ts create mode 100644 libs/logger/package.json create mode 100644 libs/logger/project.json create mode 100644 libs/logger/src/hooks/index.ts rename libs/{react-helpers => logger}/src/hooks/use-logger.ts (68%) create mode 100644 libs/logger/src/index.ts create mode 100644 libs/logger/src/lib/index.ts rename libs/{utils => logger}/src/lib/local-logger.spec.ts (100%) rename libs/{utils => logger}/src/lib/local-logger.ts (100%) rename libs/{utils => logger}/src/lib/sentry-utils.spec.ts (100%) rename libs/{utils => logger}/src/lib/sentry-utils.ts (100%) create mode 100644 libs/logger/tsconfig.json create mode 100644 libs/logger/tsconfig.lib.json create mode 100644 libs/logger/tsconfig.spec.json diff --git a/apps/trading/lib/hooks/use-telemetry-approval.spec.ts b/apps/trading/lib/hooks/use-telemetry-approval.spec.ts index 6a4c9f00d..97ec110d6 100644 --- a/apps/trading/lib/hooks/use-telemetry-approval.spec.ts +++ b/apps/trading/lib/hooks/use-telemetry-approval.spec.ts @@ -1,11 +1,11 @@ import { renderHook, act, waitFor } from '@testing-library/react'; import { useLocalStorage } from '@vegaprotocol/react-helpers'; -import { SentryInit, SentryClose } from '@vegaprotocol/utils'; +import { SentryInit, SentryClose } from '@vegaprotocol/logger'; import { STORAGE_KEY, useTelemetryApproval } from './use-telemetry-approval'; const mockSetValue = jest.fn(); const mockRemoveValue = jest.fn(); -jest.mock('@vegaprotocol/utils'); +jest.mock('@vegaprotocol/logger'); jest.mock('@vegaprotocol/react-helpers', () => ({ ...jest.requireActual('@vegaprotocol/react-helpers'), useLocalStorage: jest diff --git a/apps/trading/lib/hooks/use-telemetry-approval.ts b/apps/trading/lib/hooks/use-telemetry-approval.ts index 21e6a2469..d469e58ac 100644 --- a/apps/trading/lib/hooks/use-telemetry-approval.ts +++ b/apps/trading/lib/hooks/use-telemetry-approval.ts @@ -1,6 +1,6 @@ import { useLocalStorage } from '@vegaprotocol/react-helpers'; import { useCallback } from 'react'; -import { SentryInit, SentryClose } from '@vegaprotocol/utils'; +import { SentryInit, SentryClose } from '@vegaprotocol/logger'; import { ENV } from '../config'; export const STORAGE_KEY = 'vega_telemetry_approval'; diff --git a/apps/trading/sentry.client.config.js b/apps/trading/sentry.client.config.js index 7b5a79bb5..9ecc813be 100644 --- a/apps/trading/sentry.client.config.js +++ b/apps/trading/sentry.client.config.js @@ -1,5 +1,5 @@ import { ENV } from './lib/config/env'; -import { LocalStorage, SentryInit } from '@vegaprotocol/utils'; +import { LocalStorage, SentryInit } from '@vegaprotocol/logger'; import { STORAGE_KEY } from './lib/hooks/use-telemetry-approval'; const { dsn, envName } = ENV; diff --git a/libs/apollo-client/src/lib/apollo-client.ts b/libs/apollo-client/src/lib/apollo-client.ts index 0aac83e8e..c812ab472 100644 --- a/libs/apollo-client/src/lib/apollo-client.ts +++ b/libs/apollo-client/src/lib/apollo-client.ts @@ -13,7 +13,7 @@ import { createClient as createWSClient } from 'graphql-ws'; import { onError } from '@apollo/client/link/error'; import { RetryLink } from '@apollo/client/link/retry'; import ApolloLinkTimeout from 'apollo-link-timeout'; -import { localLoggerFactory } from '@vegaprotocol/utils'; +import { localLoggerFactory } from '@vegaprotocol/logger'; import { useHeaderStore } from './header-store'; const isBrowser = typeof window !== 'undefined'; diff --git a/libs/deposits/src/lib/use-deposit-balances.ts b/libs/deposits/src/lib/use-deposit-balances.ts index e8bb31c8c..8ceb02b17 100644 --- a/libs/deposits/src/lib/use-deposit-balances.ts +++ b/libs/deposits/src/lib/use-deposit-balances.ts @@ -8,7 +8,8 @@ import { useIsExemptDepositor, } from './use-get-deposit-maximum'; import { useGetDepositedAmount } from './use-get-deposited-amount'; -import { isAssetTypeERC20, localLoggerFactory } from '@vegaprotocol/utils'; +import { isAssetTypeERC20 } from '@vegaprotocol/utils'; +import { localLoggerFactory } from '@vegaprotocol/logger'; import { useAccountBalance } from '@vegaprotocol/accounts'; import type { Asset } from '@vegaprotocol/assets'; import { useWeb3React } from '@web3-react/core'; diff --git a/libs/deposits/src/lib/use-get-allowance.ts b/libs/deposits/src/lib/use-get-allowance.ts index 53548a55b..dd6ffa07e 100644 --- a/libs/deposits/src/lib/use-get-allowance.ts +++ b/libs/deposits/src/lib/use-get-allowance.ts @@ -4,7 +4,8 @@ import { useCallback } from 'react'; import { useEthereumConfig } from '@vegaprotocol/web3'; import BigNumber from 'bignumber.js'; import type { Asset } from '@vegaprotocol/assets'; -import { addDecimal, localLoggerFactory } from '@vegaprotocol/utils'; +import { addDecimal } from '@vegaprotocol/utils'; +import { localLoggerFactory } from '@vegaprotocol/logger'; export const useGetAllowance = ( contract: Token | null, diff --git a/libs/deposits/src/lib/use-get-deposit-maximum.ts b/libs/deposits/src/lib/use-get-deposit-maximum.ts index 7aa027e09..d680a3ad8 100644 --- a/libs/deposits/src/lib/use-get-deposit-maximum.ts +++ b/libs/deposits/src/lib/use-get-deposit-maximum.ts @@ -1,7 +1,8 @@ import { useCallback } from 'react'; import BigNumber from 'bignumber.js'; import type { Asset } from '@vegaprotocol/assets'; -import { addDecimal, localLoggerFactory } from '@vegaprotocol/utils'; +import { addDecimal } from '@vegaprotocol/utils'; +import { localLoggerFactory } from '@vegaprotocol/logger'; import type { CollateralBridge } from '@vegaprotocol/smart-contracts'; export const useGetDepositMaximum = ( diff --git a/libs/deposits/src/lib/use-get-deposited-amount.ts b/libs/deposits/src/lib/use-get-deposited-amount.ts index d8814b91a..5edea56a9 100644 --- a/libs/deposits/src/lib/use-get-deposited-amount.ts +++ b/libs/deposits/src/lib/use-get-deposited-amount.ts @@ -3,7 +3,8 @@ import { ethers } from 'ethers'; import { useEthereumConfig } from '@vegaprotocol/web3'; import BigNumber from 'bignumber.js'; import type { Asset } from '@vegaprotocol/assets'; -import { addDecimal, localLoggerFactory } from '@vegaprotocol/utils'; +import { addDecimal } from '@vegaprotocol/utils'; +import { localLoggerFactory } from '@vegaprotocol/logger'; import { useWeb3React } from '@web3-react/core'; export const useGetDepositedAmount = (asset: Asset | undefined) => { diff --git a/libs/environment/src/hooks/use-releases.ts b/libs/environment/src/hooks/use-releases.ts index bb10de383..6d5ffc701 100644 --- a/libs/environment/src/hooks/use-releases.ts +++ b/libs/environment/src/hooks/use-releases.ts @@ -1,4 +1,4 @@ -import { localLoggerFactory } from '@vegaprotocol/utils'; +import { localLoggerFactory } from '@vegaprotocol/logger'; import { useCallback, useEffect, useState } from 'react'; import z from 'zod'; diff --git a/libs/logger/.babelrc b/libs/logger/.babelrc new file mode 100644 index 000000000..ccae900be --- /dev/null +++ b/libs/logger/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nrwl/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/logger/.eslintrc.json b/libs/logger/.eslintrc.json new file mode 100644 index 000000000..734ddacee --- /dev/null +++ b/libs/logger/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/logger/README.md b/libs/logger/README.md new file mode 100644 index 000000000..116bb4a11 --- /dev/null +++ b/libs/logger/README.md @@ -0,0 +1,7 @@ +# logger + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test logger` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/logger/jest.config.ts b/libs/logger/jest.config.ts new file mode 100644 index 000000000..08ccc7749 --- /dev/null +++ b/libs/logger/jest.config.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +export default { + displayName: 'logger', + preset: '../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': 'babel-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/logger', +}; diff --git a/libs/logger/package.json b/libs/logger/package.json new file mode 100644 index 000000000..52aafb7dd --- /dev/null +++ b/libs/logger/package.json @@ -0,0 +1,5 @@ +{ + "name": "@vegaprotocol/logger", + "version": "0.0.1", + "type": "commonjs" +} diff --git a/libs/logger/project.json b/libs/logger/project.json new file mode 100644 index 000000000..8a501a8ea --- /dev/null +++ b/libs/logger/project.json @@ -0,0 +1,34 @@ +{ + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/logger/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "format": ["esm", "cjs"], + "options": { + "outputPath": "dist/libs/logger", + "main": "libs/logger/src/index.ts", + "tsConfig": "libs/logger/tsconfig.lib.json", + "assets": ["libs/logger/*.md"] + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/logger/**/*.{ts,tsx,js,jsx}"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/logger"], + "options": { + "jestConfig": "libs/logger/jest.config.ts", + "passWithNoTests": true + } + } + } +} diff --git a/libs/logger/src/hooks/index.ts b/libs/logger/src/hooks/index.ts new file mode 100644 index 000000000..bb4bdc019 --- /dev/null +++ b/libs/logger/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './use-logger'; diff --git a/libs/react-helpers/src/hooks/use-logger.ts b/libs/logger/src/hooks/use-logger.ts similarity index 68% rename from libs/react-helpers/src/hooks/use-logger.ts rename to libs/logger/src/hooks/use-logger.ts index 759d3f3b8..e4837d579 100644 --- a/libs/react-helpers/src/hooks/use-logger.ts +++ b/libs/logger/src/hooks/use-logger.ts @@ -1,6 +1,7 @@ import { useRef } from 'react'; -import type { LocalLogger, LoggerConf } from '@vegaprotocol/utils'; -import { localLoggerFactory, SentryInit } from '@vegaprotocol/utils'; +import type { LocalLogger, LoggerConf } from '../lib/local-logger'; +import { localLoggerFactory } from '../lib/local-logger'; +import { SentryInit } from '../lib/sentry-utils'; export interface LoggerProps extends LoggerConf { dsn?: string; diff --git a/libs/logger/src/index.ts b/libs/logger/src/index.ts new file mode 100644 index 000000000..6a96501a1 --- /dev/null +++ b/libs/logger/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib'; +export * from './hooks'; diff --git a/libs/logger/src/lib/index.ts b/libs/logger/src/lib/index.ts new file mode 100644 index 000000000..8c93e3456 --- /dev/null +++ b/libs/logger/src/lib/index.ts @@ -0,0 +1,2 @@ +export * from './local-logger'; +export * from './sentry-utils'; diff --git a/libs/utils/src/lib/local-logger.spec.ts b/libs/logger/src/lib/local-logger.spec.ts similarity index 100% rename from libs/utils/src/lib/local-logger.spec.ts rename to libs/logger/src/lib/local-logger.spec.ts diff --git a/libs/utils/src/lib/local-logger.ts b/libs/logger/src/lib/local-logger.ts similarity index 100% rename from libs/utils/src/lib/local-logger.ts rename to libs/logger/src/lib/local-logger.ts diff --git a/libs/utils/src/lib/sentry-utils.spec.ts b/libs/logger/src/lib/sentry-utils.spec.ts similarity index 100% rename from libs/utils/src/lib/sentry-utils.spec.ts rename to libs/logger/src/lib/sentry-utils.spec.ts diff --git a/libs/utils/src/lib/sentry-utils.ts b/libs/logger/src/lib/sentry-utils.ts similarity index 100% rename from libs/utils/src/lib/sentry-utils.ts rename to libs/logger/src/lib/sentry-utils.ts diff --git a/libs/logger/tsconfig.json b/libs/logger/tsconfig.json new file mode 100644 index 000000000..e302a4d20 --- /dev/null +++ b/libs/logger/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/logger/tsconfig.lib.json b/libs/logger/tsconfig.lib.json new file mode 100644 index 000000000..e85ef50f6 --- /dev/null +++ b/libs/logger/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/libs/logger/tsconfig.spec.json b/libs/logger/tsconfig.spec.json new file mode 100644 index 000000000..ff08addd6 --- /dev/null +++ b/libs/logger/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/libs/react-helpers/.eslintrc.json b/libs/react-helpers/.eslintrc.json index 04cd72d3c..25ff9d375 100644 --- a/libs/react-helpers/.eslintrc.json +++ b/libs/react-helpers/.eslintrc.json @@ -7,15 +7,21 @@ "rules": { "no-restricted-imports": [ "error", - "@apollo/client", - "@vegaprotocol/data-provider", - "ag-grid-react", - "ag-grid-community", - "graphql", - "graphql-tag", - "graphql-ws", - "ethers", - "@ethersproject" + { + "paths": [ + "error", + "@apollo/client", + "@vegaprotocol/data-provider", + "ag-grid-react", + "ag-grid-community", + "graphql", + "graphql-tag", + "graphql-ws", + "ethers", + "@ethersproject" + ], + "patterns": ["@sentry/*"] + } ] } }, diff --git a/libs/react-helpers/src/hooks/index.ts b/libs/react-helpers/src/hooks/index.ts index f9b9e40c3..d63e0d6b8 100644 --- a/libs/react-helpers/src/hooks/index.ts +++ b/libs/react-helpers/src/hooks/index.ts @@ -11,6 +11,5 @@ export * from './use-theme-switcher'; export * from './use-storybook-theme-observer'; export * from './use-yesterday'; export * from './use-previous'; -export * from './use-logger'; export * from './use-pane-layout'; export * from './use-copy-timeout'; diff --git a/libs/react-helpers/src/hooks/use-mutation-observer.ts b/libs/react-helpers/src/hooks/use-mutation-observer.ts index d2c93ccde..b423aa634 100644 --- a/libs/react-helpers/src/hooks/use-mutation-observer.ts +++ b/libs/react-helpers/src/hooks/use-mutation-observer.ts @@ -1,4 +1,3 @@ -import { captureException } from '@sentry/react'; import debounce from 'lodash/debounce'; import { useEffect, useMemo } from 'react'; @@ -31,11 +30,7 @@ export function useMutationObserver( useEffect(() => { if (!observer || !target) return; - try { - observer.observe(target, options.config); - } catch (err) { - captureException(err); - } + observer.observe(target, options.config); return () => observer?.disconnect(); }, [observer, options.config, target]); } diff --git a/libs/react-helpers/src/hooks/use-resize-observer.ts b/libs/react-helpers/src/hooks/use-resize-observer.ts index 6e2c510f2..a7b7fe672 100644 --- a/libs/react-helpers/src/hooks/use-resize-observer.ts +++ b/libs/react-helpers/src/hooks/use-resize-observer.ts @@ -1,6 +1,5 @@ import debounce from 'lodash/debounce'; import { useCallback, useEffect, useMemo } from 'react'; -import { localLoggerFactory } from '@vegaprotocol/utils'; type ResizeObserverConfiguration = { debounceTime: number; @@ -35,11 +34,7 @@ export function useResizeObserver( useEffect(() => { if (!observer || !target) return; - try { - observer.observe(target, options.config); - } catch (err) { - localLoggerFactory({ application: 'react-helpers' }).debug(err as Error); - } + observer.observe(target, options.config); return () => observer?.disconnect(); }, [observer, options.config, target]); } diff --git a/libs/ui-toolkit/.eslintrc.json b/libs/ui-toolkit/.eslintrc.json index 734ddacee..f889a206a 100644 --- a/libs/ui-toolkit/.eslintrc.json +++ b/libs/ui-toolkit/.eslintrc.json @@ -4,7 +4,15 @@ "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [], + "patterns": ["@sentry/*"] + } + ] + } }, { "files": ["*.ts", "*.tsx"], diff --git a/libs/utils/.eslintrc.json b/libs/utils/.eslintrc.json index ee1206a44..e8c3634b5 100644 --- a/libs/utils/.eslintrc.json +++ b/libs/utils/.eslintrc.json @@ -7,15 +7,21 @@ "rules": { "no-restricted-imports": [ "error", - "@apollo/client", - "@vegaprotocol/data-provider", - "ag-grid-react", - "ag-grid-community", - "graphql", - "graphql-tag", - "graphql-ws", - "ethers", - "@ethersproject" + { + "paths": [ + "error", + "@apollo/client", + "@vegaprotocol/data-provider", + "ag-grid-react", + "ag-grid-community", + "graphql", + "graphql-tag", + "graphql-ws", + "ethers", + "@ethersproject" + ], + "patterns": ["@sentry/*"] + } ] } }, diff --git a/libs/utils/src/index.ts b/libs/utils/src/index.ts index 3d7927235..d260e0cff 100644 --- a/libs/utils/src/index.ts +++ b/libs/utils/src/index.ts @@ -6,7 +6,6 @@ export * from './lib/get-user-locale'; export * from './lib/helpers'; export * from './lib/is-asset-erc20'; export * from './lib/is-valid-url'; -export * from './lib/local-logger'; export * from './lib/local-storage'; export * from './lib/markets'; export * from './lib/price-change'; @@ -14,4 +13,3 @@ export * from './lib/remove-0x'; export * from './lib/remove-pagination-wrapper'; export * from './lib/time'; export * from './lib/validate'; -export * from './lib/sentry-utils'; diff --git a/libs/web3/src/lib/use-eager-connect.ts b/libs/web3/src/lib/use-eager-connect.ts index 647d19e2e..e6551e9e4 100644 --- a/libs/web3/src/lib/use-eager-connect.ts +++ b/libs/web3/src/lib/use-eager-connect.ts @@ -1,5 +1,6 @@ -import type { LoggerProps } from '@vegaprotocol/react-helpers'; -import { useLocalStorage, useLogger } from '@vegaprotocol/react-helpers'; +import { useLocalStorage } from '@vegaprotocol/react-helpers'; +import type { LoggerProps } from '@vegaprotocol/logger'; +import { localLoggerFactory } from '@vegaprotocol/logger'; import type { Web3ReactHooks } from '@web3-react/core'; import { MetaMask } from '@web3-react/metamask'; import type { Connector } from '@web3-react/types'; @@ -14,7 +15,7 @@ export const useEagerConnect = (loggerConf: LoggerProps) => { const [eagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT); const attemptedRef = useRef(false); - const logger = useLogger(loggerConf); + const logger = localLoggerFactory(loggerConf); useEffect(() => { if (attemptedRef.current || 'Cypress' in window) return; diff --git a/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.tsx b/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.tsx index c79bdd6c5..89d9947cd 100644 --- a/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.tsx +++ b/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.tsx @@ -5,7 +5,7 @@ import { addDecimal } from '@vegaprotocol/utils'; import { useGetWithdrawThreshold } from './use-get-withdraw-threshold'; import { useGetWithdrawDelay } from './use-get-withdraw-delay'; import { t } from '@vegaprotocol/i18n'; -import { localLoggerFactory } from '@vegaprotocol/utils'; +import { localLoggerFactory } from '@vegaprotocol/logger'; import { CollateralBridge } from '@vegaprotocol/smart-contracts'; diff --git a/libs/web3/src/lib/use-get-withdraw-delay.ts b/libs/web3/src/lib/use-get-withdraw-delay.ts index e11f84201..1099226a2 100644 --- a/libs/web3/src/lib/use-get-withdraw-delay.ts +++ b/libs/web3/src/lib/use-get-withdraw-delay.ts @@ -1,6 +1,6 @@ import { useBridgeContract } from './use-bridge-contract'; import { useCallback } from 'react'; -import { localLoggerFactory } from '@vegaprotocol/utils'; +import { localLoggerFactory } from '@vegaprotocol/logger'; /** * Gets the delay in seconds thats required if the withdrawal amount is diff --git a/libs/withdraws/src/lib/use-complete-withdraw.ts b/libs/withdraws/src/lib/use-complete-withdraw.ts index e826f1e6b..d5932b002 100644 --- a/libs/withdraws/src/lib/use-complete-withdraw.ts +++ b/libs/withdraws/src/lib/use-complete-withdraw.ts @@ -6,7 +6,7 @@ import { useEthereumTransaction, } from '@vegaprotocol/web3'; import { useCallback, useEffect, useState } from 'react'; -import { localLoggerFactory } from '@vegaprotocol/utils'; +import { localLoggerFactory } from '@vegaprotocol/logger'; import { Erc20ApprovalDocument } from './__generated__/Erc20Approval'; import type { Erc20ApprovalQuery, diff --git a/libs/withdraws/src/lib/use-verify-withdrawal.ts b/libs/withdraws/src/lib/use-verify-withdrawal.ts index 28596f613..252eea6b4 100644 --- a/libs/withdraws/src/lib/use-verify-withdrawal.ts +++ b/libs/withdraws/src/lib/use-verify-withdrawal.ts @@ -1,6 +1,7 @@ import { useCallback, useState } from 'react'; import BigNumber from 'bignumber.js'; -import { addDecimal, localLoggerFactory } from '@vegaprotocol/utils'; +import { addDecimal } from '@vegaprotocol/utils'; +import { localLoggerFactory } from '@vegaprotocol/logger'; import { t } from '@vegaprotocol/i18n'; import { ApprovalStatus, diff --git a/libs/withdraws/src/lib/use-withdraw-asset.tsx b/libs/withdraws/src/lib/use-withdraw-asset.tsx index 6f05dcf69..d65584d32 100644 --- a/libs/withdraws/src/lib/use-withdraw-asset.tsx +++ b/libs/withdraws/src/lib/use-withdraw-asset.tsx @@ -1,5 +1,6 @@ import type { Asset } from '@vegaprotocol/assets'; -import { addDecimal, localLoggerFactory } from '@vegaprotocol/utils'; +import { addDecimal } from '@vegaprotocol/utils'; +import { localLoggerFactory } from '@vegaprotocol/logger'; import * as Schema from '@vegaprotocol/types'; import BigNumber from 'bignumber.js'; import { useCallback, useEffect } from 'react'; diff --git a/tsconfig.base.json b/tsconfig.base.json index e13e6b4de..c39912da7 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -31,6 +31,7 @@ "@vegaprotocol/i18n": ["libs/i18n/src/index.ts"], "@vegaprotocol/ledger": ["libs/ledger/src/index.ts"], "@vegaprotocol/liquidity": ["libs/liquidity/src/index.ts"], + "@vegaprotocol/logger": ["libs/logger/src/index.ts"], "@vegaprotocol/market-depth": ["libs/market-depth/src/index.ts"], "@vegaprotocol/markets": ["libs/markets/src/index.ts"], "@vegaprotocol/mock": ["libs/cypress/mock.ts"], diff --git a/workspace.json b/workspace.json index 41a68bac1..f4da2a020 100644 --- a/workspace.json +++ b/workspace.json @@ -21,6 +21,7 @@ "ledger": "libs/ledger", "liquidity": "libs/liquidity", "liquidity-provision-dashboard": "apps/liquidity-provision-dashboard", + "logger": "libs/logger", "market-depth": "libs/market-depth", "markets": "libs/markets", "multisig-signer": "apps/multisig-signer", From 3928dd5c0edc048398d44a155e1aa5784afb1db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20M=C5=82odzikowski?= Date: Tue, 6 Jun 2023 15:36:59 +0200 Subject: [PATCH 12/49] fix(ci): way of resolving affected --- .github/workflows/ci-cd-trigger.yml | 3 ++- apps/multisig-signer/.env.mainnet | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd-trigger.yml b/.github/workflows/ci-cd-trigger.yml index b319d66ab..4caffe95d 100644 --- a/.github/workflows/ci-cd-trigger.yml +++ b/.github/workflows/ci-cd-trigger.yml @@ -135,7 +135,6 @@ jobs: preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug") preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug") fi - projects_e2e=${projects_e2e%?} projects=$(echo $projects_e2e | sed 's|-e2e||g') if echo "$affected" | grep -q multisig-signer; then echo "Tools are affected" @@ -150,7 +149,9 @@ jobs: projects+='"multisig-signer" ' fi fi + projects_e2e=${projects_e2e%?} projects_e2e=[${projects_e2e// /,}] + projects=${projects%?} projects=[${projects// /,}] echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV echo PROJECTS=$projects >> $GITHUB_ENV diff --git a/apps/multisig-signer/.env.mainnet b/apps/multisig-signer/.env.mainnet index 3c2646f18..3ee5b5286 100644 --- a/apps/multisig-signer/.env.mainnet +++ b/apps/multisig-signer/.env.mainnet @@ -3,3 +3,5 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/maste NX_VEGA_URL=https://api.vega.community/graphql NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}' NX_VEGA_ENV=MAINNET + + From cefdbe6a3c9f93f24ea17eabec9a1f9c4c0af20a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20M=C5=82odzikowski?= Date: Tue, 6 Jun 2023 16:46:58 +0200 Subject: [PATCH 13/49] fix(ci): missing space on resolving projects --- .github/workflows/ci-cd-trigger.yml | 6 +++--- apps/multisig-signer/.env.mainnet | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-cd-trigger.yml b/.github/workflows/ci-cd-trigger.yml index 4caffe95d..da5d340ea 100644 --- a/.github/workflows/ci-cd-trigger.yml +++ b/.github/workflows/ci-cd-trigger.yml @@ -135,18 +135,18 @@ jobs: preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug") preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug") fi - projects=$(echo $projects_e2e | sed 's|-e2e||g') + projects="$(echo $projects_e2e | sed 's|-e2e||g')" if echo "$affected" | grep -q multisig-signer; then echo "Tools are affected" # tools are only applicable to check previews or deploy from develop to mainnet if [[ "${{ github.event_name }}" = "pull_request" ]]; then echo "Deploying tools on preview" preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug") - projects+='"multisig-signer" ' + projects+=' "multisig-signer" ' fi if [[ "${{ github.ref }}" =~ .*develop$ ]]; then echo "Deploying tools on s3" - projects+='"multisig-signer" ' + projects+=' "multisig-signer" ' fi fi projects_e2e=${projects_e2e%?} diff --git a/apps/multisig-signer/.env.mainnet b/apps/multisig-signer/.env.mainnet index 3ee5b5286..3c2646f18 100644 --- a/apps/multisig-signer/.env.mainnet +++ b/apps/multisig-signer/.env.mainnet @@ -3,5 +3,3 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/maste NX_VEGA_URL=https://api.vega.community/graphql NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}' NX_VEGA_ENV=MAINNET - - From 71540a90fb98bdb294e504798ecc18e3cd97dd23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20M=C5=82odzikowski?= Date: Tue, 6 Jun 2023 17:02:24 +0200 Subject: [PATCH 14/49] fix(ci): resolving bucket name --- .github/workflows/publish-dist.yml | 8 ++++++-- apps/multisig-signer/.env.mainnet | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-dist.yml b/.github/workflows/publish-dist.yml index 3defc8474..1c0b7f4ad 100644 --- a/.github/workflows/publish-dist.yml +++ b/.github/workflows/publish-dist.yml @@ -82,10 +82,14 @@ jobs: if [[ "${envName}" = "mainnet" ]]; then domain="vega.xyz" - bucketName="${{ matrix.app }}.${domain}" + if [[ -z "${bucketName}" ]]; then + bucketName="${{ matrix.app }}.${domain}" + fi elif [[ "${envName}" = "testnet" ]]; then domain="fairground.wtf" - bucketName="${{ matrix.app }}.${domain}" + if [[ -z "${bucketName}" ]]; then + bucketName="${{ matrix.app }}.${domain}" + fi fi if [[ -z "${bucketName}" ]]; then diff --git a/apps/multisig-signer/.env.mainnet b/apps/multisig-signer/.env.mainnet index 3c2646f18..2a65459fa 100644 --- a/apps/multisig-signer/.env.mainnet +++ b/apps/multisig-signer/.env.mainnet @@ -3,3 +3,6 @@ NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/maste NX_VEGA_URL=https://api.vega.community/graphql NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}' NX_VEGA_ENV=MAINNET + + + From d6a32f5090419110511a6249fd7324233903fa7d Mon Sep 17 00:00:00 2001 From: Joe Tsang <30622993+jtsang586@users.noreply.github.com> Date: Tue, 6 Jun 2023 22:19:23 +0100 Subject: [PATCH 15/49] chore(governance): fix flaky test failures (#4043) --- .../src/integration/flow/proposal-forms.cy.ts | 170 +++++++++++------- .../src/integration/flow/rewards-flow.cy.ts | 8 +- .../vega-wallet-top-up-rewards-pool.ts | 70 ++++---- 3 files changed, 144 insertions(+), 104 deletions(-) diff --git a/apps/governance-e2e/src/integration/flow/proposal-forms.cy.ts b/apps/governance-e2e/src/integration/flow/proposal-forms.cy.ts index a649fe862..d1827e47a 100644 --- a/apps/governance-e2e/src/integration/flow/proposal-forms.cy.ts +++ b/apps/governance-e2e/src/integration/flow/proposal-forms.cy.ts @@ -105,9 +105,9 @@ context( goToMakeNewProposal(governanceProposalType.RAW); submitUniqueRawProposal({ proposalBody: filePath, submit: false }); }); - cy.get(newProposalSubmitButton).click(); - validateDialogContentMsg('PROPOSAL_ERROR_CLOSE_TIME_TOO_SOON'); }); + cy.get(newProposalSubmitButton).click(); + validateDialogContentMsg('PROPOSAL_ERROR_CLOSE_TIME_TOO_SOON'); }); // 3007-PNEC-001 3007-PNEC-003 @@ -187,13 +187,17 @@ context( 'have.text', 'Proposal will fail if enactment is earlier than the voting deadline' ); - cy.get(proposalDownloadBtn).click(); - cy.wrap( - getDownloadedProposalJsonPath('vega-network-param-proposal-') - ).then((filePath) => { - goToMakeNewProposal(governanceProposalType.RAW); - submitUniqueRawProposal({ proposalBody: filePath, submit: false }); - }); + cy.get(proposalDownloadBtn) + .should('be.visible') + .click() + .then(() => { + cy.wrap( + getDownloadedProposalJsonPath('vega-network-param-proposal-') + ).then((filePath) => { + goToMakeNewProposal(governanceProposalType.RAW); + submitUniqueRawProposal({ proposalBody: filePath, submit: false }); + }); + }); cy.get(newProposalSubmitButton).click(); validateFeedBackMsg( 'Invalid params: proposal_submission.terms.closing_timestamp (cannot be after enactment time)' @@ -202,9 +206,6 @@ context( // 3003-PMAN-001 it('Able to submit valid new market proposal', function () { - // Wait needed for test to pass in CI because of report name discrepancy when time passes - // eslint-disable-next-line cypress/no-unnecessary-waiting - cy.wait(5000); goToMakeNewProposal(governanceProposalType.NEW_MARKET); cy.get(newProposalTitle).type('Test new market proposal'); cy.get(newProposalDescription).type('E2E test for proposals'); @@ -215,13 +216,17 @@ context( delay: 2, }); }); - cy.get(proposalDownloadBtn).should('be.visible').click(); - cy.wrap(getDownloadedProposalJsonPath('vega-new-market-proposal-')).then( - (filePath) => { - goToMakeNewProposal(governanceProposalType.RAW); - submitUniqueRawProposal({ proposalBody: filePath }); // 3003-PMAN-003 - } - ); + cy.get(proposalDownloadBtn) + .should('be.visible') + .click() + .then(() => { + cy.wrap( + getDownloadedProposalJsonPath('vega-new-market-proposal-') + ).then((filePath) => { + goToMakeNewProposal(governanceProposalType.RAW); + submitUniqueRawProposal({ proposalBody: filePath }); // 3003-PMAN-003 + }); + }); }); it('Unable to submit new market proposal with missing/invalid fields', function () { @@ -241,13 +246,17 @@ context( delay: 2, }); }); - cy.get(proposalDownloadBtn).should('be.visible').click(); - cy.wrap(getDownloadedProposalJsonPath('vega-new-market-proposal-')).then( - (filePath) => { - goToMakeNewProposal(governanceProposalType.RAW); - submitUniqueRawProposal({ proposalBody: filePath, submit: false }); - } - ); + cy.get(proposalDownloadBtn) + .should('be.visible') + .click() + .then(() => { + cy.wrap( + getDownloadedProposalJsonPath('vega-new-market-proposal-') + ).then((filePath) => { + goToMakeNewProposal(governanceProposalType.RAW); + submitUniqueRawProposal({ proposalBody: filePath, submit: false }); + }); + }); cy.get(newProposalSubmitButton).should('be.visible').click(); cy.contains('Transaction failed', proposalTimeout).should('be.visible'); validateFeedBackMsg(errorMsg); @@ -270,13 +279,17 @@ context( delay: 2, }); }); - cy.get(proposalDownloadBtn).should('be.visible').click(); - cy.wrap( - getDownloadedProposalJsonPath('vega-update-market-proposal-') - ).then((filePath) => { - goToMakeNewProposal(governanceProposalType.RAW); - submitUniqueRawProposal({ proposalBody: filePath, submit: false }); - }); + cy.get(proposalDownloadBtn) + .should('be.visible') + .click() + .then(() => { + cy.wrap( + getDownloadedProposalJsonPath('vega-update-market-proposal-') + ).then((filePath) => { + goToMakeNewProposal(governanceProposalType.RAW); + submitUniqueRawProposal({ proposalBody: filePath, submit: false }); + }); + }); cy.get(newProposalSubmitButton).should('be.visible').click(); cy.contains('Proposal rejected', proposalTimeout).should('be.visible'); validateDialogContentMsg('PROPOSAL_ERROR_INSUFFICIENT_EQUITY_LIKE_SHARE'); @@ -306,13 +319,17 @@ context( delay: 2, }); }); - cy.get(proposalDownloadBtn).should('be.visible').click(); - cy.wrap( - getDownloadedProposalJsonPath('vega-update-market-proposal-') - ).then((filePath) => { - goToMakeNewProposal(governanceProposalType.RAW); - submitUniqueRawProposal({ proposalBody: filePath, submit: false }); - }); + cy.get(proposalDownloadBtn) + .should('be.visible') + .click() + .then(() => { + cy.wrap( + getDownloadedProposalJsonPath('vega-update-market-proposal-') + ).then((filePath) => { + goToMakeNewProposal(governanceProposalType.RAW); + submitUniqueRawProposal({ proposalBody: filePath, submit: false }); + }); + }); cy.get(newProposalSubmitButton).should('be.visible').click(); cy.contains('Transaction failed', proposalTimeout).should('be.visible'); validateFeedBackMsg( @@ -350,13 +367,17 @@ context( delay: 2, }); }); - cy.get(proposalDownloadBtn).should('be.visible').click(); - cy.wrap( - getDownloadedProposalJsonPath('vega-update-market-proposal-') - ).then((filePath) => { - goToMakeNewProposal(governanceProposalType.RAW); - submitUniqueRawProposal({ proposalBody: filePath }); - }); + cy.get(proposalDownloadBtn) + .should('be.visible') + .click() + .then(() => { + cy.wrap( + getDownloadedProposalJsonPath('vega-update-market-proposal-') + ).then((filePath) => { + goToMakeNewProposal(governanceProposalType.RAW); + submitUniqueRawProposal({ proposalBody: filePath }); + }); + }); navigateTo(navigation.proposals); cy.get('@EnactedMarketId').then((marketId) => { cy.contains(String(marketId).slice(0, 6)) @@ -405,14 +426,17 @@ context( cy.get(minVoteDeadline).click(); cy.get(minValidationDeadline).click(); cy.get(minEnactDeadline).click(); - - cy.get(proposalDownloadBtn).should('be.visible').click(); - cy.wrap(getDownloadedProposalJsonPath('vega-new-asset-proposal-')).then( - (filePath) => { - goToMakeNewProposal(governanceProposalType.RAW); - submitUniqueRawProposal({ proposalBody: filePath, submit: false }); // 3005-PASN-003 - } - ); + cy.get(proposalDownloadBtn) + .should('be.visible') + .click() + .then(() => { + cy.wrap( + getDownloadedProposalJsonPath('vega-new-asset-proposal-') + ).then((filePath) => { + goToMakeNewProposal(governanceProposalType.RAW); + submitUniqueRawProposal({ proposalBody: filePath, submit: false }); // 3005-PASN-003 + }); + }); cy.get(newProposalSubmitButton).should('be.visible').click(); closeDialog(); cy.get(newProposalSubmitButton).should('be.visible').click(); @@ -448,13 +472,17 @@ context( enterUpdateAssetProposalDetails(); cy.get(minVoteDeadline).click(); cy.get(minEnactDeadline).click(); - cy.get(proposalDownloadBtn).should('be.visible').click(); - cy.wrap( - getDownloadedProposalJsonPath('vega-update-asset-proposal-') - ).then((filePath) => { - goToMakeNewProposal(governanceProposalType.RAW); - submitUniqueRawProposal({ proposalBody: filePath }); - }); + cy.get(proposalDownloadBtn) + .should('be.visible') + .click() + .then(() => { + cy.wrap( + getDownloadedProposalJsonPath('vega-update-asset-proposal-') + ).then((filePath) => { + goToMakeNewProposal(governanceProposalType.RAW); + submitUniqueRawProposal({ proposalBody: filePath }); + }); + }); navigateTo(navigation.proposals); cy.get(openProposals).within(() => { cy.get(proposalType) @@ -486,13 +514,17 @@ context( enterUpdateAssetProposalDetails(); cy.get(maxVoteDeadline).click(); cy.get(maxEnactDeadline).click(); - cy.get(proposalDownloadBtn).should('be.visible').click(); - cy.wrap( - getDownloadedProposalJsonPath('vega-update-asset-proposal-') - ).then((filePath) => { - goToMakeNewProposal(governanceProposalType.RAW); - submitUniqueRawProposal({ proposalBody: filePath }); - }); + cy.get(proposalDownloadBtn) + .should('be.visible') + .click() + .then(() => { + cy.wrap( + getDownloadedProposalJsonPath('vega-update-asset-proposal-') + ).then((filePath) => { + goToMakeNewProposal(governanceProposalType.RAW); + submitUniqueRawProposal({ proposalBody: filePath }); + }); + }); }); it('Unable to submit edit asset proposal with missing/invalid fields', function () { diff --git a/apps/governance-e2e/src/integration/flow/rewards-flow.cy.ts b/apps/governance-e2e/src/integration/flow/rewards-flow.cy.ts index 939a700ff..59fe420b7 100644 --- a/apps/governance-e2e/src/integration/flow/rewards-flow.cy.ts +++ b/apps/governance-e2e/src/integration/flow/rewards-flow.cy.ts @@ -20,10 +20,8 @@ const vegaAssetAddress = '0x67175Da1D5e966e40D11c4B2519392B2058373de'; const vegaWalletUnstakedBalance = '[data-testid="vega-wallet-balance-unstaked"]'; const rewardsTable = 'epoch-total-rewards-table'; -const rewardsStartEpoch = 380; // Use 30 running locally -const rewardsEndEpoch = 500; // Change to 200 running locally const txTimeout = Cypress.env('txTimeout'); -const rewardsTimeOut = { timeout: 5 * 60 * 1000 }; +const rewardsTimeOut = { timeout: 60000 }; context('rewards - flow', { tags: '@slow' }, function () { before('set up environment to allow rewards', function () { @@ -40,13 +38,13 @@ context('rewards - flow', { tags: '@slow' }, function () { ); vegaWalletTeardown(); cy.associateTokensToVegaWallet('6000'); - cy.VegaWalletTopUpRewardsPool(rewardsStartEpoch, rewardsEndEpoch); + navigateTo(navigation.validators); + cy.VegaWalletTopUpRewardsPool(); cy.get(vegaWalletUnstakedBalance, txTimeout).should( 'contain', '6,000.0', txTimeout ); - navigateTo(navigation.validators); clickOnValidatorFromList(0); stakingValidatorPageAddStake('3000'); closeStakingDialog(); diff --git a/libs/cypress/src/lib/commands/vega-wallet-top-up-rewards-pool.ts b/libs/cypress/src/lib/commands/vega-wallet-top-up-rewards-pool.ts index 4b06b3e2e..2942120c4 100644 --- a/libs/cypress/src/lib/commands/vega-wallet-top-up-rewards-pool.ts +++ b/libs/cypress/src/lib/commands/vega-wallet-top-up-rewards-pool.ts @@ -7,44 +7,54 @@ declare global { namespace Cypress { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface Chainable { - VegaWalletTopUpRewardsPool( - transferStartEpoch: number, - transferEndEpoch: number - ): void; + VegaWalletTopUpRewardsPool(): void; } } } export function addVegaWalletTopUpRewardsPool() { - Cypress.Commands.add( - 'VegaWalletTopUpRewardsPool', - (transferStartEpoch, transferEndEpoch) => { - const vegaWalletUrl = Cypress.env('VEGA_WALLET_URL'); - const token = Cypress.env('VEGA_WALLET_API_TOKEN'); - const vegaPubKey = Cypress.env('VEGA_PUBLIC_KEY'); - const assetAddress = - 'b4f2726571fbe8e33b442dc92ed2d7f0d810e21835b7371a7915a365f07ccd9b'; + Cypress.Commands.add('VegaWalletTopUpRewardsPool', () => { + let transferStartEpoch = 0; + let transferEndEpoch = 0; + const vegaWalletUrl = Cypress.env('VEGA_WALLET_URL'); + const token = Cypress.env('VEGA_WALLET_API_TOKEN'); + const vegaPubKey = Cypress.env('VEGA_PUBLIC_KEY'); + const assetAddress = + 'b4f2726571fbe8e33b442dc92ed2d7f0d810e21835b7371a7915a365f07ccd9b'; - createWalletClient(vegaWalletUrl, token); + cy.getByTestId('epoch-countdown') + .within(() => { + cy.get('h3') + .invoke('text') + .then((epochText) => { + transferStartEpoch = Number(epochText.replace('Epoch', '')) + 5; + transferEndEpoch = transferStartEpoch + 100; - const transactionBody: TransferBody = { - transfer: { - fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL, - toAccountType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD, - to: '0000000000000000000000000000000000000000000000000000000000000000', - asset: assetAddress, - amount: '1000000000000000000', - recurring: { - factor: '1', - startEpoch: transferStartEpoch, - endEpoch: transferEndEpoch, + console.log(transferStartEpoch); + console.log(transferEndEpoch); + }); + }) + .then(() => { + createWalletClient(vegaWalletUrl, token); + + const transactionBody: TransferBody = { + transfer: { + fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL, + toAccountType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD, + to: '0000000000000000000000000000000000000000000000000000000000000000', + asset: assetAddress, + amount: '1000000000000000000', + recurring: { + factor: '1', + startEpoch: transferStartEpoch, + endEpoch: transferEndEpoch, + }, }, - }, - }; + }; - cy.highlight('Topping up rewards pool'); + cy.highlight('Topping up rewards pool'); - sendVegaTx(vegaPubKey, transactionBody); - } - ); + sendVegaTx(vegaPubKey, transactionBody); + }); + }); } From 473c244d7bb5b240a2ccd74e2eaa9e20a6f9bcaa Mon Sep 17 00:00:00 2001 From: Edd Date: Tue, 6 Jun 2023 22:19:42 +0100 Subject: [PATCH 16/49] chore(governance,liquidity-provision-dashboard,trading): remove broken urls (#4042) --- apps/governance/.env.devnet | 2 +- apps/governance/.env.validators-testnet | 2 +- apps/liquidity-provision-dashboard/.env.devnet | 2 +- apps/trading/.env.devnet | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/governance/.env.devnet b/apps/governance/.env.devnet index edd65ec03..3c3621283 100644 --- a/apps/governance/.env.devnet +++ b/apps/governance/.env.devnet @@ -6,7 +6,7 @@ NX_VEGA_NETWORKS='{"DEVNET":"https://dev.governance.vega.xyz","TESTNET":"https:/ NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions -NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz +NX_VEGA_EXPLORER_URL=# NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet NX_DELEGATIONS_PAGINATION=50 NX_TRANCHES_SERVICE_URL=https://tranches-devnet1-k8s.ops.vega.xyz diff --git a/apps/governance/.env.validators-testnet b/apps/governance/.env.validators-testnet index d17e65b26..f01cece2c 100644 --- a/apps/governance/.env.validators-testnet +++ b/apps/governance/.env.validators-testnet @@ -7,7 +7,7 @@ NX_VEGA_NETWORKS='{"DEVNET":"https://dev.governance.vega.xyz","TESTNET":"https:/ NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions -NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz +NX_VEGA_EXPLORER_URL=https://explorer.validators-testnet.vega.rocks/ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_VEGA_REST_URL=https://api-validators-testnet.vega.rocks/api/v2/ NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996 diff --git a/apps/liquidity-provision-dashboard/.env.devnet b/apps/liquidity-provision-dashboard/.env.devnet index 43a189766..112c12354 100644 --- a/apps/liquidity-provision-dashboard/.env.devnet +++ b/apps/liquidity-provision-dashboard/.env.devnet @@ -5,4 +5,4 @@ NX_VEGA_ENV=DEVNET NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"} NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 NX_ETHERSCAN_URL=https://sepolia.etherscan.io -NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz +NX_VEGA_EXPLORER_URL=# diff --git a/apps/trading/.env.devnet b/apps/trading/.env.devnet index cd9afcc97..e53d01828 100644 --- a/apps/trading/.env.devnet +++ b/apps/trading/.env.devnet @@ -9,7 +9,7 @@ NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"} NX_VEGA_TOKEN_URL=https://governance.fairground.wtf NX_VEGA_WALLET_URL=http://localhost:1789 -NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet +NX_VEGA_DOCS_URL=# NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports From 3f01b9315907cd8514e8456cb531a530d6f3fab9 Mon Sep 17 00:00:00 2001 From: Sam Keen Date: Tue, 6 Jun 2023 22:21:17 +0100 Subject: [PATCH 17/49] feat(governance,proposals,web3): remove use of word "we" in copy (#4034) --- apps/governance/src/i18n/translations/dev.json | 4 ++-- .../proposal-form-vote-and-enactment-deadline.spec.tsx | 2 +- libs/proposals/src/utils/get-closing-timestamp.ts | 2 +- libs/proposals/src/utils/get-enactment-timestamp.ts | 2 +- libs/web3/src/lib/withdrawal-approval-dialog.tsx | 3 +-- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/governance/src/i18n/translations/dev.json b/apps/governance/src/i18n/translations/dev.json index 86b9666ee..e2079bb87 100644 --- a/apps/governance/src/i18n/translations/dev.json +++ b/apps/governance/src/i18n/translations/dev.json @@ -116,7 +116,7 @@ "Showing tranches with <{{trancheMinimum}} VEGA, click to hide these tranches": "Showing tranches with ≤{{trancheMinimum}} $VEGA, click to hide these tranches", "Not showing tranches with <{{trancheMinimum}} VEGA, click to show all tranches": "Not showing tranches with ≤{{trancheMinimum}} $VEGA, click to show all tranches", "the holder": "the holder", - "We couldn't seem to load your data.": "We couldn't seem to load your data.", + "Your data couldn't be loaded": "Your data couldn't be loaded", "Vesting VEGA": "Vesting VEGA", "All the tokens in this tranche are locked and can not be redeemed yet.": "All the tokens in this tranche are locked and can not be redeemed yet.", "Redeem unlocked VEGA from tranche {{id}}": "Redeem unlocked $VEGA from tranche {{id}}", @@ -728,7 +728,7 @@ "ThisWillSetEnactmentDeadlineTo": "This will set the enactment date to", "ThisWillSetValidationDeadlineTo": "This will set the validation deadline to", "Hours": "hours", - "ThisWillAdd2MinutesToAllowTimeToConfirmInWallet": "Note: we add 2 minutes of extra time when you choose the minimum value. This gives you time to confirm the proposal in your wallet.", + "ThisWillAdd2MinutesToAllowTimeToConfirmInWallet": "Note: 2 minutes of extra time are added when you choose the minimum value. This gives you time to confirm the proposal in your wallet.", "ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline": "Proposal will fail if enactment is earlier than the voting deadline", "SelectAMarketToChange": "Select a market to change", "MarketName": "Market name", diff --git a/apps/governance/src/routes/proposals/components/propose/proposal-form-vote-and-enactment-deadline.spec.tsx b/apps/governance/src/routes/proposals/components/propose/proposal-form-vote-and-enactment-deadline.spec.tsx index 743f319ff..aef005c41 100644 --- a/apps/governance/src/routes/proposals/components/propose/proposal-form-vote-and-enactment-deadline.spec.tsx +++ b/apps/governance/src/routes/proposals/components/propose/proposal-form-vote-and-enactment-deadline.spec.tsx @@ -146,7 +146,7 @@ describe('Proposal form vote, validation and enactment deadline', () => { it('should show the correct datetimes', () => { renderComponent(); // Should be adding 2 mins to the vote deadline as the minimum is set by - // default, and we add 2 mins for wallet confirmation + // default, and 2 mins are added for wallet confirmation expect(screen.getByTestId('voting-date')).toHaveTextContent( '2022-01-01T01:02:00.000Z' ); diff --git a/libs/proposals/src/utils/get-closing-timestamp.ts b/libs/proposals/src/utils/get-closing-timestamp.ts index ab5c59d68..8d2eb0e82 100644 --- a/libs/proposals/src/utils/get-closing-timestamp.ts +++ b/libs/proposals/src/utils/get-closing-timestamp.ts @@ -1,7 +1,7 @@ import { addHours, getTime } from 'date-fns'; import { addTwoMinutes, subtractTwoSeconds } from './deadline-helpers'; -// If the vote deadline is at its minimum, then we add 2 extra minutes to the +// If the vote deadline is at its minimum, then 2 extra minutes are added to the // closing timestamp to ensure that there's time to confirm in the wallet. // If it's at its maximum, remove a couple of seconds to ensure rounding errors diff --git a/libs/proposals/src/utils/get-enactment-timestamp.ts b/libs/proposals/src/utils/get-enactment-timestamp.ts index 2fe4f6acc..7c79c78a2 100644 --- a/libs/proposals/src/utils/get-enactment-timestamp.ts +++ b/libs/proposals/src/utils/get-enactment-timestamp.ts @@ -1,7 +1,7 @@ import { addHours, getTime } from 'date-fns'; import { addTwoMinutes, subtractTwoSeconds } from './deadline-helpers'; -// If the enactment deadline is at its minimum, then we add 2 extra minutes to the +// If the enactment deadline is at its minimum, then 2 extra minutes are added to the // closing timestamp to ensure that there's time to confirm in the wallet. // If it's at its maximum, remove a couple of seconds to ensure rounding errors diff --git a/libs/web3/src/lib/withdrawal-approval-dialog.tsx b/libs/web3/src/lib/withdrawal-approval-dialog.tsx index 16f954113..d3cb264e0 100644 --- a/libs/web3/src/lib/withdrawal-approval-dialog.tsx +++ b/libs/web3/src/lib/withdrawal-approval-dialog.tsx @@ -50,8 +50,7 @@ export const WithdrawalApprovalDialog = ({

{t( `If the network is reset or has an outage, records of your withdrawal - may be lost. We recommend you save these details in a safe place so - you can still complete your withdrawal.` + may be lost. It is recommended that you save these details in a safe place so you can still complete your withdrawal.` )}

{withdrawalId ? ( From 6f9f432c9015056e903a0b2b6eb837888903fef1 Mon Sep 17 00:00:00 2001 From: Sam Keen Date: Tue, 6 Jun 2023 22:21:31 +0100 Subject: [PATCH 18/49] feat(governance): multisig warning (#3994) --- .../multisig-incorrect-notice/index.ts | 1 + .../multisig-incorrect-notice.spec.tsx | 52 ++++++++++++++ .../multisig-incorrect-notice.tsx | 49 +++++++++++++ .../governance/src/i18n/translations/dev.json | 5 +- .../src/lib/get-multisig-status-info.spec.ts | 70 +++++++++++++++++++ .../src/lib/get-multisig-status-info.ts | 42 +++++++++++ .../src/routes/rewards/home/rewards-page.tsx | 24 ++++++- .../src/routes/staking/home/epoch-data.tsx | 10 +++ libs/environment/src/hooks/use-links.ts | 1 + 9 files changed, 251 insertions(+), 3 deletions(-) create mode 100644 apps/governance/src/components/multisig-incorrect-notice/index.ts create mode 100644 apps/governance/src/components/multisig-incorrect-notice/multisig-incorrect-notice.spec.tsx create mode 100644 apps/governance/src/components/multisig-incorrect-notice/multisig-incorrect-notice.tsx create mode 100644 apps/governance/src/lib/get-multisig-status-info.spec.ts create mode 100644 apps/governance/src/lib/get-multisig-status-info.ts diff --git a/apps/governance/src/components/multisig-incorrect-notice/index.ts b/apps/governance/src/components/multisig-incorrect-notice/index.ts new file mode 100644 index 000000000..8700b7c95 --- /dev/null +++ b/apps/governance/src/components/multisig-incorrect-notice/index.ts @@ -0,0 +1 @@ +export * from './multisig-incorrect-notice'; diff --git a/apps/governance/src/components/multisig-incorrect-notice/multisig-incorrect-notice.spec.tsx b/apps/governance/src/components/multisig-incorrect-notice/multisig-incorrect-notice.spec.tsx new file mode 100644 index 000000000..deb48efb4 --- /dev/null +++ b/apps/governance/src/components/multisig-incorrect-notice/multisig-incorrect-notice.spec.tsx @@ -0,0 +1,52 @@ +import { render, screen } from '@testing-library/react'; +import { useEthereumConfig } from '@vegaprotocol/web3'; +import { useEnvironment } from '@vegaprotocol/environment'; +import { MultisigIncorrectNotice } from './multisig-incorrect-notice'; + +jest.mock('@vegaprotocol/web3', () => ({ + useEthereumConfig: jest.fn(), +})); + +jest.mock('@vegaprotocol/environment', () => ({ + useEnvironment: jest.fn(), +})); + +describe('MultisigIncorrectNotice', () => { + it('renders correctly when config is provided', () => { + (useEthereumConfig as jest.Mock).mockReturnValue({ + config: { + multisig_control_contract: { + address: '0x1234', + }, + }, + }); + + (useEnvironment as unknown as jest.Mock).mockReturnValue({ + ETHERSCAN_URL: 'https://etherscan.io', + }); + + render(); + + expect(screen.getByTestId('multisig-contract-link')).toHaveAttribute( + 'href', + 'https://etherscan.io/address/0x1234' + ); + expect(screen.getByTestId('multisig-contract-link')).toHaveAttribute( + 'title', + '0x1234' + ); + expect( + screen.getByTestId('multisig-validators-learn-more') + ).toBeInTheDocument(); + }); + + it('does not render when config is not provided', () => { + (useEthereumConfig as jest.Mock).mockReturnValue({ + config: null, + }); + + const { container } = render(); + + expect(container.firstChild).toBeNull(); + }); +}); diff --git a/apps/governance/src/components/multisig-incorrect-notice/multisig-incorrect-notice.tsx b/apps/governance/src/components/multisig-incorrect-notice/multisig-incorrect-notice.tsx new file mode 100644 index 000000000..09529318d --- /dev/null +++ b/apps/governance/src/components/multisig-incorrect-notice/multisig-incorrect-notice.tsx @@ -0,0 +1,49 @@ +import { useTranslation } from 'react-i18next'; +import { Callout, Intent, Link } from '@vegaprotocol/ui-toolkit'; +import { useEthereumConfig } from '@vegaprotocol/web3'; +import { useEnvironment, DocsLinks } from '@vegaprotocol/environment'; +import type { EthereumConfig } from '@vegaprotocol/web3'; + +export const MultisigIncorrectNotice = () => { + const { t } = useTranslation(); + const { config } = useEthereumConfig(); + const { ETHERSCAN_URL } = useEnvironment(); + + if (!config) { + return null; + } + + const contract = config[ + 'multisig_control_contract' as keyof EthereumConfig + ] as { + address: string; + }; + + return ( +
+ +
+ + {t('multisigContractLink')} + {' '} + {t('multisigContractIncorrect')} +
+ +
+ + {t('learnMore')} + +
+
+
+ ); +}; diff --git a/apps/governance/src/i18n/translations/dev.json b/apps/governance/src/i18n/translations/dev.json index e2079bb87..c266a73d5 100644 --- a/apps/governance/src/i18n/translations/dev.json +++ b/apps/governance/src/i18n/translations/dev.json @@ -821,5 +821,8 @@ "disclaimer2": "The Vega Governance App is free, public and open source software. Software upgrades may contain bugs or security vulnerabilities that might result in loss of functionality.", "disclaimer3": "The Vega Governance App uses data obtained from nodes on the Vega Blockchain. The developers of the Vega Governance App do not operate or run the Vega Blockchain or any other blockchain.", "disclaimer4": "The Vega Governance App is provided “as is”. The developers of the Vega Governance App make no representations or warranties of any kind, whether express or implied, statutory or otherwise regarding the Vega Governance App. They disclaim all warranties of merchantability, quality, fitness for purpose. They disclaim all warranties that the Vega Governance App is free of harmful components or errors.", - "disclaimer5": "No developer of the Vega Governance App accepts any responsibility for, or liability to users in connection with their use of the Vega Governance App." + "disclaimer5": "No developer of the Vega Governance App accepts any responsibility for, or liability to users in connection with their use of the Vega Governance App.", + "multisigContractLink": "Ethereum Multisig Contract", + "multisigContractIncorrect": "is incorrectly configured. Validator and delegator rewards will be penalised until this is resolved.", + "learnMore": "Learn more" } diff --git a/apps/governance/src/lib/get-multisig-status-info.spec.ts b/apps/governance/src/lib/get-multisig-status-info.spec.ts new file mode 100644 index 000000000..d92d5a7d5 --- /dev/null +++ b/apps/governance/src/lib/get-multisig-status-info.spec.ts @@ -0,0 +1,70 @@ +import { + getMultisigStatusInfo, + MultisigStatus, +} from './get-multisig-status-info'; +import type { PreviousEpochQuery } from '../routes/staking/__generated__/PreviousEpoch'; + +const createNode = (id: string, multisigScore: string) => ({ + node: { + id, + stakedTotal: '1000', + rewardScore: { multisigScore }, + }, +}); + +describe('getMultisigStatus', () => { + it('should return MultisigStatus.noNodes when no nodes are present', () => { + const result = getMultisigStatusInfo({ + epoch: { id: '1', validatorsConnection: { edges: [] } }, + } as PreviousEpochQuery); + expect(result).toEqual({ + multisigStatus: MultisigStatus.noNodes, + showMultisigStatusError: true, + }); + }); + + it('should return MultisigStatus.correct when all nodes have multisigScore of 1', () => { + const result = getMultisigStatusInfo({ + epoch: { + id: '1', + validatorsConnection: { + edges: [createNode('1', '1'), createNode('2', '1')], + }, + }, + } as PreviousEpochQuery); + expect(result).toEqual({ + multisigStatus: MultisigStatus.correct, + showMultisigStatusError: false, + }); + }); + + it('should return MultisigStatus.nodeNeedsRemoving when all nodes have multisigScore of 0', () => { + const result = getMultisigStatusInfo({ + epoch: { + id: '1', + validatorsConnection: { + edges: [createNode('1', '0'), createNode('2', '0')], + }, + }, + } as PreviousEpochQuery); + expect(result).toEqual({ + multisigStatus: MultisigStatus.nodeNeedsRemoving, + showMultisigStatusError: true, + }); + }); + + it('should return MultisigStatus.nodeNeedsAdding when some nodes have multisigScore of 0 and others have 1', () => { + const result = getMultisigStatusInfo({ + epoch: { + id: '1', + validatorsConnection: { + edges: [createNode('1', '0'), createNode('2', '1')], + }, + }, + } as PreviousEpochQuery); + expect(result).toEqual({ + multisigStatus: MultisigStatus.nodeNeedsAdding, + showMultisigStatusError: true, + }); + }); +}); diff --git a/apps/governance/src/lib/get-multisig-status-info.ts b/apps/governance/src/lib/get-multisig-status-info.ts new file mode 100644 index 000000000..53352c6bf --- /dev/null +++ b/apps/governance/src/lib/get-multisig-status-info.ts @@ -0,0 +1,42 @@ +import { removePaginationWrapper } from '@vegaprotocol/utils'; +import type { PreviousEpochQuery } from '../routes/staking/__generated__/PreviousEpoch'; + +export enum MultisigStatus { + 'correct' = 'correct', + 'nodeNeedsAdding' = 'nodeNeedsAdding', + 'nodeNeedsRemoving' = 'nodeNeedsRemoving ', + 'noNodes' = 'noNodes', +} + +export const getMultisigStatusInfo = ( + previousEpochData: PreviousEpochQuery +) => { + let status = MultisigStatus.noNodes; + + const allNodesInPreviousEpoch = removePaginationWrapper( + previousEpochData?.epoch.validatorsConnection?.edges + ); + + const hasZero = allNodesInPreviousEpoch.some( + (node) => Number(node?.rewardScore?.multisigScore) === 0 + ); + const hasOne = allNodesInPreviousEpoch.some( + (node) => Number(node?.rewardScore?.multisigScore) === 1 + ); + + if (hasZero && hasOne) { + // If any individual node has 0 it means that node is missing from the multisig and needs to be added + status = MultisigStatus.nodeNeedsAdding; + } else if (hasZero) { + // If all nodes have 0 it means there is an incorrect address in the multisig that needs to be removed + status = MultisigStatus.nodeNeedsRemoving; + } else if (allNodesInPreviousEpoch.length > 0) { + // If all nodes have 1 it means the multisig is correct + status = MultisigStatus.correct; + } + + return { + showMultisigStatusError: status !== MultisigStatus.correct, + multisigStatus: status, + }; +}; diff --git a/apps/governance/src/routes/rewards/home/rewards-page.tsx b/apps/governance/src/routes/rewards/home/rewards-page.tsx index 7533ddfea..195716be4 100644 --- a/apps/governance/src/routes/rewards/home/rewards-page.tsx +++ b/apps/governance/src/routes/rewards/home/rewards-page.tsx @@ -23,6 +23,9 @@ import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch'; import { DocsLinks } from '@vegaprotocol/environment'; import { ConnectToSeeRewards } from '../connect-to-see-rewards'; import { EpochTotalRewards } from '../epoch-total-rewards/epoch-total-rewards'; +import { usePreviousEpochQuery } from '../../staking/__generated__/PreviousEpoch'; +import { getMultisigStatusInfo } from '../../../lib/get-multisig-status-info'; +import { MultisigIncorrectNotice } from '../../../components/multisig-incorrect-notice'; type RewardsView = 'total' | 'individual'; @@ -41,12 +44,25 @@ export const RewardsPage = () => { useRefreshAfterEpoch(epochData?.epoch.timestamps.expiry, refetch); + const { data: previousEpochData } = usePreviousEpochQuery({ + variables: { + epochId: (Number(epochData?.epoch.id) - 1).toString(), + }, + skip: !epochData?.epoch.id, + }); + + const multisigStatus = previousEpochData + ? getMultisigStatusInfo(previousEpochData) + : undefined; + const { params, loading: paramsLoading, error: paramsError, } = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]); + console.log('params', params); + const payoutDuration = useMemo(() => { if (!params) { return 0; @@ -78,14 +94,18 @@ export const RewardsPage = () => { )}

- {payoutDuration ? ( + {multisigStatus?.showMultisigStatusError ? ( + + ) : null} + + {!multisigStatus?.showMultisigStatusError && payoutDuration ? (

{t('rewardsCalloutDetail')}

diff --git a/apps/governance/src/routes/staking/home/epoch-data.tsx b/apps/governance/src/routes/staking/home/epoch-data.tsx index c828d2d38..ada3408d3 100644 --- a/apps/governance/src/routes/staking/home/epoch-data.tsx +++ b/apps/governance/src/routes/staking/home/epoch-data.tsx @@ -7,6 +7,8 @@ import { ValidatorTables } from './validator-tables'; import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch'; import { useVegaWallet } from '@vegaprotocol/wallet'; import { ENV } from '../../../config'; +import { getMultisigStatusInfo } from '../../../lib/get-multisig-status-info'; +import { MultisigIncorrectNotice } from '../../../components/multisig-incorrect-notice'; export const EpochData = () => { // errorPolicy due to vegaprotocol/vega issue 5898 @@ -46,12 +48,20 @@ export const EpochData = () => { userStakingRefetch(); }); + const multisigStatus = previousEpochData + ? getMultisigStatusInfo(previousEpochData) + : undefined; + return ( + {multisigStatus?.showMultisigStatusError ? ( + + ) : null} + {nodesData?.epoch && nodesData.epoch.timestamps.start && nodesData?.epoch.timestamps.expiry && ( diff --git a/libs/environment/src/hooks/use-links.ts b/libs/environment/src/hooks/use-links.ts index 164804b34..556fd0af7 100644 --- a/libs/environment/src/hooks/use-links.ts +++ b/libs/environment/src/hooks/use-links.ts @@ -72,6 +72,7 @@ export const DocsLinks = VEGA_DOCS_URL POSITION_RESOLUTION: `${VEGA_DOCS_URL}/concepts/trading-on-vega/market-protections#position-resolution`, LIQUIDITY: `${VEGA_DOCS_URL}/concepts/liquidity/provision`, WITHDRAWAL_LIMITS: `${VEGA_DOCS_URL}/concepts/assets/deposits-withdrawals#withdrawal-limits`, + VALIDATOR_SCORES_REWARDS: `${VEGA_DOCS_URL}/concepts/vega-chain/validator-scores-and-rewards`, } : undefined; From 878bed9c7ac9daba28df05e3f998f1205a6646b1 Mon Sep 17 00:00:00 2001 From: Art Date: Tue, 6 Jun 2023 23:21:48 +0200 Subject: [PATCH 19/49] fix(withdraws): minimal withdrawal amount validation (#3993) --- .../src/use-network-params.ts | 2 ++ libs/withdraws/src/lib/use-withdraw-asset.tsx | 27 ++++++++++++++++--- .../src/lib/withdraw-form-container.spec.tsx | 14 +++++++++- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/libs/network-parameters/src/use-network-params.ts b/libs/network-parameters/src/use-network-params.ts index bd598c85e..0f028ea31 100644 --- a/libs/network-parameters/src/use-network-params.ts +++ b/libs/network-parameters/src/use-network-params.ts @@ -99,6 +99,8 @@ export const NetworkParams = { governance_proposal_freeform_minProposerBalance: 'governance_proposal_freeform_minProposerBalance', validators_delegation_minAmount: 'validators_delegation_minAmount', + spam_protection_minimumWithdrawalQuantumMultiple: + 'spam_protection_minimumWithdrawalQuantumMultiple', spam_protection_voting_min_tokens: 'spam_protection_voting_min_tokens', spam_protection_proposal_min_tokens: 'spam_protection_proposal_min_tokens', market_liquidity_stakeToCcyVolume: 'market_liquidity_stakeToCcyVolume', diff --git a/libs/withdraws/src/lib/use-withdraw-asset.tsx b/libs/withdraws/src/lib/use-withdraw-asset.tsx index d65584d32..6d58a2621 100644 --- a/libs/withdraws/src/lib/use-withdraw-asset.tsx +++ b/libs/withdraws/src/lib/use-withdraw-asset.tsx @@ -3,13 +3,14 @@ import { addDecimal } from '@vegaprotocol/utils'; import { localLoggerFactory } from '@vegaprotocol/logger'; import * as Schema from '@vegaprotocol/types'; import BigNumber from 'bignumber.js'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import type { AccountFieldsFragment } from '@vegaprotocol/accounts'; import { useGetWithdrawDelay, useGetWithdrawThreshold, } from '@vegaprotocol/web3'; import { useWithdrawStore } from './withdraw-store'; +import { useNetworkParam } from '@vegaprotocol/network-parameters'; export const useWithdrawAsset = ( assets: Asset[], @@ -19,6 +20,17 @@ export const useWithdrawAsset = ( const { asset, balance, min, threshold, delay, update } = useWithdrawStore(); const getThreshold = useGetWithdrawThreshold(); const getDelay = useGetWithdrawDelay(); + const { param } = useNetworkParam( + 'spam_protection_minimumWithdrawalQuantumMultiple' + ); + + const minimumWithdrawalQuantumMultiple = useMemo(() => { + const factor = new BigNumber(param || ''); + if (factor.isNaN()) { + return new BigNumber(1); + } + return factor; + }, [param]); // Every time an asset is selected we need to find the corresponding // account, balance, min viable amount and delay threshold @@ -37,7 +49,9 @@ export const useWithdrawAsset = ( const min = asset ? BigNumber.max( new BigNumber(addDecimal('1', asset.decimals)), - new BigNumber(addDecimal(asset.quantum, asset.decimals)) + new BigNumber(addDecimal(asset.quantum, asset.decimals)).times( + minimumWithdrawalQuantumMultiple + ) ) : new BigNumber(0); // Query collateral bridge for threshold for selected asset @@ -56,7 +70,14 @@ export const useWithdrawAsset = ( update({ asset, balance, min, threshold, delay }); }, - [accounts, assets, update, getThreshold, getDelay] + [ + assets, + accounts, + minimumWithdrawalQuantumMultiple, + update, + getThreshold, + getDelay, + ] ); useEffect(() => { diff --git a/libs/withdraws/src/lib/withdraw-form-container.spec.tsx b/libs/withdraws/src/lib/withdraw-form-container.spec.tsx index 47f910080..69545b756 100644 --- a/libs/withdraws/src/lib/withdraw-form-container.spec.tsx +++ b/libs/withdraws/src/lib/withdraw-form-container.spec.tsx @@ -15,6 +15,18 @@ jest.mock('@vegaprotocol/data-provider', () => ({ })); jest.mock('@web3-react/core'); jest.mock('@vegaprotocol/accounts'); +jest.mock('@vegaprotocol/network-parameters', () => { + const impl = jest.requireActual('@vegaprotocol/network-parameters'); + return { + ...impl, + useNetworkParam: jest.fn((param) => { + if (param === 'spam_protection_minimumWithdrawalQuantumMultiple') { + return { param: '10.00', loading: false, error: undefined }; + } + return impl.useNetworkParam(param); + }), + }; +}); describe('WithdrawFormContainer', () => { const props = { @@ -99,7 +111,7 @@ describe('WithdrawFormContainer', () => { accountDecimals: null, }); }); - afterEach(() => { + afterAll(() => { jest.resetAllMocks(); }); it('should be properly rendered', async () => { From aae5c44fa4e8d2b79c19de67cf318440b0945e41 Mon Sep 17 00:00:00 2001 From: "m.ray" <16125548+MadalinaRaicu@users.noreply.github.com> Date: Wed, 7 Jun 2023 04:45:52 +0300 Subject: [PATCH 20/49] fix(trading): market selector volume update (#3989) --- .../src/integration/market-info.cy.ts | 1 - .../src/integration/market-selector.cy.ts | 18 +-- .../market/market-selector-item.spec.tsx | 112 ++++++++++++++---- .../market/market-selector-item.tsx | 24 ++-- .../market/use-market-selector-list.ts | 2 +- .../last-24h-price-change.tsx | 53 +-------- .../last-24h-volume/last-24h-volume.tsx | 35 +----- libs/markets/src/lib/hooks/index.ts | 1 + .../markets/src/lib/hooks/use-candles.spec.ts | 105 ++++++++++++++++ libs/markets/src/lib/hooks/use-candles.ts | 37 ++++++ 10 files changed, 271 insertions(+), 117 deletions(-) create mode 100644 libs/markets/src/lib/hooks/use-candles.spec.ts create mode 100644 libs/markets/src/lib/hooks/use-candles.ts diff --git a/apps/trading-e2e/src/integration/market-info.cy.ts b/apps/trading-e2e/src/integration/market-info.cy.ts index 849e557cc..2c3555ebc 100644 --- a/apps/trading-e2e/src/integration/market-info.cy.ts +++ b/apps/trading-e2e/src/integration/market-info.cy.ts @@ -57,7 +57,6 @@ describe('market info is displayed', { tags: '@smoke' }, () => { it('market volume displayed', () => { cy.getByTestId(marketTitle).contains('Market volume').click(); - validateMarketDataRow(0, '24 Hour Volume', 'Unknown'); validateMarketDataRow(1, 'Open Interest', '-'); validateMarketDataRow(2, 'Best Bid Volume', '1'); validateMarketDataRow(3, 'Best Offer Volume', '3'); diff --git a/apps/trading-e2e/src/integration/market-selector.cy.ts b/apps/trading-e2e/src/integration/market-selector.cy.ts index 6ef994263..8a5fa15f3 100644 --- a/apps/trading-e2e/src/integration/market-selector.cy.ts +++ b/apps/trading-e2e/src/integration/market-selector.cy.ts @@ -40,26 +40,26 @@ describe('markets selector', { tags: '@smoke' }, () => { { code: 'SOLUSD', markPrice: '84.41XYZalpha', - change: '+200.00%', - vol: '324h vol', + change: '', + vol: '0.0024h vol', }, { code: 'ETHBTC.QM21', markPrice: '46,126.90058tBTC', - change: '+200.00%', - vol: '324h vol', + change: '', + vol: '0.0024h vol', }, { code: 'BTCUSD.MF21', markPrice: '46,126.90058tDAI', - change: '+200.00%', - vol: '324h vol', + change: '', + vol: '0.0024h vol', }, { code: 'AAPL.MF21', markPrice: '46,126.90058tUSDC', - change: '+200.00%', - vol: '324h vol', + change: '', + vol: '0.0024h vol', }, ]; cy.getByTestId(list) @@ -80,7 +80,7 @@ describe('markets selector', { tags: '@smoke' }, () => { market.change ); // 6001-MARK-025 - expect(item.find('[data-testid="sparkline-svg"]')).to.exist; + expect(item.find('[data-testid="sparkline-svg"]')).to.not.exist; }); }); diff --git a/apps/trading/client-pages/market/market-selector-item.spec.tsx b/apps/trading/client-pages/market/market-selector-item.spec.tsx index d4c7b824f..f55b5540d 100644 --- a/apps/trading/client-pages/market/market-selector-item.spec.tsx +++ b/apps/trading/client-pages/market/market-selector-item.spec.tsx @@ -6,25 +6,32 @@ import { MemoryRouter } from 'react-router-dom'; import type { MockedResponse } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing'; import type { + MarketCandlesQuery, + MarketCandlesQueryVariables, MarketDataUpdateFieldsFragment, MarketDataUpdateSubscription, } from '@vegaprotocol/markets'; +import { MarketCandlesDocument } from '@vegaprotocol/markets'; import { MarketDataUpdateDocument } from '@vegaprotocol/markets'; import { AuctionTrigger, + Interval, MarketState, MarketTradingMode, } from '@vegaprotocol/types'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; +import { subDays } from 'date-fns'; describe('MarketSelectorItem', () => { + const yesterday = new Date(); + yesterday.setHours(yesterday.getHours() - 20); const market = createMarketFragment({ id: 'market-0', decimalPlaces: 2, // @ts-ignore fragment doesn't contain candles candles: [ - { close: '5', volume: '50' }, - { close: '10', volume: '50' }, + { close: '5', volume: '50', periodStart: yesterday.toISOString() }, + { close: '10', volume: '50', periodStart: yesterday.toISOString() }, ], tradableInstrument: { instrument: { @@ -36,6 +43,7 @@ describe('MarketSelectorItem', () => { }, }, }); + const marketData: MarketDataUpdateFieldsFragment = { __typename: 'ObservableMarketData', marketId: market.id, @@ -63,26 +71,32 @@ describe('MarketSelectorItem', () => { trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED, priceMonitoringBounds: null, }; - const mock: MockedResponse = { - request: { - query: MarketDataUpdateDocument, - variables: { - marketId: market.id, - }, + + const candles = [ + { + open: '5', + close: '5', + high: '5', + low: '5', + volume: '50', + periodStart: yesterday.toISOString(), }, - result: { - data: { - marketsData: [marketData], - }, + { + open: '10', + close: '10', + high: '10', + low: '10', + volume: '50', + periodStart: yesterday.toISOString(), }, - }; + ]; const mockOnSelect = jest.fn(); - const renderJsx = () => { + const renderJsx = (mocks: MockedResponse[]) => { return render( - + { ); }; + let dateSpy: jest.SpyInstance; + const ts = 1685577600000; // 2023-06-01 + + beforeAll(() => { + dateSpy = jest.spyOn(Date, 'now').mockImplementation(() => ts); + }); + + afterAll(() => { + dateSpy.mockRestore(); + }); + it('renders market information', async () => { const symbol = market.tradableInstrument.instrument.product.settlementAsset.symbol; - renderJsx(); + const mock: MockedResponse = { + request: { + query: MarketDataUpdateDocument, + variables: { + marketId: market.id, + }, + }, + result: { + data: { + marketsData: [marketData], + }, + }, + }; + + const since = subDays(Date.now(), 5).toISOString(); + const variables: MarketCandlesQueryVariables = { + marketId: market.id, + interval: Interval.INTERVAL_I1H, + since, + }; + const mockCandles: MockedResponse = { + request: { + query: MarketCandlesDocument, + variables, + }, + result: { + data: { + marketsConnection: { + edges: [ + { + node: { + candlesConnection: { + edges: candles.map((c) => ({ + node: c, + })), + }, + }, + }, + ], + }, + }, + }, + }; + + renderJsx([mock, mockCandles]); const link = screen.getByRole('link'); // link renders and is styled @@ -106,18 +175,17 @@ describe('MarketSelectorItem', () => { expect(link).toHaveClass('ring-1'); - expect(screen.getByTitle('24h vol')).toHaveTextContent('100'); + expect(screen.getByTitle('24h vol')).toHaveTextContent('0.00'); expect(screen.getByTitle(symbol)).toHaveTextContent('-'); - // candles are loaded immediately - expect(screen.getByTestId('market-item-change')).toHaveTextContent( - '+100.00%' - ); - await waitFor(() => { + expect(screen.getByTitle('24h vol')).toHaveTextContent('100'); expect(screen.getByTitle(symbol)).toHaveTextContent( addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces) ); + expect(screen.getByTestId('market-item-change')).toHaveTextContent( + '+100.00%' + ); }); await userEvent.click(link); diff --git a/apps/trading/client-pages/market/market-selector-item.tsx b/apps/trading/client-pages/market/market-selector-item.tsx index 99731e844..9b0b4f9cc 100644 --- a/apps/trading/client-pages/market/market-selector-item.tsx +++ b/apps/trading/client-pages/market/market-selector-item.tsx @@ -1,4 +1,4 @@ -import type { CSSProperties } from 'react'; +import type { CSSProperties, ReactNode } from 'react'; import { Link } from 'react-router-dom'; import classNames from 'classnames'; import { @@ -8,6 +8,7 @@ import { } from '@vegaprotocol/utils'; import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets'; import { calcCandleVolume } from '@vegaprotocol/markets'; +import { useCandles } from '@vegaprotocol/markets'; import { useMarketDataUpdateSubscription } from '@vegaprotocol/markets'; import { Sparkline } from '@vegaprotocol/ui-toolkit'; import { @@ -80,8 +81,9 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => { : ''; const instrument = market.tradableInstrument.instrument; + const { oneDayCandles } = useCandles({ marketId: market.id }); - const vol = market.candles ? calcCandleVolume(market.candles) : '0'; + const vol = oneDayCandles ? calcCandleVolume(oneDayCandles) : '0'; const volume = vol && vol !== '0' ? addDecimalsFormatNumber(vol, market.positionDecimalPlaces) @@ -111,20 +113,20 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => { value={price} label={instrument.product.settlementAsset.symbol} /> -
- {market.candles && ( - c.close)} /> +
+ {oneDayCandles && ( + c.close)} /> )}
- {market.candles && ( + {oneDayCandles && ( Number(c.close))} + data={oneDayCandles.map((c) => Number(c.close))} /> )}
@@ -133,7 +135,13 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => { ); }; -const DataRow = ({ value, label }: { value: string; label: string }) => { +const DataRow = ({ + value, + label, +}: { + value: string | ReactNode; + label: string; +}) => { return (
; } export const Last24hPriceChange = ({ marketId, decimalPlaces, initialValue, - inViewRoot, }: Props) => { - const [ref, inView] = useInView({ root: inViewRoot?.current }); - const fiveDaysAgo = useFiveDaysAgo(); - const yesterday = useYesterday(); - const { data, error } = useThrottledDataProvider({ - dataProvider: marketCandlesProvider, - variables: { - marketId: marketId || '', - interval: Schema.Interval.INTERVAL_I1H, - since: new Date(fiveDaysAgo).toISOString(), - }, - skip: !marketId || !inView, + const { oneDayCandles, error, fiveDaysCandles } = useCandles({ + marketId, }); - - const fiveDaysCandles = data?.filter((candle) => Boolean(candle)); - - const candles = fiveDaysCandles?.filter((candle) => - isCandleLessThan24hOld(candle, yesterday) - ); - const oneDayCandles = - candles - ?.map((candle) => candle?.close) - .filter((c): c is CandleClose => c !== null) || initialValue; - if ( fiveDaysCandles && fiveDaysCandles.length > 0 && @@ -68,30 +39,18 @@ export const Last24hPriceChange = ({ } > - {t('Unknown')} + - ); } if (error || !isNumeric(decimalPlaces)) { - return -; + return -; } return ( c.close) || initialValue || []} decimalPlaces={decimalPlaces} - ref={ref} /> ); }; - -export const isCandleLessThan24hOld = ( - candle: MarketCandlesFieldsFragment | undefined, - yesterday: number -) => { - if (!candle?.open) { - return false; - } - const candleDate = new Date(candle.close); - return candleDate > new Date(yesterday); -}; diff --git a/libs/markets/src/lib/components/last-24h-volume/last-24h-volume.tsx b/libs/markets/src/lib/components/last-24h-volume/last-24h-volume.tsx index 516d066b5..ab19b3d70 100644 --- a/libs/markets/src/lib/components/last-24h-volume/last-24h-volume.tsx +++ b/libs/markets/src/lib/components/last-24h-volume/last-24h-volume.tsx @@ -1,20 +1,13 @@ -import type { RefObject } from 'react'; -import { useInView } from 'react-intersection-observer'; -import { marketCandlesProvider } from '../../market-candles-provider'; import { calcCandleVolume } from '../../market-utils'; import { addDecimalsFormatNumber, isNumeric } from '@vegaprotocol/utils'; -import { useFiveDaysAgo, useYesterday } from '@vegaprotocol/react-helpers'; -import { useThrottledDataProvider } from '@vegaprotocol/data-provider'; -import * as Schema from '@vegaprotocol/types'; -import { isCandleLessThan24hOld } from '../last-24h-price-change'; import { t } from '@vegaprotocol/i18n'; import { Tooltip } from '@vegaprotocol/ui-toolkit'; +import { useCandles } from '../../hooks'; interface Props { marketId?: string; positionDecimalPlaces?: number; formatDecimals?: number; - inViewRoot?: RefObject; initialValue?: string; } @@ -22,29 +15,12 @@ export const Last24hVolume = ({ marketId, positionDecimalPlaces, formatDecimals, - inViewRoot, initialValue, }: Props) => { - const yesterday = useYesterday(); - const fiveDaysAgo = useFiveDaysAgo(); - const [ref, inView] = useInView({ root: inViewRoot?.current }); - - const { data } = useThrottledDataProvider({ - dataProvider: marketCandlesProvider, - variables: { - marketId: marketId || '', - interval: Schema.Interval.INTERVAL_I1H, - since: new Date(fiveDaysAgo).toISOString(), - }, - skip: !(inView && marketId), + const { oneDayCandles, fiveDaysCandles } = useCandles({ + marketId, }); - const fiveDaysCandles = data?.filter((candle) => Boolean(candle)); - - const oneDayCandles = fiveDaysCandles?.filter((candle) => - isCandleLessThan24hOld(candle, yesterday) - ); - if ( fiveDaysCandles && fiveDaysCandles.length > 0 && @@ -72,20 +48,21 @@ export const Last24hVolume = ({
} > - {t('Unknown')} + - ); } const candleVolume = oneDayCandles ? calcCandleVolume(oneDayCandles) : initialValue; + return ( - + {candleVolume && isNumeric(positionDecimalPlaces) ? addDecimalsFormatNumber( candleVolume, diff --git a/libs/markets/src/lib/hooks/index.ts b/libs/markets/src/lib/hooks/index.ts index 8c84cd40c..28dbecf18 100644 --- a/libs/markets/src/lib/hooks/index.ts +++ b/libs/markets/src/lib/hooks/index.ts @@ -2,3 +2,4 @@ export * from './use-market-oracle'; export * from './use-oracle-markets'; export * from './use-oracle-proofs'; export * from './use-oracle-spec-binding-data'; +export * from './use-candles'; diff --git a/libs/markets/src/lib/hooks/use-candles.spec.ts b/libs/markets/src/lib/hooks/use-candles.spec.ts new file mode 100644 index 000000000..2bbe50b80 --- /dev/null +++ b/libs/markets/src/lib/hooks/use-candles.spec.ts @@ -0,0 +1,105 @@ +import { renderHook } from '@testing-library/react'; +import { useCandles } from './use-candles'; + +const today = new Date(); +const fiveDaysAgo = new Date(); +fiveDaysAgo.setDate(today.getDate() - 5); + +const mockData = [ + { + high: '6293819', + low: '6263737', + open: '6266893', + close: '6293819', + volume: '72447', + periodStart: today.toISOString(), + __typename: 'Candle', + }, + null, + { + high: '6309988', + low: '6296335', + open: '6307451', + close: '6296335', + volume: '73657', + periodStart: today.toISOString(), + __typename: 'Candle', + }, + { + high: '6315153', + low: '6294001', + open: '6296335', + close: '6315152', + volume: '89395', + periodStart: today.toISOString(), + __typename: 'Candle', + }, + { + high: '6309988', + low: '6296335', + open: '6307451', + close: '6296335', + volume: '73657', + periodStart: fiveDaysAgo.toISOString(), + __typename: 'Candle', + }, + { + high: '6315153', + low: '6294001', + open: '6296335', + close: '6315152', + volume: '89395', + periodStart: fiveDaysAgo.toISOString(), + __typename: 'Candle', + }, +]; + +jest.mock('@vegaprotocol/data-provider', () => { + return { + ...jest.requireActual('@vegaprotocol/data-provider'), + useThrottledDataProvider: jest.fn(() => ({ + data: mockData, + error: false, + })), + }; +}); + +describe('useCandles', () => { + it('should return one day candles and five day candles', () => { + const { result } = renderHook(() => useCandles({ marketId: '3456789' })); + const expectedOneDayCandles = [ + { + high: '6293819', + low: '6263737', + open: '6266893', + close: '6293819', + volume: '72447', + periodStart: today.toISOString(), + __typename: 'Candle', + }, + { + high: '6309988', + low: '6296335', + open: '6307451', + close: '6296335', + volume: '73657', + periodStart: today.toISOString(), + __typename: 'Candle', + }, + { + high: '6315153', + low: '6294001', + open: '6296335', + close: '6315152', + volume: '89395', + periodStart: today.toISOString(), + __typename: 'Candle', + }, + ]; + expect(result.current).toStrictEqual({ + oneDayCandles: expectedOneDayCandles, + fiveDaysCandles: mockData.filter(Boolean), + error: false, + }); + }); +}); diff --git a/libs/markets/src/lib/hooks/use-candles.ts b/libs/markets/src/lib/hooks/use-candles.ts new file mode 100644 index 000000000..72b43aceb --- /dev/null +++ b/libs/markets/src/lib/hooks/use-candles.ts @@ -0,0 +1,37 @@ +import { useThrottledDataProvider } from '@vegaprotocol/data-provider'; +import { useFiveDaysAgo, useYesterday } from '@vegaprotocol/react-helpers'; +import type { MarketCandlesFieldsFragment } from '../__generated__'; +import { marketCandlesProvider } from '../market-candles-provider'; +import { Interval } from '@vegaprotocol/types'; + +export const useCandles = ({ marketId }: { marketId?: string }) => { + const fiveDaysAgo = useFiveDaysAgo(); + const yesterday = useYesterday(); + const { data, error } = useThrottledDataProvider({ + dataProvider: marketCandlesProvider, + variables: { + marketId: marketId || '', + interval: Interval.INTERVAL_I1H, + since: new Date(fiveDaysAgo).toISOString(), + }, + skip: !marketId, + }); + + const fiveDaysCandles = data?.filter(Boolean); + + const oneDayCandles = fiveDaysCandles?.filter((candle) => + isCandleLessThan24hOld(candle, yesterday) + ); + return { oneDayCandles, error, fiveDaysCandles }; +}; + +export const isCandleLessThan24hOld = ( + candle: MarketCandlesFieldsFragment | undefined, + yesterday: number +) => { + if (!candle?.periodStart) { + return false; + } + const candleDate = new Date(candle.periodStart); + return candleDate > new Date(yesterday); +}; From 43d3754c64977b66dcf431dce124b744e13c61af Mon Sep 17 00:00:00 2001 From: Maciek Date: Wed, 7 Jun 2023 10:58:34 +0200 Subject: [PATCH 21/49] feat(trading): 3945 orderbook enhancements (#4016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Głownia Co-authored-by: Matthew Russell --- .../client-pages/market/trade-grid.tsx | 2 +- libs/datagrid/src/lib/cells/numeric-cell.tsx | 9 +- .../src/lib/cells/order-type-cell.tsx | 2 +- libs/datagrid/src/lib/cells/price-cell.tsx | 7 +- .../src/lib/orderbook-data.spec.ts | 231 +---- libs/market-depth/src/lib/orderbook-data.ts | 283 +------ .../src/lib/orderbook-manager.tsx | 149 +--- libs/market-depth/src/lib/orderbook-row.tsx | 206 +++-- libs/market-depth/src/lib/orderbook.spec.tsx | 265 ++---- .../src/lib/orderbook.stories.tsx | 9 +- libs/market-depth/src/lib/orderbook.tsx | 792 ++++-------------- 11 files changed, 378 insertions(+), 1577 deletions(-) diff --git a/apps/trading/client-pages/market/trade-grid.tsx b/apps/trading/client-pages/market/trade-grid.tsx index d726c57a6..632c87b35 100644 --- a/apps/trading/client-pages/market/trade-grid.tsx +++ b/apps/trading/client-pages/market/trade-grid.tsx @@ -296,7 +296,7 @@ const MainGrid = memo( diff --git a/libs/datagrid/src/lib/cells/numeric-cell.tsx b/libs/datagrid/src/lib/cells/numeric-cell.tsx index 0164cea8c..bdc0c4e31 100644 --- a/libs/datagrid/src/lib/cells/numeric-cell.tsx +++ b/libs/datagrid/src/lib/cells/numeric-cell.tsx @@ -1,10 +1,12 @@ import { forwardRef } from 'react'; +import classNames from 'classnames'; import { getDecimalSeparator, isNumeric } from '@vegaprotocol/utils'; interface NumericCellProps { value: number | bigint | null | undefined; valueFormatted: string; testId?: string; + className?: string; } /** @@ -12,7 +14,7 @@ interface NumericCellProps { * use, right aligned, monospace and decimals deemphasised */ export const NumericCell = forwardRef( - ({ value, valueFormatted, testId }, ref) => { + ({ value, valueFormatted, testId, className }, ref) => { if (!isNumeric(value)) { return ( @@ -29,7 +31,10 @@ export const NumericCell = forwardRef( return ( diff --git a/libs/datagrid/src/lib/cells/order-type-cell.tsx b/libs/datagrid/src/lib/cells/order-type-cell.tsx index 127c6af1b..72d155eec 100644 --- a/libs/datagrid/src/lib/cells/order-type-cell.tsx +++ b/libs/datagrid/src/lib/cells/order-type-cell.tsx @@ -16,7 +16,7 @@ export const OrderTypeCell = ({ data: order, onClick, }: OrderTypeCellProps) => { - const id = order ? order.market.id : ''; + const id = order?.market?.id ?? ''; const label = useMemo(() => { if (!order) { diff --git a/libs/datagrid/src/lib/cells/price-cell.tsx b/libs/datagrid/src/lib/cells/price-cell.tsx index 56a5d772b..5880162c1 100644 --- a/libs/datagrid/src/lib/cells/price-cell.tsx +++ b/libs/datagrid/src/lib/cells/price-cell.tsx @@ -6,11 +6,15 @@ export interface IPriceCellProps { valueFormatted: string; testId?: string; onClick?: (price?: string | number) => void; + className?: string; } export const PriceCell = memo( forwardRef( - ({ value, valueFormatted, testId, onClick }: IPriceCellProps, ref) => { + ( + { value, valueFormatted, testId, onClick, className }: IPriceCellProps, + ref + ) => { if (!isNumeric(value)) { return ( @@ -27,6 +31,7 @@ export const PriceCell = memo( value={value} valueFormatted={valueFormatted} testId={testId || 'price'} + className={className} /> ) : ( diff --git a/libs/market-depth/src/lib/orderbook-data.spec.ts b/libs/market-depth/src/lib/orderbook-data.spec.ts index 4f38a696d..871ca4145 100644 --- a/libs/market-depth/src/lib/orderbook-data.spec.ts +++ b/libs/market-depth/src/lib/orderbook-data.spec.ts @@ -1,9 +1,4 @@ -import { - compactRows, - updateLevels, - updateCompactedRows, -} from './orderbook-data'; -import type { OrderbookRowData } from './orderbook-data'; +import { compactRows, updateLevels, VolumeType } from './orderbook-data'; import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth'; describe('compactRows', () => { @@ -26,51 +21,31 @@ describe('compactRows', () => { numberOfOrders: (numberOfRows - i).toString(), })); it('groups data by price and resolution', () => { - expect(compactRows(sell, buy, 1).length).toEqual(200); - expect(compactRows(sell, buy, 5).length).toEqual(41); - expect(compactRows(sell, buy, 10).length).toEqual(21); + expect(compactRows(sell, VolumeType.ask, 1).length).toEqual(100); + expect(compactRows(buy, VolumeType.bid, 1).length).toEqual(100); + expect(compactRows(sell, VolumeType.ask, 5).length).toEqual(21); + expect(compactRows(buy, VolumeType.bid, 5).length).toEqual(21); + expect(compactRows(sell, VolumeType.ask, 10).length).toEqual(11); + expect(compactRows(buy, VolumeType.bid, 10).length).toEqual(11); }); it('counts cumulative vol', () => { - const orderbookRows = compactRows(sell, buy, 10); - expect(orderbookRows[0].cumulativeVol.ask).toEqual(4950); - expect(orderbookRows[0].cumulativeVol.bid).toEqual(0); - expect(orderbookRows[10].cumulativeVol.ask).toEqual(390); - expect(orderbookRows[10].cumulativeVol.bid).toEqual(579); - expect(orderbookRows[orderbookRows.length - 1].cumulativeVol.bid).toEqual( - 4950 - ); - expect(orderbookRows[orderbookRows.length - 1].cumulativeVol.ask).toEqual( - 0 - ); - }); - it('stores volume by level', () => { - const orderbookRows = compactRows(sell, buy, 10); - expect(orderbookRows[0].askByLevel).toEqual({ - '1095': 5, - '1096': 4, - '1097': 3, - '1098': 2, - '1099': 1, - }); - expect(orderbookRows[orderbookRows.length - 1].bidByLevel).toEqual({ - '902': 1, - '903': 2, - '904': 3, - }); + 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 orderbookRows = compactRows(sell, buy, 10); - expect(orderbookRows[0].cumulativeVol.relativeAsk).toEqual(100); - expect(orderbookRows[0].cumulativeVol.relativeBid).toEqual(0); - expect(orderbookRows[0].relativeAsk).toEqual(2); - expect(orderbookRows[0].relativeBid).toEqual(0); - expect(orderbookRows[10].cumulativeVol.relativeAsk).toEqual(8); - expect(orderbookRows[10].cumulativeVol.relativeBid).toEqual(12); - expect(orderbookRows[10].relativeAsk).toEqual(44); - expect(orderbookRows[10].relativeBid).toEqual(64); - expect(orderbookRows[orderbookRows.length - 1].relativeAsk).toEqual(0); - expect(orderbookRows[orderbookRows.length - 1].relativeBid).toEqual(1); + 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); }); }); @@ -130,167 +105,3 @@ describe('updateLevels', () => { expect(updateLevels([], [updateLastRow])).toEqual([updateLastRow]); }); }); - -describe('updateCompactedRows', () => { - const orderbookRows: OrderbookRowData[] = [ - { - price: '120', - cumulativeVol: { - ask: 50, - relativeAsk: 100, - bid: 0, - relativeBid: 0, - }, - askByLevel: { - '121': 10, - }, - bidByLevel: {}, - ask: 10, - bid: 0, - relativeAsk: 25, - relativeBid: 0, - }, - { - price: '100', - cumulativeVol: { - ask: 40, - relativeAsk: 80, - bid: 40, - relativeBid: 80, - }, - askByLevel: { - '101': 10, - '102': 30, - }, - bidByLevel: { - '99': 10, - '98': 30, - }, - ask: 40, - bid: 40, - relativeAsk: 100, - relativeBid: 100, - }, - { - price: '80', - cumulativeVol: { - ask: 0, - relativeAsk: 0, - bid: 50, - relativeBid: 100, - }, - askByLevel: {}, - bidByLevel: { - '79': 10, - }, - ask: 0, - bid: 10, - relativeAsk: 0, - relativeBid: 25, - }, - ]; - const resolution = 10; - - it('update volume', () => { - const sell: PriceLevelFieldsFragment = { - __typename: 'PriceLevel', - price: '120', - volume: '10', - numberOfOrders: '10', - }; - const buy: PriceLevelFieldsFragment = { - __typename: 'PriceLevel', - price: '80', - volume: '10', - numberOfOrders: '10', - }; - const updatedRows = updateCompactedRows( - orderbookRows, - [sell], - [buy], - resolution - ); - expect(updatedRows[0].ask).toEqual(20); - expect(updatedRows[0].askByLevel?.[120]).toEqual(10); - expect(updatedRows[0].cumulativeVol.ask).toEqual(60); - expect(updatedRows[2].bid).toEqual(20); - expect(updatedRows[2].bidByLevel?.[80]).toEqual(10); - expect(updatedRows[2].cumulativeVol.bid).toEqual(60); - }); - - it('remove row', () => { - const sell: PriceLevelFieldsFragment = { - __typename: 'PriceLevel', - price: '121', - volume: '0', - numberOfOrders: '0', - }; - const buy: PriceLevelFieldsFragment = { - __typename: 'PriceLevel', - price: '79', - volume: '0', - numberOfOrders: '0', - }; - const updatedRows = updateCompactedRows( - orderbookRows, - [sell], - [buy], - resolution - ); - expect(updatedRows.length).toEqual(1); - }); - - it('add new row at the end', () => { - const sell: PriceLevelFieldsFragment = { - __typename: 'PriceLevel', - price: '131', - volume: '5', - numberOfOrders: '5', - }; - const buy: PriceLevelFieldsFragment = { - __typename: 'PriceLevel', - price: '59', - volume: '5', - numberOfOrders: '5', - }; - const updatedRows = updateCompactedRows( - orderbookRows, - [sell], - [buy], - resolution - ); - expect(updatedRows.length).toEqual(5); - expect(updatedRows[0].price).toEqual('130'); - expect(updatedRows[0].cumulativeVol.ask).toEqual(55); - expect(updatedRows[4].price).toEqual('60'); - expect(updatedRows[4].cumulativeVol.bid).toEqual(55); - }); - - it('add new row in the middle', () => { - const sell: PriceLevelFieldsFragment = { - __typename: 'PriceLevel', - price: '111', - volume: '5', - numberOfOrders: '5', - }; - const buy: PriceLevelFieldsFragment = { - __typename: 'PriceLevel', - price: '91', - volume: '5', - numberOfOrders: '5', - }; - const updatedRows = updateCompactedRows( - orderbookRows, - [sell], - [buy], - resolution - ); - expect(updatedRows.length).toEqual(5); - expect(updatedRows[1].price).toEqual('110'); - expect(updatedRows[1].cumulativeVol.ask).toEqual(45); - expect(updatedRows[0].cumulativeVol.ask).toEqual(55); - expect(updatedRows[3].price).toEqual('90'); - expect(updatedRows[3].cumulativeVol.bid).toEqual(45); - expect(updatedRows[4].cumulativeVol.bid).toEqual(55); - }); -}); diff --git a/libs/market-depth/src/lib/orderbook-data.ts b/libs/market-depth/src/lib/orderbook-data.ts index f0d2ad0ad..8d54ae2e2 100644 --- a/libs/market-depth/src/lib/orderbook-data.ts +++ b/libs/market-depth/src/lib/orderbook-data.ts @@ -1,9 +1,4 @@ import groupBy from 'lodash/groupBy'; -import uniqBy from 'lodash/uniqBy'; -import reverse from 'lodash/reverse'; -import cloneDeep from 'lodash/cloneDeep'; -import * as Schema from '@vegaprotocol/types'; -import type { MarketData } from '@vegaprotocol/markets'; import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth'; export enum VolumeType { @@ -11,39 +6,16 @@ export enum VolumeType { ask, } export interface CumulativeVol { - bid: number; - relativeBid?: number; - ask: number; - relativeAsk?: number; + value: number; + relativeValue?: number; } export interface OrderbookRowData { price: string; - bid: number; - bidByLevel: Record; - relativeBid?: number; - ask: number; - askByLevel: Record; - relativeAsk?: number; + value: number; cumulativeVol: CumulativeVol; } -type PartialOrderbookRowData = Pick; - -type OrderbookMarketData = Pick< - MarketData, - | 'bestStaticBidPrice' - | 'bestStaticOfferPrice' - | 'indicativePrice' - | 'indicativeVolume' - | 'marketTradingMode' ->; - -export type OrderbookData = Partial & { - rows: OrderbookRowData[] | null; - midPrice?: string; -}; - export const getPriceLevel = (price: string | bigint, resolution: number) => { const p = BigInt(price); const r = BigInt(resolution); @@ -54,135 +26,66 @@ export const getPriceLevel = (price: string | bigint, resolution: number) => { return priceLevel.toString(); }; -export const getMidPrice = ( - sell: PriceLevelFieldsFragment[] | null | undefined, - buy: PriceLevelFieldsFragment[] | null | undefined, - resolution: number -) => - buy?.length && sell?.length - ? getPriceLevel( - (BigInt(buy[0].price) + BigInt(sell[0].price)) / BigInt(2), - resolution - ) - : undefined; - const getMaxVolumes = (orderbookData: OrderbookRowData[]) => ({ - bid: Math.max(...orderbookData.map((data) => data.bid)), - ask: Math.max(...orderbookData.map((data) => data.ask)), cumulativeVol: Math.max( - orderbookData[0]?.cumulativeVol.ask, - orderbookData[orderbookData.length - 1]?.cumulativeVol.bid + 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); -/** - * @summary Updates relativeAsk, relativeBid, cumulativeVol.relativeAsk, cumulativeVol.relativeBid - */ const updateRelativeData = (data: OrderbookRowData[]) => { - const { bid, ask, cumulativeVol } = getMaxVolumes(data); - const maxBidAsk = Math.max(bid, ask); + const { cumulativeVol } = getMaxVolumes(data); data.forEach((data, i) => { - data.relativeAsk = toPercentValue(data.ask / maxBidAsk); - data.relativeBid = toPercentValue(data.bid / maxBidAsk); - data.cumulativeVol.relativeAsk = toPercentValue( - data.cumulativeVol.ask / cumulativeVol - ); - data.cumulativeVol.relativeBid = toPercentValue( - data.cumulativeVol.bid / cumulativeVol + data.cumulativeVol.relativeValue = toPercentValue( + data.cumulativeVol.value / cumulativeVol ); }); }; -const updateCumulativeVolume = (data: OrderbookRowData[]) => { - if (data.length > 1) { +const updateCumulativeVolumeByType = ( + data: OrderbookRowData[], + dataType: VolumeType +) => { + if (data.length) { const maxIndex = data.length - 1; - for (let i = 0; i <= maxIndex; i++) { - data[i].cumulativeVol.bid = - data[i].bid + (i !== 0 ? data[i - 1].cumulativeVol.bid : 0); - } - for (let i = maxIndex; i >= 0; i--) { - data[i].cumulativeVol.ask = - data[i].ask + (i !== maxIndex ? data[i + 1].cumulativeVol.ask : 0); + 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); + } + } else { + for (let i = maxIndex; i >= 0; i--) { + data[i].cumulativeVol.value = + data[i].value + + (i !== maxIndex ? data[i + 1].cumulativeVol.value : 0); + } } } }; -export const createPartialRow = ( - price: string, - volume = 0, - dataType?: VolumeType -): PartialOrderbookRowData => ({ - price, - ask: dataType === VolumeType.ask ? volume : 0, - bid: dataType === VolumeType.bid ? volume : 0, -}); - -export const extendRow = (row: PartialOrderbookRowData): OrderbookRowData => - Object.assign(row, { - cumulativeVol: { - ask: 0, - bid: 0, - }, - askByLevel: row.ask ? { [row.price]: row.ask } : {}, - bidByLevel: row.bid ? { [row.price]: row.bid } : {}, - }); - -export const createRow = ( - price: string, - volume = 0, - dataType?: VolumeType -): OrderbookRowData => extendRow(createPartialRow(price, volume, dataType)); - -const mapRawData = - (dataType: VolumeType.ask | VolumeType.bid) => - (data: PriceLevelFieldsFragment): PartialOrderbookRowData => - createPartialRow(data.price, Number(data.volume), dataType); - -/** - * @summary merges sell amd buy data, orders by price desc, group by price level, counts cumulative and relative values - */ export const compactRows = ( - sell: PriceLevelFieldsFragment[] | null | undefined, - buy: PriceLevelFieldsFragment[] | null | undefined, + data: PriceLevelFieldsFragment[] | null | undefined, + dataType: VolumeType, resolution: number ) => { - // map raw sell data to OrderbookData - const askOrderbookData = [...(sell ?? [])].map( - mapRawData(VolumeType.ask) - ); - // map raw buy data to OrderbookData - const bidOrderbookData = [...(buy ?? [])].map( - mapRawData(VolumeType.bid) - ); - // group by price level - const groupedByLevel = groupBy( - [...askOrderbookData, ...bidOrderbookData], - (row) => getPriceLevel(row.price, resolution) + const groupedByLevel = groupBy(data, (row) => + getPriceLevel(row.price, resolution) ); const orderbookData: OrderbookRowData[] = []; Object.keys(groupedByLevel).forEach((price) => { - const row = extendRow( - groupedByLevel[price].pop() as PartialOrderbookRowData - ); - row.price = price; - let subRow: PartialOrderbookRowData | undefined = - groupedByLevel[price].pop(); + const { volume } = groupedByLevel[price].pop() as PriceLevelFieldsFragment; + let value = Number(volume); + let subRow: { volume: string } | undefined = groupedByLevel[price].pop(); while (subRow) { - row.ask += subRow.ask; - row.bid += subRow.bid; - if (subRow.ask) { - row.askByLevel[subRow.price] = subRow.ask; - } - if (subRow.bid) { - row.bidByLevel[subRow.price] = subRow.bid; - } + value += Number(subRow.volume); subRow = groupedByLevel[price].pop(); } - orderbookData.push(row); + orderbookData.push({ price, value, cumulativeVol: { value: 0 } }); }); + orderbookData.sort((a, b) => { if (a === b) { return 0; @@ -192,100 +95,11 @@ export const compactRows = ( } return 1; }); - // count cumulative volumes - if (orderbookData.length > 1) { - const maxIndex = orderbookData.length - 1; - for (let i = 0; i <= maxIndex; i++) { - orderbookData[i].cumulativeVol.bid = - orderbookData[i].bid + - (i !== 0 ? orderbookData[i - 1].cumulativeVol.bid : 0); - } - for (let i = maxIndex; i >= 0; i--) { - orderbookData[i].cumulativeVol.ask = - orderbookData[i].ask + - (i !== maxIndex ? orderbookData[i + 1].cumulativeVol.ask : 0); - } - } - updateCumulativeVolume(orderbookData); - // count relative volumes + updateCumulativeVolumeByType(orderbookData, dataType); updateRelativeData(orderbookData); return orderbookData; }; -/** - * - * @param type - * @param draft - * @param delta - * @param resolution - * @param modifiedIndex - * @returns max (sell) or min (buy) modified index in draft data, mutates draft - */ -const partiallyUpdateCompactedRows = ( - dataType: VolumeType, - data: OrderbookRowData[], - delta: PriceLevelFieldsFragment, - resolution: number -) => { - const { price } = delta; - const volume = Number(delta.volume); - const priceLevel = getPriceLevel(price, resolution); - const isAskDataType = dataType === VolumeType.ask; - const volKey = isAskDataType ? 'ask' : 'bid'; - const volByLevelKey = isAskDataType ? 'askByLevel' : 'bidByLevel'; - let index = data.findIndex((row) => row.price === priceLevel); - if (index !== -1) { - data[index][volKey] = - data[index][volKey] - (data[index][volByLevelKey][price] || 0) + volume; - data[index][volByLevelKey][price] = volume; - } else { - const newData: OrderbookRowData = createRow(priceLevel, volume, dataType); - index = data.findIndex((row) => BigInt(row.price) < BigInt(priceLevel)); - if (index !== -1) { - data.splice(index, 0, newData); - } else { - data.push(newData); - } - } -}; - -/** - * Updates OrderbookData[] with new data received from subscription - mutates input - * - * @param rows - * @param sell - * @param buy - * @param resolution - * @returns void - */ -export const updateCompactedRows = ( - rows: Readonly, - sell: Readonly | null, - buy: Readonly | null, - resolution: number -) => { - const data = cloneDeep(rows as OrderbookRowData[]); - uniqBy(reverse(sell || []), 'price')?.forEach((delta) => { - partiallyUpdateCompactedRows(VolumeType.ask, data, delta, resolution); - }); - uniqBy(reverse(buy || []), 'price')?.forEach((delta) => { - partiallyUpdateCompactedRows(VolumeType.bid, data, delta, resolution); - }); - updateCumulativeVolume(data); - let index = 0; - // remove levels that do not have any volume - while (index < data.length) { - if (!data[index].ask && !data[index].bid) { - data.splice(index, 1); - } else { - index += 1; - } - } - // count relative volumes - updateRelativeData(data); - return data; -}; - /** * Updates raw data with new data received from subscription - mutates input * @param levels @@ -326,12 +140,9 @@ export interface MockDataGeneratorParams { numberOfSellRows: number; numberOfBuyRows: number; overlap: number; - midPrice: number; + midPrice?: string; bestStaticBidPrice: number; bestStaticOfferPrice: number; - indicativePrice?: number; - indicativeVolume?: number; - resolution: number; } export const generateMockData = ({ @@ -341,12 +152,10 @@ export const generateMockData = ({ overlap, bestStaticBidPrice, bestStaticOfferPrice, - indicativePrice, - indicativeVolume, - resolution, }: MockDataGeneratorParams) => { let matrix = new Array(numberOfSellRows).fill(undefined); - let price = midPrice + (numberOfSellRows - Math.ceil(overlap / 2) + 1); + let price = + Number(midPrice) + (numberOfSellRows - Math.ceil(overlap / 2) + 1); const sell: PriceLevelFieldsFragment[] = matrix.map((row, i) => ({ price: (price -= 1).toString(), volume: (numberOfSellRows - i + 1).toString(), @@ -359,21 +168,11 @@ export const generateMockData = ({ volume: (i + 2).toString(), numberOfOrders: '', })); - const rows = compactRows(sell, buy, resolution); - const marketTradingMode = - overlap > 0 - ? Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION - : Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS; return { - rows, - resolution, - indicativeVolume: indicativeVolume?.toString(), - marketTradingMode, - midPrice: ((bestStaticBidPrice + bestStaticOfferPrice) / 2).toString(), + asks: sell, + bids: buy, + midPrice, bestStaticBidPrice: bestStaticBidPrice.toString(), bestStaticOfferPrice: bestStaticOfferPrice.toString(), - indicativePrice: indicativePrice - ? getPriceLevel(indicativePrice.toString(), resolution) - : undefined, }; }; diff --git a/libs/market-depth/src/lib/orderbook-manager.tsx b/libs/market-depth/src/lib/orderbook-manager.tsx index 90c6f3a40..486d7064b 100644 --- a/libs/market-depth/src/lib/orderbook-manager.tsx +++ b/libs/market-depth/src/lib/orderbook-manager.tsx @@ -1,119 +1,34 @@ -import throttle from 'lodash/throttle'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { Orderbook } from './orderbook'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { marketDepthProvider } from './market-depth-provider'; import { marketDataProvider, marketProvider } from '@vegaprotocol/markets'; -import type { MarketData } from '@vegaprotocol/markets'; -import { useCallback, useEffect, useRef, useState } from 'react'; import type { - MarketDepthUpdateSubscription, MarketDepthQuery, MarketDepthQueryVariables, + MarketDepthUpdateSubscription, + PriceLevelFieldsFragment, } from './__generated__/MarketDepth'; -import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth'; -import { - compactRows, - updateCompactedRows, - getMidPrice, - getPriceLevel, -} from './orderbook-data'; -import type { OrderbookData } from './orderbook-data'; import { useOrderStore } from '@vegaprotocol/orders'; +export type OrderbookData = { + asks: PriceLevelFieldsFragment[]; + bids: PriceLevelFieldsFragment[]; +}; + interface OrderbookManagerProps { marketId: string; } export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => { - const [resolution, setResolution] = useState(1); const variables = { marketId }; - const resolutionRef = useRef(resolution); - const [orderbookData, setOrderbookData] = useState({ - rows: null, - }); - const dataRef = useRef({ rows: null }); - const marketDataRef = useRef(null); - const rawDataRef = useRef(null); - const deltaRef = useRef<{ - sell: PriceLevelFieldsFragment[]; - buy: PriceLevelFieldsFragment[]; - }>({ - sell: [], - buy: [], - }); - const updateOrderbookData = useRef( - throttle(() => { - dataRef.current = { - ...marketDataRef.current, - indicativePrice: - marketDataRef.current?.indicativePrice && - getPriceLevel( - marketDataRef.current.indicativePrice, - resolutionRef.current - ), - midPrice: getMidPrice( - rawDataRef.current?.depth.sell, - rawDataRef.current?.depth.buy, - resolution - ), - rows: - deltaRef.current.buy.length || deltaRef.current.sell.length - ? updateCompactedRows( - dataRef.current.rows ?? [], - deltaRef.current.sell, - deltaRef.current.buy, - resolutionRef.current - ) - : dataRef.current.rows, - }; - deltaRef.current.buy = []; - deltaRef.current.sell = []; - setOrderbookData(dataRef.current); - }, 250) - ); - useEffect(() => { - deltaRef.current.buy = []; - deltaRef.current.sell = []; - }, [marketId]); - - const update = useCallback( - ({ - delta: deltas, - data: rawData, - }: { - delta?: MarketDepthUpdateSubscription['marketsDepthUpdate'] | null; - data: NonNullable | null | undefined; - }) => { - if (!dataRef.current.rows) { - return false; - } - for (const delta of deltas || []) { - if (delta.marketId !== marketId) { - continue; - } - if (delta.sell) { - deltaRef.current.sell.push(...delta.sell); - } - if (delta.buy) { - deltaRef.current.buy.push(...delta.buy); - } - rawDataRef.current = rawData; - updateOrderbookData.current(); - } - return true; - }, - [marketId, updateOrderbookData] - ); - - const { data, error, loading, flush, reload } = useDataProvider< + const { data, error, loading, reload } = useDataProvider< MarketDepthQuery['market'] | undefined, MarketDepthUpdateSubscription['marketsDepthUpdate'] | null, MarketDepthQueryVariables >({ dataProvider: marketDepthProvider, - update, variables, }); @@ -127,57 +42,15 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => { variables, }); - const marketDataUpdate = useCallback( - ({ data }: { data: MarketData | null }) => { - marketDataRef.current = data; - updateOrderbookData.current(); - return true; - }, - [] - ); - const { data: marketData, error: marketDataError, loading: marketDataLoading, } = useDataProvider({ dataProvider: marketDataProvider, - update: marketDataUpdate, variables, }); - if (!marketDataRef.current && marketData) { - marketDataRef.current = marketData; - } - - useEffect(() => { - const throttleRunner = updateOrderbookData.current; - if (!data) { - dataRef.current = { rows: null }; - setOrderbookData(dataRef.current); - return; - } - dataRef.current = { - ...marketDataRef.current, - indicativePrice: - marketDataRef.current?.indicativePrice && - getPriceLevel(marketDataRef.current.indicativePrice, resolution), - midPrice: getMidPrice(data.depth.sell, data.depth.buy, resolution), - rows: compactRows(data.depth.sell, data.depth.buy, resolution), - }; - rawDataRef.current = data; - setOrderbookData(dataRef.current); - - return () => { - throttleRunner.cancel(); - }; - }, [data, resolution]); - - useEffect(() => { - resolutionRef.current = resolution; - flush(); - }, [resolution, flush]); - const updateOrder = useOrderStore((store) => store.update); return ( @@ -188,16 +61,16 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => { reload={reload} > setResolution(resolution)} onClick={(price: string) => { if (price) { updateOrder(marketId, { price }); } }} + midPrice={marketData?.midPrice} /> ); diff --git a/libs/market-depth/src/lib/orderbook-row.tsx b/libs/market-depth/src/lib/orderbook-row.tsx index 701ed7e17..227ae0215 100644 --- a/libs/market-depth/src/lib/orderbook-row.tsx +++ b/libs/market-depth/src/lib/orderbook-row.tsx @@ -1,130 +1,118 @@ -import React from 'react'; +import React, { memo } from 'react'; import { addDecimal, addDecimalsFixedFormatNumber } from '@vegaprotocol/utils'; -import { PriceCell, VolCell, CumulativeVol } from '@vegaprotocol/datagrid'; +import { NumericCell, PriceCell } from '@vegaprotocol/datagrid'; +import { VolumeType } from './orderbook-data'; +import classNames from 'classnames'; interface OrderbookRowProps { - ask: number; - bid: number; - cumulativeAsk?: number; - cumulativeBid?: number; - cumulativeRelativeAsk?: number; - cumulativeRelativeBid?: number; + value: number; + cumulativeValue?: number; + cumulativeRelativeValue?: number; decimalPlaces: number; positionDecimalPlaces: number; - indicativeVolume?: string; price: string; - relativeAsk?: number; - relativeBid?: number; onClick?: (price: string) => void; + type: VolumeType; } +const CumulationBar = ({ + cumulativeValue = 0, + type, +}: { + cumulativeValue?: number; + type: VolumeType; +}) => { + return ( +
+ ); +}; + +const CumulativeVol = memo( + ({ + testId, + positionDecimalPlaces, + cumulativeValue, + }: { + ask?: number; + bid?: number; + cumulativeValue?: number; + testId?: string; + className?: string; + positionDecimalPlaces: number; + }) => { + const volume = cumulativeValue ? ( + + ) : null; + + return ( +
+ {volume} +
+ ); + } +); +CumulativeVol.displayName = 'OrderBookCumulativeVol'; + export const OrderbookRow = React.memo( ({ - ask, - bid, - cumulativeAsk, - cumulativeBid, - cumulativeRelativeAsk, - cumulativeRelativeBid, + value, + cumulativeValue, + cumulativeRelativeValue, decimalPlaces, positionDecimalPlaces, - indicativeVolume, price, - relativeAsk, - relativeBid, onClick, + type, }: OrderbookRowProps) => { + const txtId = type === VolumeType.bid ? 'bid' : 'ask'; return ( - <> - - - onClick && onClick(addDecimal(price, decimalPlaces))} - valueFormatted={addDecimalsFixedFormatNumber(price, decimalPlaces)} - /> - - +
+ +
+ onClick && onClick(addDecimal(price, decimalPlaces))} + valueFormatted={addDecimalsFixedFormatNumber(price, decimalPlaces)} + className={ + type === VolumeType.ask + ? '!text-vega-pink dark:text-vega-pink' + : 'text-vega-green-550 dark:text-vega-green' + } + /> + + +
+
); } ); OrderbookRow.displayName = 'OrderbookRow'; - -export const OrderbookContinuousRow = React.memo( - ({ - ask, - bid, - cumulativeAsk, - cumulativeBid, - cumulativeRelativeAsk, - cumulativeRelativeBid, - decimalPlaces, - positionDecimalPlaces, - indicativeVolume, - price, - relativeAsk, - relativeBid, - onClick, - }: OrderbookRowProps) => { - const type = bid ? 'bid' : 'ask'; - const value = bid || ask; - const relativeValue = bid ? relativeBid : relativeAsk; - return ( - <> - - onClick && onClick(addDecimal(price, decimalPlaces))} - valueFormatted={addDecimalsFixedFormatNumber(price, decimalPlaces)} - /> - - - ); - } -); -OrderbookContinuousRow.displayName = 'OrderbookContinuousRow'; diff --git a/libs/market-depth/src/lib/orderbook.spec.tsx b/libs/market-depth/src/lib/orderbook.spec.tsx index d505a6c3b..5bb961ab9 100644 --- a/libs/market-depth/src/lib/orderbook.spec.tsx +++ b/libs/market-depth/src/lib/orderbook.spec.tsx @@ -1,260 +1,89 @@ import { render, fireEvent, waitFor, screen } from '@testing-library/react'; -import { generateMockData } from './orderbook-data'; -import { Orderbook, rowHeight } from './orderbook'; +import { generateMockData, VolumeType } from './orderbook-data'; +import { Orderbook } from './orderbook'; +import * as orderbookData from './orderbook-data'; + +function mockOffsetSize(width: number, height: number) { + Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', { + configurable: true, + value: () => ({ height, width }), + }); + Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { + configurable: true, + value: height, + }); + Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { + configurable: true, + value: width, + }); +} describe('Orderbook', () => { const params = { numberOfSellRows: 100, numberOfBuyRows: 100, step: 1, - midPrice: 122900, + midPrice: '122900', bestStaticBidPrice: 122905, bestStaticOfferPrice: 122895, decimalPlaces: 3, - overlap: 10, - indicativePrice: 122900, - indicativeVolume: 11, + overlap: 0, resolution: 1, }; - const onResolutionChange = jest.fn(); const decimalPlaces = 3; - it('should scroll to mid price on init', async () => { - window.innerHeight = 11 * rowHeight; + + beforeEach(() => { + mockOffsetSize(800, 768); + }); + it('markPrice should be in the middle', async () => { render( ); - await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`)); - expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight); - }); - - it('should keep mid price row in the middle', async () => { - window.innerHeight = 11 * rowHeight; - const result = render( - + await waitFor(() => + screen.getByTestId(`middle-mark-price-${params.midPrice}`) ); - await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`)); - expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight); - result.rerender( - - ); - await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`)); - expect(result.getByTestId('scroll').scrollTop).toBe(90 * rowHeight); - }); - - it('should scroll to mid price when it will change', async () => { - window.innerHeight = 11 * rowHeight; - const result = render( - - ); - await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`)); - expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight); - result.rerender( - - ); - await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`)); - expect(result.getByTestId('scroll').scrollTop).toBe(90 * rowHeight); - }); - - it('should keep price it the middle', async () => { - window.innerHeight = 11 * rowHeight; - const result = render( - - ); - await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`)); - const scrollElement = result.getByTestId('scroll'); - expect(scrollElement.scrollTop).toBe(91 * rowHeight); - scrollElement.scrollTop = 92 * rowHeight + 0.01; - fireEvent.scroll(scrollElement); - result.rerender( - - ); - await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`)); - expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 0.01); - }); - - it('should get back to mid price on click', async () => { - window.innerHeight = 11 * rowHeight; - const result = render( - - ); - await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`)); - const scrollElement = result.getByTestId('scroll'); - expect(scrollElement.scrollTop).toBe(91 * rowHeight); - scrollElement.scrollTop = 1; - fireEvent.scroll(scrollElement); - expect(result.getByTestId('scroll').scrollTop).toBe(1); - const scrollToMidPriceButton = result.getByTestId('scroll-to-midprice'); - fireEvent.click(scrollToMidPriceButton); - expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 1); - }); - - it('should get back to mid price on resolution change', async () => { - window.innerHeight = 11 * rowHeight; - const result = render( - - ); - await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`)); - const scrollElement = screen.getByTestId('scroll'); - expect(scrollElement.scrollTop).toBe(91 * rowHeight); - scrollElement.scrollTop = 1; - fireEvent.scroll(scrollElement); - expect(screen.getByTestId('scroll').scrollTop).toBe(1); - const resolutionSelect = screen.getByTestId( - 'resolution' - ) as HTMLSelectElement; - fireEvent.change(resolutionSelect, { target: { value: '10' } }); - expect(onResolutionChange.mock.calls.length).toBe(1); - expect(onResolutionChange.mock.calls[0][0]).toBe(10); - result.rerender( - - ); - expect(screen.getByTestId('scroll').scrollTop).toBe(6 * rowHeight); + expect( + screen.getByTestId(`middle-mark-price-${params.midPrice}`) + ).toHaveTextContent('122.90'); }); it('should format correctly the numbers on resolution change', async () => { const onClickSpy = jest.fn(); - const result = render( + jest.spyOn(orderbookData, 'compactRows'); + const mockedData = generateMockData(params); + render( ); expect( - await screen.findByTestId(`bid-vol-${params.midPrice}`) + await screen.findByTestId(`middle-mark-price-${params.midPrice}`) ).toBeInTheDocument(); // Before resolution change the price is 122.934 - await fireEvent.click(await screen.getByTestId('price-122934')); - expect(onClickSpy).toBeCalledWith('122.934'); + await fireEvent.click(await screen.getByTestId('price-122901')); + expect(onClickSpy).toBeCalledWith('122.901'); const resolutionSelect = screen.getByTestId( 'resolution' ) as HTMLSelectElement; await fireEvent.change(resolutionSelect, { target: { value: '10' } }); - await result.rerender( - + expect(orderbookData.compactRows).toHaveBeenCalledWith( + mockedData.bids, + VolumeType.bid, + 10 ); - await fireEvent.click(await screen.getByTestId('price-12299')); - // After resolution change the price is 122.99 - expect(onResolutionChange.mock.calls[0][0]).toBe(10); - expect(onClickSpy).toBeCalledWith('122.99'); - }); - - it('should have three or four columns', async () => { - window.innerHeight = 11 * rowHeight; - const { rerender } = render( - + expect(orderbookData.compactRows).toHaveBeenCalledWith( + mockedData.asks, + VolumeType.ask, + 10 ); - await waitFor(() => { - expect(screen.queryByText('Bid / Ask vol')).toBeInTheDocument(); - }); - rerender( - - ); - await waitFor(() => { - expect(screen.getByText('Bid vol')).toBeInTheDocument(); - expect(screen.getByText('Ask vol')).toBeInTheDocument(); - }); - await expect(screen.queryByText('Bid / Ask vol')).not.toBeInTheDocument(); + await fireEvent.click(await screen.getByTestId('price-12294')); + expect(onClickSpy).toBeCalledWith('122.94'); }); }); diff --git a/libs/market-depth/src/lib/orderbook.stories.tsx b/libs/market-depth/src/lib/orderbook.stories.tsx index c3340ddb8..f710a8336 100644 --- a/libs/market-depth/src/lib/orderbook.stories.tsx +++ b/libs/market-depth/src/lib/orderbook.stories.tsx @@ -2,14 +2,12 @@ import type { Story, Meta } from '@storybook/react'; import { generateMockData } from './orderbook-data'; import type { MockDataGeneratorParams } from './orderbook-data'; import { Orderbook } from './orderbook'; -import { useState } from 'react'; type Props = Omit & { decimalPlaces: number; }; const OrderbookMockDataProvider = ({ decimalPlaces, ...props }: Props) => { - const [resolution, setResolution] = useState(1); return (
{ >
@@ -54,8 +51,6 @@ Auction.args = { bestStaticOfferPrice: 122895, decimalPlaces: 3, overlap: 10, - indicativePrice: 122900, - indicativeVolume: 11, }; export const Empty = Template.bind({}); @@ -66,6 +61,4 @@ Empty.args = { bestStaticOfferPrice: 0, decimalPlaces: 3, overlap: 0, - indicativePrice: 0, - indicativeVolume: 0, }; diff --git a/libs/market-depth/src/lib/orderbook.tsx b/libs/market-depth/src/lib/orderbook.tsx index 46eef4ee4..a03b250b9 100644 --- a/libs/market-depth/src/lib/orderbook.tsx +++ b/libs/market-depth/src/lib/orderbook.tsx @@ -1,680 +1,178 @@ -import colors from 'tailwindcss/colors'; +import { useMemo } from 'react'; +import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer'; import { - useEffect, - useRef, - useState, - useCallback, - Fragment, - useMemo, -} from 'react'; -import classNames from 'classnames'; -import { - addDecimalsFixedFormatNumber, + addDecimalsFormatNumber, formatNumberFixed, } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; -import { - useResizeObserver, - useThemeSwitcher, -} from '@vegaprotocol/react-helpers'; -import * as Schema from '@vegaprotocol/types'; -import { OrderbookRow, OrderbookContinuousRow } from './orderbook-row'; -import { createRow } from './orderbook-data'; -import { Checkbox, Icon, Splash, TinyScroll } from '@vegaprotocol/ui-toolkit'; -import type { OrderbookData, OrderbookRowData } from './orderbook-data'; +import { OrderbookRow } from './orderbook-row'; +import type { OrderbookRowData } from './orderbook-data'; +import { compactRows, VolumeType } from './orderbook-data'; +import { Splash } from '@vegaprotocol/ui-toolkit'; +import classNames from 'classnames'; +import { useState } from 'react'; +import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth'; -interface OrderbookProps extends OrderbookData { +interface OrderbookProps { decimalPlaces: number; positionDecimalPlaces: number; - resolution: number; - onResolutionChange: (resolution: number) => void; onClick?: (price: string) => void; - fillGaps?: boolean; + midPrice?: string; + bids: PriceLevelFieldsFragment[]; + asks: PriceLevelFieldsFragment[]; } -const HorizontalLine = ({ top, testId }: { top: string; testId: string }) => ( -
-); +// Sets row height, will be used to calculate number of rows that can be +// displayed each side of the book without overflow +export const rowHeight = 17; +const midHeight = 30; -const getNumberOfRows = ( - rows: OrderbookRowData[] | null, - resolution: number -) => { - if (!rows || !rows.length) { - return 0; - } - if (rows.length === 1) { - return 1; - } - return ( - Number(BigInt(rows[0].price) - BigInt(rows[rows.length - 1].price)) / - resolution + - 1 - ); -}; - -const getRowsToRender = ( - rows: OrderbookRowData[] | null, - resolution: number, - offset: number, - limit: number -): OrderbookRowData[] | null => { - if (!rows || !rows.length) { - return rows; - } - if (rows.length === 1) { - return rows; - } - const selectedRows: OrderbookRowData[] = []; - let price = BigInt(rows[0].price) - BigInt(offset * resolution); - let index = Math.max( - rows.findIndex((row) => BigInt(row.price) <= price) - 1, - -1 - ); - while (selectedRows.length < limit && index + 1 < rows.length) { - if (rows[index + 1].price === price.toString()) { - selectedRows.push(rows[index + 1]); - index += 1; - } else { - const row = createRow(price.toString()); - row.cumulativeVol = { - bid: rows[index].cumulativeVol.bid, - relativeBid: rows[index].cumulativeVol.relativeBid, - ask: rows[index + 1].cumulativeVol.ask, - relativeAsk: rows[index + 1].cumulativeVol.relativeAsk, - }; - selectedRows.push(row); - } - price -= BigInt(resolution); - } - return selectedRows; -}; - -// 17px of row height plus 4px gap -export const gridGap = 4; -export const rowHeight = 21; -// top padding to make space for header -const headerPadding = 30; -// bottom padding to make space for footer -const footerPadding = 25; -// buffer size in rows -const bufferSize = 30; -// margin size in px, when reached scrollOffset will be updated -const marginSize = bufferSize * 0.9 * rowHeight; - -const getBestStaticBidPriceLinePosition = ( - bestStaticBidPrice: string | undefined, - fillGaps: boolean, - maxPriceLevel: string, - minPriceLevel: string, - resolution: number, - rows: OrderbookRowData[] | null -) => { - let bestStaticBidPriceLinePosition = ''; - if ( - rows?.length && - bestStaticBidPrice && - BigInt(bestStaticBidPrice) < BigInt(maxPriceLevel) && - BigInt(bestStaticBidPrice) > BigInt(minPriceLevel) - ) { - if (fillGaps) { - bestStaticBidPriceLinePosition = ( - ((BigInt(maxPriceLevel) - BigInt(bestStaticBidPrice)) / - BigInt(resolution)) * - BigInt(rowHeight) + - BigInt(headerPadding) - - BigInt(3) - ).toString(); - } else { - const index = rows?.findIndex( - (row) => BigInt(row.price) <= BigInt(bestStaticBidPrice) - ); - if (index !== undefined && index !== -1) { - bestStaticBidPriceLinePosition = ( - index * rowHeight + - headerPadding - - 3 - ).toString(); - } - } - } - return bestStaticBidPriceLinePosition; -}; -const getBestStaticOfferPriceLinePosition = ( - bestStaticOfferPrice: string | undefined, - fillGaps: boolean, - maxPriceLevel: string, - minPriceLevel: string, - resolution: number, - rows: OrderbookRowData[] | null -) => { - let bestStaticOfferPriceLinePosition = ''; - if ( - rows?.length && - bestStaticOfferPrice && - BigInt(bestStaticOfferPrice) <= BigInt(maxPriceLevel) && - BigInt(bestStaticOfferPrice) > BigInt(minPriceLevel) - ) { - if (fillGaps) { - bestStaticOfferPriceLinePosition = ( - ((BigInt(maxPriceLevel) - BigInt(bestStaticOfferPrice)) / - BigInt(resolution) + - BigInt(1)) * - BigInt(rowHeight) + - BigInt(headerPadding) - - BigInt(3) - ).toString(); - } else { - const index = rows?.findIndex( - (row) => BigInt(row.price) <= BigInt(bestStaticOfferPrice) - ); - if (index !== undefined && index !== -1) { - bestStaticOfferPriceLinePosition = ( - (index + 1) * rowHeight + - headerPadding - - 3 - ).toString(); - } - } - } - return bestStaticOfferPriceLinePosition; -}; -const OrderbookDebugInfo = ({ - decimalPlaces, - numberOfRows, - viewportHeight, - lockOnMidPrice, - priceInCenter, - bestStaticBidPrice, - bestStaticOfferPrice, - maxPriceLevel, - minPriceLevel, - midPrice, -}: { - decimalPlaces: number; - numberOfRows: number; - viewportHeight: number; - lockOnMidPrice: boolean; - priceInCenter?: string; - bestStaticBidPrice?: string; - bestStaticOfferPrice?: string; - maxPriceLevel: string; - minPriceLevel: string; - midPrice?: string; -}) => ( - -
-
-
-        {JSON.stringify(
-          {
-            numberOfRows,
-            viewportHeight,
-            lockOnMidPrice,
-            priceInCenter: priceInCenter
-              ? addDecimalsFixedFormatNumber(priceInCenter, decimalPlaces)
-              : '-',
-            maxPriceLevel: addDecimalsFixedFormatNumber(
-              maxPriceLevel ?? '0',
-              decimalPlaces
-            ),
-            bestStaticBidPrice: addDecimalsFixedFormatNumber(
-              bestStaticBidPrice ?? '0',
-              decimalPlaces
-            ),
-            bestStaticOfferPrice: addDecimalsFixedFormatNumber(
-              bestStaticOfferPrice ?? '0',
-              decimalPlaces
-            ),
-            minPriceLevel: addDecimalsFixedFormatNumber(
-              minPriceLevel ?? '0',
-              decimalPlaces
-            ),
-            midPrice: addDecimalsFixedFormatNumber(
-              midPrice ?? '0',
-              decimalPlaces
-            ),
-          },
-          null,
-          2
-        )}
-      
-
- -); - -export const Orderbook = ({ +const OrderbookTable = ({ rows, - midPrice, - bestStaticBidPrice, - bestStaticOfferPrice, - marketTradingMode, - indicativeVolume, - indicativePrice, + resolution, + type, decimalPlaces, positionDecimalPlaces, - resolution, - fillGaps: initialFillGaps, - onResolutionChange, onClick, -}: OrderbookProps) => { - const { theme } = useThemeSwitcher(); - const scrollElement = useRef(null); - const rootElement = useRef(null); - const gridElement = useRef(null); - const headerElement = useRef(null); - const footerElement = useRef(null); - // scroll offset for which rendered rows are selected, will change after user will scroll to margin of rendered data - const [scrollOffset, setScrollOffset] = useState(0); - // actual scrollTop of scrollElement current element - const scrollTopRef = useRef(0); - // price level which is rendered in center of viewport, need to preserve price level when rows will be added or removed - // if undefined then we render mid price in center - const priceInCenter = useRef(); - // by default mid price is rendered in center - view locked on mid price - const [lockOnMidPrice, setLockOnMidPrice] = useState(true); - const resolutionRef = useRef(resolution); - const [viewportHeight, setViewportHeight] = useState(window.innerHeight); - // show price levels with no orders, can lead to enormous number of rows - const [fillGaps, setFillGaps] = useState(!!initialFillGaps); - const [debug, setDebug] = useState(false); - - const numberOfRows = fillGaps - ? getNumberOfRows(rows, resolution) - : rows?.length ?? 0; - const maxPriceLevel = rows?.[0]?.price ?? '0'; - const minPriceLevel = rows?.[rows.length - 1]?.price ?? '0'; - - let offset = Math.max(0, Math.round(scrollOffset / rowHeight)); - const prependingBufferSize = Math.min(bufferSize, offset); - offset -= prependingBufferSize; - const viewportSize = Math.round(viewportHeight / rowHeight); - const limit = Math.min( - prependingBufferSize + viewportSize + bufferSize, - numberOfRows - offset - ); - const data = fillGaps - ? getRowsToRender(rows, resolution, offset, limit) - : rows?.slice(offset, offset + limit) ?? []; - - const paddingTop = offset * rowHeight + headerPadding; - const paddingBottom = - (numberOfRows - offset - limit) * rowHeight + footerPadding; - - const updateScrollOffset = useCallback( - (scrollTop: number) => { - if (Math.abs(scrollOffset - scrollTop) > marginSize) { - setScrollOffset(scrollTop); - } - }, - [scrollOffset] - ); - - const onScroll = useCallback( - (event: React.UIEvent) => { - const { scrollTop, scrollHeight, clientHeight } = event.currentTarget; - updateScrollOffset(scrollTop); - if (scrollTop === scrollTopRef.current) { - return; - } else if ((scrollTop - scrollTopRef.current) % rowHeight === 0) { - if (scrollElement.current) { - scrollElement.current.scrollTop = scrollTopRef.current; - } - return; - } - if (scrollTop === 0 || scrollHeight === clientHeight + scrollTop) { - priceInCenter.current = undefined; - } else { - // top offset in rows to row in the middle - const offsetTop = Math.floor( - (scrollTop + - Math.floor((viewportHeight - footerPadding - headerPadding) / 2)) / - rowHeight - ); - priceInCenter.current = fillGaps - ? ( - BigInt(maxPriceLevel) - - BigInt(offsetTop) * BigInt(resolution) - ).toString() - : rows?.[Math.min(offsetTop, rows.length - 1)].price.toString(); - } - if (lockOnMidPrice) { - setLockOnMidPrice(false); - } - scrollTopRef.current = scrollTop; - }, - [ - resolution, - lockOnMidPrice, - maxPriceLevel, - viewportHeight, - updateScrollOffset, - fillGaps, - rows, - ] - ); - - const scrollToPrice = useCallback( - (price: string) => { - if (scrollElement.current && maxPriceLevel !== '0') { - let scrollTop = 0; - if (fillGaps) { - scrollTop = - // distance in rows between given price and first row price * row Height - (Number( - (BigInt(maxPriceLevel) - BigInt(price)) / BigInt(resolution) - ) + - 1) * - rowHeight; - } else if (rows) { - const index = rows.findIndex( - (row) => BigInt(row.price) <= BigInt(price) - ); - if (index !== -1) { - scrollTop = rowHeight * (index + 1); - if (index !== 0) { - const diffToCurrentRow = - BigInt(price) - BigInt(rows[index].price); - const diffToPreviousRow = - BigInt(rows[index - 1].price) - BigInt(price); - if (diffToPreviousRow < diffToCurrentRow) { - scrollTop -= rowHeight; - } - } - } - } - // minus half height of viewport plus half of row - scrollTop -= Math.ceil((viewportHeight - rowHeight) / 2); - // adjust to current rows position - scrollTop += - (scrollTopRef.current % rowHeight) - (scrollTop % rowHeight); - const priceCenterScrollOffset = Math.max( - 0, - Math.min( - scrollTop, - numberOfRows * rowHeight + - headerPadding + - footerPadding + - -viewportHeight - - gridGap - ) - ); - if (scrollTopRef.current !== priceCenterScrollOffset) { - updateScrollOffset(priceCenterScrollOffset); - scrollTopRef.current = priceCenterScrollOffset; - scrollElement.current.scrollTop = priceCenterScrollOffset; - } - } - }, - [ - maxPriceLevel, - resolution, - viewportHeight, - numberOfRows, - updateScrollOffset, - fillGaps, - rows, - ] - ); - - const scrollToMidPrice = useCallback(() => { - if (!midPrice) { - return; - } - priceInCenter.current = undefined; - scrollToPrice(midPrice); - setLockOnMidPrice(true); - }, [midPrice, scrollToPrice]); - - // adjust scroll position to keep selected price in center - useEffect(() => { - if (priceInCenter.current) { - scrollToPrice(priceInCenter.current); - } else if (lockOnMidPrice && midPrice) { - scrollToPrice(midPrice); - } - }, [midPrice, scrollToPrice, lockOnMidPrice]); - - useEffect(() => { - if (resolutionRef.current !== resolution) { - priceInCenter.current = undefined; - resolutionRef.current = resolution; - setLockOnMidPrice(true); - } - }, [resolution]); - - // handles resizing of the Allotment.Pane (x-axis) - // adjusts the header and footer width - const gridResizeHandler: ResizeObserverCallback = useCallback( - (entries) => { - if ( - !headerElement.current || - !footerElement.current || - entries.length === 0 - ) { - return; - } - const { - contentRect: { width }, - } = entries[0]; - headerElement.current.style.width = `${width}px`; - footerElement.current.style.width = `${width}px`; - }, - [headerElement, footerElement] - ); - // handles resizing of the Allotment.Pane (y-axis) - // adjusts the scroll height - const rootElementResizeHandler: ResizeObserverCallback = useCallback( - (entries) => { - if (!rootElement.current || entries.length === 0) { - return; - } - setViewportHeight(entries[0].contentRect.height); - }, - [setViewportHeight, rootElement] - ); - useResizeObserver(gridElement.current, gridResizeHandler); - useResizeObserver(rootElement.current, rootElementResizeHandler); - const isContinuousMode = - marketTradingMode === Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS; - const tableHeader = useMemo(() => { - return ( -
- {isContinuousMode ? ( -
{t('Bid / Ask vol')}
- ) : ( - <> -
{t('Bid vol')}
-
{t('Ask vol')}
- - )} -
{t('Price')}
-
- {t('Cumulative vol')} -
-
- ); - }, [isContinuousMode]); - - const OrderBookRowComponent = isContinuousMode - ? OrderbookContinuousRow - : OrderbookRow; - - const tableBody = data?.length ? ( +}: { + rows: OrderbookRowData[]; + resolution: number; + decimalPlaces: number; + positionDecimalPlaces: number; + type: VolumeType; + onClick?: (price: string) => void; +}) => { + return (
- {data.map((data, i) => ( - - ))} +
+ {rows.map((data) => ( + + ))} +
- ) : null; + ); +}; - const c = theme === 'dark' ? colors.neutral[600] : colors.neutral[300]; - const gradientStyles = isContinuousMode - ? `linear-gradient(${c},${c}) 33.4% 0/1px 100% no-repeat, linear-gradient(${c},${c}) 66.7% 0/1px 100% no-repeat` - : `linear-gradient(${c},${c}) 25% 0/1px 100% no-repeat, linear-gradient(${c},${c}) 50% 0/1px 100% no-repeat, linear-gradient(${c},${c}) 75% 0/1px 100% no-repeat`; - - const resolutions = new Array(decimalPlaces + 1) +export const Orderbook = ({ + decimalPlaces, + positionDecimalPlaces, + onClick, + midPrice, + asks, + bids, +}: 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 bestStaticBidPriceLinePosition = getBestStaticBidPriceLinePosition( - bestStaticBidPrice, - fillGaps, - maxPriceLevel, - minPriceLevel, - resolution, - rows - ); + const groupedAsks = useMemo(() => { + return compactRows(asks, VolumeType.ask, resolution); + }, [asks, resolution]); - const bestStaticOfferPriceLinePosition = getBestStaticOfferPriceLinePosition( - bestStaticOfferPrice, - fillGaps, - maxPriceLevel, - minPriceLevel, - resolution, - rows - ); + const groupedBids = useMemo(() => { + return compactRows(bids, VolumeType.bid, resolution); + }, [bids, resolution]); - /* eslint-disable jsx-a11y/no-static-element-interactions */ return ( -
setDebug(!debug)} - > - {tableHeader} - -
+
+ + {({ height }) => { + const limit = Math.max( + 1, + Math.floor((height - midHeight) / 2 / rowHeight) + ); + const askRows = groupedAsks?.slice(limit * -1) ?? []; + const bidRows = groupedBids?.slice(0, limit) ?? []; + return ( +
+ {askRows.length || bidRows.length ? ( + <> + +
+ {midPrice && ( + + {addDecimalsFormatNumber(midPrice, decimalPlaces)} + + )} +
+ + + ) : ( +
+ {t('No data')} +
+ )} +
+ ); }} - ref={gridElement} +
+
+
+ onResolutionChange(Number(e.currentTarget.value))} - value={resolution} - className="block bg-neutral-100 dark:bg-neutral-700 font-mono text-right w-full h-full" - data-testid="resolution" - > - {resolutions.map((r) => ( - - ))} - -
-
- -
+ {resolutions.map((r) => ( + + ))} +
- {debug && ( - - )}
); - /* eslint-enable jsx-a11y/no-static-element-interactions */ }; export default Orderbook; From 2ba0e9a1b2369aa7fd0ded2a82336905abefee6d Mon Sep 17 00:00:00 2001 From: "m.ray" <16125548+MadalinaRaicu@users.noreply.github.com> Date: Wed, 7 Jun 2023 13:49:50 +0300 Subject: [PATCH 22/49] chore(trading): add quantum formatting to deal ticket (#4030) --- .../trading-deal-ticket-order.cy.ts | 3 +- .../deal-ticket/deal-ticket-fee-details.tsx | 43 ++++++---- .../use-fee-deal-ticket-details.spec.tsx | 68 ++++++++++++++++ .../src/hooks/use-fee-deal-ticket-details.tsx | 79 ++++++++++++++++--- libs/deal-ticket/src/test-helpers.ts | 1 + libs/fills/src/lib/test-helpers.ts | 1 + libs/markets/src/lib/__generated__/markets.ts | 5 +- libs/markets/src/lib/markets.graphql | 1 + libs/markets/src/lib/markets.mock.ts | 1 + .../lib/components/mocks/generate-orders.ts | 1 + libs/utils/src/lib/format/number.spec.ts | 23 ++++++ libs/utils/src/lib/format/number.ts | 12 +++ .../src/lib/withdraw-form-container.spec.tsx | 1 + 13 files changed, 206 insertions(+), 33 deletions(-) create mode 100644 libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.spec.tsx diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts index 00d4f1c9d..5ac9b5133 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts @@ -100,9 +100,8 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => { .within(() => { cy.get('[data-state="closed"]').should( 'have.text', - 'Total margin available' + 'Total margin available100,000.01 tDAI' ); - cy.get('.text-neutral-500').should('have.text', '100,000.01 tDAI'); }); }); }); diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx index 52969be14..4309d0069 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx @@ -33,24 +33,35 @@ export const DealTicketFeeDetails = (props: FeeDetails) => { const details = getFeeDetailsValues(props); return (
- {details.map(({ label, value, labelDescription, symbol, indent }) => ( -
-
- -
{label}
+ {details.map( + ({ + label, + value, + labelDescription, + symbol, + indent, + formattedValue, + }) => ( +
+
+ +
{label}
+
+
+ +
{`${ + formattedValue ?? '-' + } ${symbol || ''}`}
-
{`${ - value ?? '-' - } ${symbol || ''}`}
-
- ))} + ) + )}
); }; diff --git a/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.spec.tsx b/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.spec.tsx new file mode 100644 index 000000000..f5a899fbf --- /dev/null +++ b/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.spec.tsx @@ -0,0 +1,68 @@ +import { formatRange, formatValue } from './use-fee-deal-ticket-details'; + +describe('useFeeDealTicketDetails', () => { + it.each([ + { v: 123000, d: 5, o: '1.23' }, + { v: 123000, d: 3, o: '123.00' }, + { v: 123000, d: 1, o: '12,300.0' }, + { v: 123001000, d: 2, o: '1,230,010.00' }, + { v: 123001, d: 2, o: '1,230.01' }, + { + v: '123456789123456789', + d: 10, + o: '12,345,678.91234568', + }, + ])('formats values correctly', ({ v, d, o }) => { + expect(formatValue(v, d)).toStrictEqual(o); + }); + + it.each([ + { v: 123000, d: 5, o: '1.23', q: '0.1' }, + { v: 123000, d: 3, o: '123.00', q: '0.1' }, + { v: 123000, d: 1, o: '12,300.00', q: '0.1' }, + { v: 123001000, d: 2, o: '1,230,010.00', q: '0.1' }, + { v: 123001, d: 2, o: '1,230', q: '100' }, + { v: 123001, d: 2, o: '1,230.01', q: '0.1' }, + { + v: '123456789123456789', + d: 10, + o: '12,345,678.9123457', + q: '0.00003846', + }, + ])( + 'formats with formatValue with quantum given number correctly', + ({ v, d, o, q }) => { + expect(formatValue(v.toString(), d, q)).toStrictEqual(o); + } + ); + + it.each([ + { min: 123000, max: 12300011111, d: 5, o: '1.23 - 123,000.111', q: '0.1' }, + { + min: 123000, + max: 12300011111, + d: 3, + o: '123.00 - 12,300,011.111', + q: '0.1', + }, + { + min: 123000, + max: 12300011111, + d: 1, + o: '12,300.00 - 1,230,001,111.10', + q: '0.1', + }, + { + min: 123001000, + max: 12300011111, + d: 2, + o: '1,230,010 - 123,000,111', + q: '100', + }, + ])( + 'formats with formatValue with quantum given number correctly', + ({ min, max, d, o, q }) => { + expect(formatRange(min, max, d, q)).toStrictEqual(o); + } + ); +}); diff --git a/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.tsx b/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.tsx index df295e562..735c2b54f 100644 --- a/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.tsx +++ b/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.tsx @@ -1,5 +1,9 @@ import { FeesBreakdown } from '@vegaprotocol/markets'; -import { addDecimalsFormatNumber, isNumeric } from '@vegaprotocol/utils'; +import { + addDecimalsFormatNumber, + addDecimalsFormatNumberQuantum, + isNumeric, +} from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; import { useVegaWallet } from '@vegaprotocol/wallet'; import type { Market } from '@vegaprotocol/markets'; @@ -52,21 +56,25 @@ export interface FeeDetails { } const emptyValue = '-'; -const formatValue = ( + +export const formatValue = ( value: string | number | null | undefined, - formatDecimals: number + formatDecimals: number, + quantum?: string ): string => { - return isNumeric(value) - ? addDecimalsFormatNumber(value, formatDecimals) - : emptyValue; + if (!isNumeric(value)) return emptyValue; + if (!quantum) return addDecimalsFormatNumber(value, formatDecimals); + return addDecimalsFormatNumberQuantum(value, formatDecimals, quantum); }; -const formatRange = ( + +export const formatRange = ( min: string | number | null | undefined, max: string | number | null | undefined, - formatDecimals: number + formatDecimals: number, + quantum?: string ) => { - const minFormatted = formatValue(min, formatDecimals); - const maxFormatted = formatValue(max, formatDecimals); + const minFormatted = formatValue(min, formatDecimals, quantum); + const maxFormatted = formatValue(max, formatDecimals, quantum); if (minFormatted !== maxFormatted) { return `${minFormatted} - ${maxFormatted}`; } @@ -93,9 +101,12 @@ export const getFeeDetailsValues = ({ BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0'); const assetDecimals = market.tradableInstrument.instrument.product.settlementAsset.decimals; + const quantum = + market.tradableInstrument.instrument.product.settlementAsset.quantum; const details: { label: string; value?: string | null; + formattedValue?: string | null; symbol: string; indent?: boolean; labelDescription?: React.ReactNode; @@ -103,6 +114,7 @@ export const getFeeDetailsValues = ({ { label: t('Notional'), value: formatValue(notionalSize, assetDecimals), + formattedValue: formatValue(notionalSize, assetDecimals, quantum), symbol: assetSymbol, labelDescription: NOTIONAL_SIZE_TOOLTIP_TEXT(assetSymbol), }, @@ -111,6 +123,9 @@ export const getFeeDetailsValues = ({ value: feeEstimate?.totalFeeAmount && `~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`, + formattedValue: + feeEstimate?.totalFeeAmount && + `~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`, labelDescription: ( <> @@ -154,6 +169,12 @@ export const getFeeDetailsValues = ({ } details.push({ label: t('Margin required'), + formattedValue: formatRange( + marginRequiredBestCase, + marginRequiredWorstCase, + assetDecimals, + quantum + ), value: formatRange( marginRequiredBestCase, marginRequiredWorstCase, @@ -172,12 +193,13 @@ export const getFeeDetailsValues = ({ details.push({ indent: true, label: t('Total margin available'), + formattedValue: formatValue(totalMarginAvailable, assetDecimals, quantum), value: formatValue(totalMarginAvailable, assetDecimals), symbol: assetSymbol, labelDescription: TOTAL_MARGIN_AVAILABLE( - formatValue(generalAccountBalance, assetDecimals), - formatValue(marginAccountBalance, assetDecimals), - formatValue(currentMaintenanceMargin, assetDecimals), + formatValue(generalAccountBalance, assetDecimals, quantum), + formatValue(marginAccountBalance, assetDecimals, quantum), + formatValue(currentMaintenanceMargin, assetDecimals, quantum), assetSymbol ), }); @@ -203,6 +225,16 @@ export const getFeeDetailsValues = ({ : '0', assetDecimals ), + formattedValue: formatRange( + deductionFromCollateralBestCase > 0 + ? deductionFromCollateralBestCase.toString() + : '0', + deductionFromCollateralWorstCase > 0 + ? deductionFromCollateralWorstCase.toString() + : '0', + assetDecimals, + quantum + ), symbol: assetSymbol, labelDescription: DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT(assetSymbol), }); @@ -214,6 +246,12 @@ export const getFeeDetailsValues = ({ marginEstimate?.worstCase.initialLevel, assetDecimals ), + formattedValue: formatRange( + marginEstimate?.bestCase.initialLevel, + marginEstimate?.worstCase.initialLevel, + assetDecimals, + quantum + ), symbol: assetSymbol, labelDescription: EST_TOTAL_MARGIN_TOOLTIP_TEXT, }); @@ -223,9 +261,11 @@ export const getFeeDetailsValues = ({ value: formatValue(marginAccountBalance, assetDecimals), symbol: assetSymbol, labelDescription: MARGIN_ACCOUNT_TOOLTIP_TEXT, + formattedValue: formatValue(marginAccountBalance, assetDecimals, quantum), }); let liquidationPriceEstimate = emptyValue; + let liquidationPriceEstimateFormatted; if (liquidationEstimate) { const liquidationEstimateBestCaseIncludingBuyOrders = BigInt( @@ -262,11 +302,24 @@ export const getFeeDetailsValues = ({ ).toString(), assetDecimals ); + liquidationPriceEstimateFormatted = formatRange( + (liquidationEstimateBestCase < liquidationEstimateWorstCase + ? liquidationEstimateBestCase + : liquidationEstimateWorstCase + ).toString(), + (liquidationEstimateBestCase > liquidationEstimateWorstCase + ? liquidationEstimateBestCase + : liquidationEstimateWorstCase + ).toString(), + assetDecimals, + quantum + ); } details.push({ label: t('Liquidation price estimate'), value: liquidationPriceEstimate, + formattedValue: liquidationPriceEstimateFormatted, symbol: assetSymbol, labelDescription: LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT, }); diff --git a/libs/deal-ticket/src/test-helpers.ts b/libs/deal-ticket/src/test-helpers.ts index a332ade69..1081752f8 100644 --- a/libs/deal-ticket/src/test-helpers.ts +++ b/libs/deal-ticket/src/test-helpers.ts @@ -32,6 +32,7 @@ export function generateMarket(override?: PartialDeep): Market { symbol: 'tDAI', name: 'tDAI', decimals: 5, + quantum: '1', __typename: 'Asset', }, dataSourceSpecForTradingTermination: { diff --git a/libs/fills/src/lib/test-helpers.ts b/libs/fills/src/lib/test-helpers.ts index 3d815b355..1654dabb5 100644 --- a/libs/fills/src/lib/test-helpers.ts +++ b/libs/fills/src/lib/test-helpers.ts @@ -75,6 +75,7 @@ export const generateFill = (override?: PartialDeep) => { name: 'assset-id', symbol: 'SYM', decimals: 18, + quantum: '1', }, quoteName: '', dataSourceSpecForTradingTermination: { diff --git a/libs/markets/src/lib/__generated__/markets.ts b/libs/markets/src/lib/__generated__/markets.ts index 2e3432339..addf669a7 100644 --- a/libs/markets/src/lib/__generated__/markets.ts +++ b/libs/markets/src/lib/__generated__/markets.ts @@ -7,12 +7,12 @@ export type DataSourceFilterFragment = { __typename?: 'Filter', key: { __typenam export type DataSourceSpecFragment = { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } }; -export type MarketFieldsFragment = { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } }; +export type MarketFieldsFragment = { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } }; export type MarketsQueryVariables = Types.Exact<{ [key: string]: never; }>; -export type MarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } } }> } | null }; +export type MarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } } }> } | null }; export const DataSourceFilterFragmentDoc = gql` fragment DataSourceFilter on Filter { @@ -77,6 +77,7 @@ export const MarketFieldsFragmentDoc = gql` symbol name decimals + quantum } quoteName dataSourceSpecForTradingTermination { diff --git a/libs/markets/src/lib/markets.graphql b/libs/markets/src/lib/markets.graphql index 74ae46100..84e25a6e2 100644 --- a/libs/markets/src/lib/markets.graphql +++ b/libs/markets/src/lib/markets.graphql @@ -58,6 +58,7 @@ fragment MarketFields on Market { symbol name decimals + quantum } quoteName dataSourceSpecForTradingTermination { diff --git a/libs/markets/src/lib/markets.mock.ts b/libs/markets/src/lib/markets.mock.ts index b634bd0d5..44ff183fe 100644 --- a/libs/markets/src/lib/markets.mock.ts +++ b/libs/markets/src/lib/markets.mock.ts @@ -60,6 +60,7 @@ export const createMarketFragment = ( symbol: 'tDAI', name: 'tDAI', decimals: 5, + quantum: '1', __typename: 'Asset', }, dataSourceSpecForTradingTermination: { diff --git a/libs/orders/src/lib/components/mocks/generate-orders.ts b/libs/orders/src/lib/components/mocks/generate-orders.ts index 1be8e2774..60ef14bf4 100644 --- a/libs/orders/src/lib/components/mocks/generate-orders.ts +++ b/libs/orders/src/lib/components/mocks/generate-orders.ts @@ -47,6 +47,7 @@ export const generateOrder = (partialOrder?: PartialDeep) => { decimals: 1, symbol: 'XYZ', name: 'XYZ', + quantum: '1', }, dataSourceSpecForTradingTermination: { __typename: 'DataSourceSpec', diff --git a/libs/utils/src/lib/format/number.spec.ts b/libs/utils/src/lib/format/number.spec.ts index 3c2024d38..621b1decd 100644 --- a/libs/utils/src/lib/format/number.spec.ts +++ b/libs/utils/src/lib/format/number.spec.ts @@ -2,6 +2,7 @@ import BigNumber from 'bignumber.js'; import { addDecimalsFormatNumber, + addDecimalsFormatNumberQuantum, formatNumber, formatNumberPercentage, isNumeric, @@ -23,6 +24,28 @@ describe('number utils', () => { } ); + it.each([ + { v: new BigNumber(123000), d: 5, o: '1.23', q: 0.1 }, + { v: new BigNumber(123000), d: 3, o: '123.00', q: 0.1 }, + { v: new BigNumber(123000), d: 1, o: '12,300.00', q: 0.1 }, + { v: new BigNumber(123001000), d: 2, o: '1,230,010.00', q: 0.1 }, + { v: new BigNumber(123001), d: 2, o: '1,230', q: 100 }, + { v: new BigNumber(123001), d: 2, o: '1,230.01', q: 0.1 }, + { + v: BigNumber('123456789123456789'), + d: 10, + o: '12,345,678.9123457', + q: '0.00003846', + }, + ])( + 'formats with addDecimalsFormatNumberQuantum given number correctly', + ({ v, d, o, q }) => { + expect(addDecimalsFormatNumberQuantum(v.toString(), d, q)).toStrictEqual( + o + ); + } + ); + it.each([ { v: new BigNumber(123), d: 3, o: '123.00' }, { v: new BigNumber(123.123), d: 3, o: '123.123' }, diff --git a/libs/utils/src/lib/format/number.ts b/libs/utils/src/lib/format/number.ts index 3cbf5a402..55e34cb1c 100644 --- a/libs/utils/src/lib/format/number.ts +++ b/libs/utils/src/lib/format/number.ts @@ -90,6 +90,18 @@ export const formatNumberFixed = ( return getFixedNumberFormat(formatDecimals).format(Number(rawValue)); }; +export const addDecimalsFormatNumberQuantum = ( + rawValue: string | number, + decimalPlaces: number, + quantum: number | string +) => { + if (isNaN(Number(quantum))) { + return addDecimalsFormatNumber(rawValue, decimalPlaces); + } + const numberDP = Math.max(0, Math.log10(100 / Number(quantum))); + return addDecimalsFormatNumber(rawValue, decimalPlaces, Math.ceil(numberDP)); +}; + export const addDecimalsFormatNumber = ( rawValue: string | number, decimalPlaces: number, diff --git a/libs/withdraws/src/lib/withdraw-form-container.spec.tsx b/libs/withdraws/src/lib/withdraw-form-container.spec.tsx index 69545b756..78ee7d7e4 100644 --- a/libs/withdraws/src/lib/withdraw-form-container.spec.tsx +++ b/libs/withdraws/src/lib/withdraw-form-container.spec.tsx @@ -165,6 +165,7 @@ describe('WithdrawFormContainer', () => { name: 'asset-id', symbol: 'tUSDC', decimals: 5, + quantum: '1', }, dataSourceSpecForTradingTermination: { __typename: 'DataSourceSpec', From 5eba8fe28fcfcd6e7ee8b3203501842f93835ee2 Mon Sep 17 00:00:00 2001 From: Edd Date: Wed, 7 Jun 2023 14:47:55 +0100 Subject: [PATCH 23/49] fix(explorer): fix broken protocol upgrade tx link (#4029) --- apps/explorer/.env | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/explorer/.env b/apps/explorer/.env index 1276209cf..964167bd5 100644 --- a/apps/explorer/.env +++ b/apps/explorer/.env @@ -15,6 +15,7 @@ NX_VEGA_GOVERNANCE_URL=https://governance.stagnet1.vega.rocks NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_VEGA_NETWORKS='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz"}' NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions +NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases/tag/ # App flags NX_EXPLORER_ASSETS=1 From 1d06be8f4e02ee91223945d7b4ed9cefa5c8f071 Mon Sep 17 00:00:00 2001 From: Maciek Date: Wed, 7 Jun 2023 22:44:28 +0200 Subject: [PATCH 24/49] fix: wrong css class (#4053) --- .../src/lib/orderbook-manager.tsx | 1 + libs/market-depth/src/lib/orderbook.spec.tsx | 2 + .../src/lib/orderbook.stories.tsx | 1 + libs/market-depth/src/lib/orderbook.tsx | 52 ++++++++++++------- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/libs/market-depth/src/lib/orderbook-manager.tsx b/libs/market-depth/src/lib/orderbook-manager.tsx index 486d7064b..8a9c73e95 100644 --- a/libs/market-depth/src/lib/orderbook-manager.tsx +++ b/libs/market-depth/src/lib/orderbook-manager.tsx @@ -65,6 +65,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => { asks={data?.depth.sell ?? []} decimalPlaces={market?.decimalPlaces ?? 0} positionDecimalPlaces={market?.positionDecimalPlaces ?? 0} + assetSymbol={market?.tradableInstrument.instrument.product.quoteName} onClick={(price: string) => { if (price) { updateOrder(marketId, { price }); diff --git a/libs/market-depth/src/lib/orderbook.spec.tsx b/libs/market-depth/src/lib/orderbook.spec.tsx index 5bb961ab9..e609b860a 100644 --- a/libs/market-depth/src/lib/orderbook.spec.tsx +++ b/libs/market-depth/src/lib/orderbook.spec.tsx @@ -41,6 +41,7 @@ describe('Orderbook', () => { decimalPlaces={decimalPlaces} positionDecimalPlaces={0} {...generateMockData(params)} + assetSymbol="USD" /> ); await waitFor(() => @@ -61,6 +62,7 @@ describe('Orderbook', () => { positionDecimalPlaces={0} onClick={onClickSpy} {...mockedData} + assetSymbol="USD" /> ); expect( diff --git a/libs/market-depth/src/lib/orderbook.stories.tsx b/libs/market-depth/src/lib/orderbook.stories.tsx index f710a8336..84400cddf 100644 --- a/libs/market-depth/src/lib/orderbook.stories.tsx +++ b/libs/market-depth/src/lib/orderbook.stories.tsx @@ -18,6 +18,7 @@ const OrderbookMockDataProvider = ({ decimalPlaces, ...props }: Props) => { positionDecimalPlaces={0} decimalPlaces={decimalPlaces} {...generateMockData({ ...props })} + assetSymbol="USD" />
diff --git a/libs/market-depth/src/lib/orderbook.tsx b/libs/market-depth/src/lib/orderbook.tsx index a03b250b9..479f9e25e 100644 --- a/libs/market-depth/src/lib/orderbook.tsx +++ b/libs/market-depth/src/lib/orderbook.tsx @@ -13,18 +13,10 @@ import classNames from 'classnames'; import { useState } from 'react'; import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth'; -interface OrderbookProps { - decimalPlaces: number; - positionDecimalPlaces: number; - onClick?: (price: string) => void; - midPrice?: string; - bids: PriceLevelFieldsFragment[]; - asks: PriceLevelFieldsFragment[]; -} - // Sets row height, will be used to calculate number of rows that can be // displayed each side of the book without overflow export const rowHeight = 17; +const rowGap = 1; const midHeight = 30; const OrderbookTable = ({ @@ -52,7 +44,10 @@ const OrderbookTable = ({ ) } > -
+
{rows.map((data) => ( void; + midPrice?: string; + bids: PriceLevelFieldsFragment[]; + asks: PriceLevelFieldsFragment[]; + assetSymbol: string | undefined; +} + export const Orderbook = ({ decimalPlaces, positionDecimalPlaces, @@ -78,6 +83,7 @@ export const Orderbook = ({ midPrice, asks, bids, + assetSymbol, }: OrderbookProps) => { const [resolution, setResolution] = useState(1); const resolutions = new Array( @@ -101,15 +107,18 @@ export const Orderbook = ({ {({ height }) => { const limit = Math.max( 1, - Math.floor((height - midHeight) / 2 / rowHeight) + Math.floor((height - midHeight) / 2 / (rowHeight + rowGap)) ); const askRows = groupedAsks?.slice(limit * -1) ?? []; const bidRows = groupedBids?.slice(0, limit) ?? []; return (
{askRows.length || bidRows.length ? ( <> @@ -121,14 +130,17 @@ export const Orderbook = ({ positionDecimalPlaces={positionDecimalPlaces} onClick={onClick} /> -
+
{midPrice && ( - - {addDecimalsFormatNumber(midPrice, decimalPlaces)} - + <> + + {addDecimalsFormatNumber(midPrice, decimalPlaces)} + + {assetSymbol} + )}
Date: Wed, 7 Jun 2023 23:58:20 +0100 Subject: [PATCH 25/49] chore(ui-toolkit,react-helpers,utils): publish new versions of ui-toolkit, react-helpers and utils (#4049) --- libs/react-helpers/package.json | 2 +- libs/ui-toolkit/package.json | 2 +- libs/utils/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/react-helpers/package.json b/libs/react-helpers/package.json index 7bcad0928..5359e539c 100644 --- a/libs/react-helpers/package.json +++ b/libs/react-helpers/package.json @@ -1,4 +1,4 @@ { "name": "@vegaprotocol/react-helpers", - "version": "0.2.3" + "version": "0.2.4" } diff --git a/libs/ui-toolkit/package.json b/libs/ui-toolkit/package.json index f83c25f79..755c3bda7 100644 --- a/libs/ui-toolkit/package.json +++ b/libs/ui-toolkit/package.json @@ -1,4 +1,4 @@ { "name": "@vegaprotocol/ui-toolkit", - "version": "0.12.2" + "version": "0.12.3" } diff --git a/libs/utils/package.json b/libs/utils/package.json index 335946a2c..6a29da3e3 100644 --- a/libs/utils/package.json +++ b/libs/utils/package.json @@ -1,5 +1,5 @@ { "name": "@vegaprotocol/utils", - "version": "0.0.3", + "version": "0.0.4", "type": "commonjs" } From 7b8f654906f95f8f5e3e76f90c9aea51c5ecc2d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Szpiech?= Date: Thu, 8 Jun 2023 15:47:23 +0200 Subject: [PATCH 26/49] test(trading): #3505 add test coverage to 6002-MDET-market-details (#4060) --- .../src/integration/market-info.cy.ts | 98 ++++++++++--------- .../src/integration/market-summary.cy.ts | 91 ++++++++++++----- apps/trading-e2e/src/integration/oracle.cy.ts | 27 +++++ .../liquidity-supplied/liquidity-supplied.tsx | 2 +- 4 files changed, 144 insertions(+), 74 deletions(-) create mode 100644 apps/trading-e2e/src/integration/oracle.cy.ts diff --git a/apps/trading-e2e/src/integration/market-info.cy.ts b/apps/trading-e2e/src/integration/market-info.cy.ts index 2c3555ebc..3d5d7ca64 100644 --- a/apps/trading-e2e/src/integration/market-info.cy.ts +++ b/apps/trading-e2e/src/integration/market-info.cy.ts @@ -1,15 +1,18 @@ import { MarketTradingModeMapping } from '@vegaprotocol/types'; import { MarketState } from '@vegaprotocol/types'; -const marketInfoBtn = 'Info'; -const row = 'key-value-table-row'; -const marketTitle = 'accordion-title'; -const externalLink = 'external-link'; const accordionContent = 'accordion-content'; +const blockExplorerLink = 'block-explorer-link'; +const dialogClose = 'dialog-close'; +const dialogContent = 'dialog-content'; +const externalLink = 'external-link'; +const githubLink = 'github-link'; +const liquidityLink = 'view-liquidity-link'; +const marketInfoBtn = 'Info'; +const marketTitle = 'accordion-title'; const providerName = 'provider-name'; -const oracleBannerStatus = 'oracle-banner-status'; -const oracleBannerDialogTrigger = 'oracle-banner-dialog-trigger'; -const oracleFullProfile = 'oracle-full-profile'; +const row = 'key-value-table-row'; +const verifiedProofs = 'verified-proofs'; describe('market info is displayed', { tags: '@smoke' }, () => { beforeEach(() => { @@ -17,12 +20,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => { }); before(() => { - cy.mockTradingPage( - MarketState.STATE_ACTIVE, - undefined, - undefined, - 'COMPROMISED' - ); + cy.mockTradingPage(MarketState.STATE_ACTIVE); cy.mockSubscription(); cy.visit('/#/markets/market-0'); cy.wait('@Markets'); @@ -30,16 +28,8 @@ describe('market info is displayed', { tags: '@smoke' }, () => { cy.wait('@MarketInfo'); }); - it('show oracle banner', () => { - cy.getByTestId(marketTitle).contains('Oracle').click(); - cy.getByTestId(oracleBannerStatus).should('contain.text', 'COMPROMISED'); - cy.getByTestId(oracleBannerDialogTrigger) - .should('contain.text', 'Show more') - .click(); - cy.getByTestId(oracleFullProfile).should('exist'); - }); - it('current fees displayed', () => { + // 6002-MDET-101 cy.getByTestId(marketTitle).contains('Current fees').click(); validateMarketDataRow(0, 'Maker Fee', '0.02%'); validateMarketDataRow(1, 'Infrastructure Fee', '0.05%'); @@ -48,6 +38,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => { }); it('market price', () => { + // 6002-MDET-102 cy.getByTestId(marketTitle).contains('Market price').click(); validateMarketDataRow(0, 'Mark Price', '46,126.90058'); validateMarketDataRow(1, 'Best Bid Price', '44,126.90058 '); @@ -56,6 +47,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => { }); it('market volume displayed', () => { + // 6002-MDET-103 cy.getByTestId(marketTitle).contains('Market volume').click(); validateMarketDataRow(1, 'Open Interest', '-'); validateMarketDataRow(2, 'Best Bid Volume', '1'); @@ -65,11 +57,13 @@ describe('market info is displayed', { tags: '@smoke' }, () => { }); it('insurance pool displayed', () => { + // 6002-MDET-104 cy.getByTestId(marketTitle).contains('Insurance pool').click(); validateMarketDataRow(0, 'Balance', '0'); }); it('key details displayed', () => { + // 6002-MDET-201 cy.getByTestId(marketTitle).contains('Key details').click(); validateMarketDataRow(0, 'Name', 'BTCUSD Monthly (30 Jun 2022)'); @@ -85,6 +79,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => { }); it('instrument displayed', () => { + // 6002-MDET-202 cy.getByTestId(marketTitle).contains('Instrument').click(); validateMarketDataRow(0, 'Market Name', 'BTCUSD Monthly (30 Jun 2022)'); @@ -93,7 +88,30 @@ describe('market info is displayed', { tags: '@smoke' }, () => { validateMarketDataRow(3, 'Quote Name', 'BTC'); }); + it('oracle displayed', () => { + // 6002-MDET-203 + cy.getByTestId(marketTitle).contains('Oracle').click(); + + cy.getByTestId(accordionContent) + .getByTestId(providerName) + .and('contain', 'Another oracle'); + + cy.getByTestId(providerName).should('be.visible').click(); + cy.getByTestId(dialogContent) + .eq(1) + .within(() => { + cy.getByTestId(blockExplorerLink).contains('Block explorer'); + cy.getByTestId(githubLink).contains('Oracle repository'); + }); + cy.getByTestId(dialogClose).click(); + + cy.getByTestId(accordionContent) + .getByTestId(verifiedProofs) + .and('contain', '1'); + }); + it('settlement asset displayed', () => { + // 6002-MDET-206 cy.getByTestId(marketTitle).contains('Settlement asset').click(); cy.window().then((win) => { cy.stub(win, 'prompt').returns('DISABLED WINDOW PROMPT'); @@ -112,9 +130,12 @@ describe('market info is displayed', { tags: '@smoke' }, () => { ); validateMarketDataRow(8, 'Withdrawal threshold', '0.0005'); validateMarketDataRow(9, 'Lifetime limit', '1,230'); + validateMarketDataRow(10, 'Infrastructure fee account balance', '0.00001'); + validateMarketDataRow(11, 'Global reward pool account balance', '0.00002'); }); it('metadata displayed', () => { + // 6002-MDET-207 cy.getByTestId(marketTitle).contains('Metadata').click(); validateMarketDataRow(0, 'Formerly', '076BB86A5AA41E3E'); @@ -125,18 +146,21 @@ describe('market info is displayed', { tags: '@smoke' }, () => { }); it('risk model displayed', () => { + // 6002-MDET-208 cy.getByTestId(marketTitle).contains('Risk model').click(); validateMarketDataRow(0, 'Tau', '0.0001140771161'); validateMarketDataRow(1, 'Risk Aversion Parameter', '0.01'); }); it('risk parameters displayed', () => { + // 6002-MDET-209 cy.getByTestId(marketTitle).contains('Risk parameters').click(); validateMarketDataRow(0, 'R', '0.016'); validateMarketDataRow(1, 'Sigma', '0.3'); }); it('risk factors displayed', () => { + // 6002-MDET-210 cy.getByTestId(marketTitle).contains('Risk factors').click(); validateMarketDataRow(0, 'Short', '0.008571790367285281'); @@ -144,6 +168,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => { }); it('price monitoring bounds displayed', () => { + // 6002-MDET-211 cy.getByTestId(marketTitle).contains('Price monitoring bounds 1').click(); cy.get('p.col-span-1').contains('99.99999% probability price bounds'); cy.get('p.col-span-1').contains('Within 43,200 seconds'); @@ -152,6 +177,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => { }); it('liquidity monitoring parameters displayed', () => { + // 6002-MDET-212 cy.getByTestId(marketTitle) .contains('Liquidity monitoring parameters') .click(); @@ -162,6 +188,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => { }); it('liquidity displayed', () => { + // 6002-MDET-213 cy.getByTestId(marketTitle) .contains(/Liquidity(?! m)/) .click(); @@ -169,14 +196,14 @@ describe('market info is displayed', { tags: '@smoke' }, () => { validateMarketDataRow(0, 'Target Stake', '10.00 tBTC'); validateMarketDataRow(1, 'Supplied Stake', '0.01 tBTC'); validateMarketDataRow(2, 'Market Value Proxy', '20.00 tBTC'); - - cy.getByTestId('view-liquidity-link').should( + cy.getByTestId(liquidityLink).should( 'have.text', 'View liquidity provision table' ); }); it('liquidity price range displayed', () => { + // 6002-MDET-214 cy.getByTestId(marketTitle).contains('Liquidity price range').click(); validateMarketDataRow(0, 'Liquidity Price Range', '2.00% of mid price'); @@ -184,29 +211,8 @@ describe('market info is displayed', { tags: '@smoke' }, () => { validateMarketDataRow(2, 'Highest Price', '47,049.438 BTC'); }); - it('oracle displayed', () => { - cy.getByTestId(marketTitle).contains('Oracle').click(); - - cy.getByTestId(accordionContent) - .getByTestId(providerName) - .and('contain', 'Another oracle'); - - cy.getByTestId(providerName).should('be.visible').click(); - - cy.getByTestId('dialog-content') - .eq(1) - .within(() => { - cy.getByTestId('block-explorer-link').contains('Block explorer'); - cy.getByTestId('github-link').contains('Oracle repository'); - }); - cy.getByTestId('dialog-close').click(); - - cy.getByTestId(accordionContent) - .getByTestId('verified-proofs') - .and('contain', '1'); - }); - it('proposal displayed', () => { + // 6002-MDET-301 cy.getByTestId(marketTitle).contains('Proposal').click(); cy.getByTestId(accordionContent) diff --git a/apps/trading-e2e/src/integration/market-summary.cy.ts b/apps/trading-e2e/src/integration/market-summary.cy.ts index 7d68eff8b..f6095849d 100644 --- a/apps/trading-e2e/src/integration/market-summary.cy.ts +++ b/apps/trading-e2e/src/integration/market-summary.cy.ts @@ -1,17 +1,25 @@ import * as Schema from '@vegaprotocol/types'; -const marketSummaryBlock = 'header-summary'; -const marketExpiry = 'market-expiry'; -const marketPrice = 'market-price'; -const marketChange = 'market-change'; -const marketVolume = 'market-volume'; -const marketMode = 'market-trading-mode'; -const marketState = 'market-state'; -const marketSettlement = 'market-settlement-asset'; -const percentageValue = 'price-change-percentage'; -const priceChangeValue = 'price-change'; +const expirtyTooltip = 'expiry-tooltip'; +const externalLink = 'external-link'; const itemHeader = 'item-header'; const itemValue = 'item-value'; +const link = 'link'; +const liquidityLink = 'view-liquidity-link'; +const liquiditySupplied = 'liquidity-supplied'; +const liquiditySuppliedTooltip = 'liquidity-supplied-tooltip'; +const marketChange = 'market-change'; +const marketExpiry = 'market-expiry'; +const marketMode = 'market-trading-mode'; +const marketName = 'header-title'; +const marketPrice = 'market-price'; +const marketSettlement = 'market-settlement-asset'; +const marketState = 'market-state'; +const marketSummaryBlock = 'header-summary'; +const marketVolume = 'market-volume'; +const percentageValue = 'price-change-percentage'; +const priceChangeValue = 'price-change'; +const tradingModeTooltip = 'trading-mode-tooltip'; describe('Market trading page', () => { before(() => { @@ -31,10 +39,12 @@ describe('Market trading page', () => { // 7002-SORD-001 // 7002-SORD-002 it('must display market name', () => { - cy.getByTestId('header-title').should('not.be.empty'); + // 6002-MDET-001 + cy.getByTestId(marketName).should('not.be.empty'); }); it('must see market expiry', () => { + // 6002-MDET-002 cy.getByTestId(marketSummaryBlock).within(() => { cy.getByTestId(marketExpiry).within(() => { cy.getByTestId(itemHeader).should('have.text', 'Expiry'); @@ -44,6 +54,7 @@ describe('Market trading page', () => { }); it('must see market price', () => { + // 6002-MDET-003 cy.getByTestId(marketSummaryBlock).within(() => { cy.getByTestId(marketPrice).within(() => { cy.getByTestId(itemHeader).should('have.text', 'Price'); @@ -53,6 +64,7 @@ describe('Market trading page', () => { }); it('must see market change', () => { + // 6002-MDET-004 cy.getByTestId(marketSummaryBlock).within(() => { cy.getByTestId(marketChange).within(() => { cy.getByTestId(itemHeader).should('have.text', 'Change (24h)'); @@ -63,6 +75,7 @@ describe('Market trading page', () => { }); it('must see market volume', () => { + // 6002-MDET-005 cy.getByTestId(marketSummaryBlock).within(() => { cy.getByTestId(marketVolume).within(() => { cy.getByTestId(itemHeader).should('have.text', 'Volume (24h)'); @@ -72,16 +85,21 @@ describe('Market trading page', () => { }); it('must see market mode', () => { + // 6002-MDET-006 cy.getByTestId(marketSummaryBlock).within(() => { cy.getByTestId(marketMode).within(() => { cy.getByTestId(itemHeader).should('have.text', 'Trading mode'); - cy.getByTestId(itemValue).should('not.be.empty'); + cy.getByTestId(itemValue).should( + 'have.text', + 'Monitoring auction - liquidity (target not met)' + ); }); }); }); - it('must see market state', () => { - //7002-SORD-061 + it('must see market status', () => { + // 6002-MDET-007 + // 7002-SORD-061 cy.getByTestId(marketSummaryBlock).within(() => { cy.getByTestId(marketState).within(() => { cy.getByTestId(itemHeader).should('have.text', 'Status'); @@ -91,6 +109,7 @@ describe('Market trading page', () => { }); it('must see market settlement', () => { + // 6002-MDET-008 cy.getByTestId(marketSummaryBlock).within(() => { cy.getByTestId(marketSettlement).within(() => { cy.getByTestId(itemHeader).should('have.text', 'Settlement asset'); @@ -98,15 +117,12 @@ describe('Market trading page', () => { }); }); }); - - it('must see market mode', () => { + it('must see market liquidity supplied', () => { + // 6002-MDET-009 cy.getByTestId(marketSummaryBlock).within(() => { - cy.getByTestId(marketMode).within(() => { - cy.getByTestId(itemHeader).should('have.text', 'Trading mode'); - cy.getByTestId(itemValue).should( - 'have.text', - 'Monitoring auction - liquidity (target not met)' - ); + cy.getByTestId(liquiditySupplied).within(() => { + cy.getByTestId(itemHeader).should('have.text', 'Liquidity supplied'); + cy.getByTestId(itemValue).should('not.be.empty'); }); }); }); @@ -121,14 +137,14 @@ describe('Market trading page', () => { .realHover(); }); }); - cy.getByTestId('expiry-tooltip') + cy.getByTestId(expirtyTooltip) .eq(0) .should( 'contain.text', 'This market expires when triggered by its oracle, not on a set date.' ) .within(() => { - cy.getByTestId('link') + cy.getByTestId(link) .should('have.attr', 'href') .and('include', Cypress.env('EXPLORER_URL')); }); @@ -154,15 +170,14 @@ describe('Market trading page', () => { .realHover(); }); }); - - cy.getByTestId('trading-mode-tooltip') + cy.getByTestId(tradingModeTooltip) .should( 'contain.text', 'This market is in auction until it reaches sufficient liquidity.' ) .eq(0) .within(() => { - cy.getByTestId('external-link') + cy.getByTestId(externalLink) .should('have.attr', 'href') .and('include', Cypress.env('TRADING_MODE_LINK')); @@ -174,5 +189,27 @@ describe('Market trading page', () => { } }); }); + + it('should see liquidity supplied tooltip', () => { + cy.getByTestId(marketSummaryBlock).within(() => { + cy.getByTestId(liquiditySupplied).within(() => { + cy.getByTestId(itemValue).realHover(); + }); + }); + cy.getByTestId(liquiditySuppliedTooltip) + .should('contain.text', 'Supplied stake') + .and('contain.text', 'Target stake') + .first() + .within(() => { + cy.getByTestId(liquidityLink).should( + 'have.text', + 'View liquidity provision table' + ); + cy.getByTestId(externalLink).should( + 'have.text', + 'Learn about providing liquidity' + ); + }); + }); }); }); diff --git a/apps/trading-e2e/src/integration/oracle.cy.ts b/apps/trading-e2e/src/integration/oracle.cy.ts new file mode 100644 index 000000000..58a687b2b --- /dev/null +++ b/apps/trading-e2e/src/integration/oracle.cy.ts @@ -0,0 +1,27 @@ +import { MarketState } from '@vegaprotocol/types'; + +const oracleBannerDialogTrigger = 'oracle-banner-dialog-trigger'; +const oracleBannerStatus = 'oracle-banner-status'; +const oracleFullProfile = 'oracle-full-profile'; + +describe('oracle information', { tags: '@smoke' }, () => { + before(() => { + cy.mockTradingPage( + MarketState.STATE_ACTIVE, + undefined, + undefined, + 'COMPROMISED' + ); + cy.mockSubscription(); + cy.visit('/#/markets/market-0'); + cy.wait('@Markets'); + }); + + it('show oracle banner', () => { + cy.getByTestId(oracleBannerStatus).should('contain.text', 'COMPROMISED'); + cy.getByTestId(oracleBannerDialogTrigger) + .should('contain.text', 'Show more') + .click(); + cy.getByTestId(oracleFullProfile).should('exist'); + }); +}); diff --git a/apps/trading/components/liquidity-supplied/liquidity-supplied.tsx b/apps/trading/components/liquidity-supplied/liquidity-supplied.tsx index b0837a9ff..e56951c40 100644 --- a/apps/trading/components/liquidity-supplied/liquidity-supplied.tsx +++ b/apps/trading/components/liquidity-supplied/liquidity-supplied.tsx @@ -99,7 +99,7 @@ export const MarketLiquiditySupplied = ({ AuctionTrigger.AUCTION_TRIGGER_UNABLE_TO_DEPLOY_LP_ORDERS; const description = marketId ? ( -
+
{t('Supplied stake')} From 5a8ff90890ecaa2b973bab21a05dd5a0b80ae894 Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Thu, 8 Jun 2023 09:16:51 -0700 Subject: [PATCH 27/49] fix(ui-toolkit): display of loader in large variant (#4056) Co-authored-by: maciek --- apps/static/src/assets/preloader.css | 62 +++++++++++++++---- apps/static/src/preloader.scss | 58 +++++++++++++++++ .../src/components/loader/loader.module.scss | 2 +- .../src/components/loader/loader.tsx | 2 +- 4 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 apps/static/src/preloader.scss diff --git a/apps/static/src/assets/preloader.css b/apps/static/src/assets/preloader.css index 7abb6fa99..fae2f38fd 100644 --- a/apps/static/src/assets/preloader.css +++ b/apps/static/src/assets/preloader.css @@ -26,27 +26,27 @@ animation-direction: reverse; } .pre-loader .loader-item:first-child { - animation-delay: -50ms; + animation-delay: -0.1s; animation-direction: alternate; } .pre-loader .loader-item:nth-child(2) { - animation-delay: 0.2s; + animation-delay: 0.3s; animation-direction: reverse; } .pre-loader .loader-item:nth-child(3) { - animation-delay: -0.6s; + animation-delay: -0.45s; animation-direction: alternate; } .pre-loader .loader-item:nth-child(4) { - animation-delay: 0.4s; + animation-delay: 1s; animation-direction: reverse; } .pre-loader .loader-item:nth-child(5) { - animation-delay: -0.5s; + animation-delay: -0.75s; animation-direction: alternate; } .pre-loader .loader-item:nth-child(6) { - animation-delay: 0.3s; + animation-delay: 0.9s; animation-direction: reverse; } .pre-loader .loader-item:nth-child(7) { @@ -54,11 +54,11 @@ animation-direction: alternate; } .pre-loader .loader-item:nth-child(8) { - animation-delay: 2s; + animation-delay: 1.6s; animation-direction: reverse; } .pre-loader .loader-item:nth-child(9) { - animation-delay: -0.9s; + animation-delay: -0.45s; animation-direction: alternate; } .pre-loader .loader-item:nth-child(10) { @@ -66,7 +66,7 @@ animation-direction: reverse; } .pre-loader .loader-item:nth-child(11) { - animation-delay: -0.55s; + animation-delay: -2.75s; animation-direction: alternate; } .pre-loader .loader-item:nth-child(12) { @@ -74,21 +74,57 @@ animation-direction: reverse; } .pre-loader .loader-item:nth-child(13) { - animation-delay: -0.65s; + animation-delay: -1.95s; animation-direction: alternate; } .pre-loader .loader-item:nth-child(14) { - animation-delay: 0.7s; + animation-delay: 2.8s; animation-direction: reverse; } .pre-loader .loader-item:nth-child(15) { - animation-delay: -3.75s; + animation-delay: -0.75s; animation-direction: alternate; } .pre-loader .loader-item:nth-child(16) { - animation-delay: 1.6s; + animation-delay: 4s; animation-direction: reverse; } +.pre-loader .loader-item:nth-child(17) { + animation-delay: -0.85s; + animation-direction: alternate; +} +.pre-loader .loader-item:nth-child(18) { + animation-delay: 1.8s; + animation-direction: reverse; +} +.pre-loader .loader-item:nth-child(19) { + animation-delay: -1.9s; + animation-direction: alternate; +} +.pre-loader .loader-item:nth-child(20) { + animation-delay: 5s; + animation-direction: reverse; +} +.pre-loader .loader-item:nth-child(21) { + animation-delay: -5.25s; + animation-direction: alternate; +} +.pre-loader .loader-item:nth-child(22) { + animation-delay: 4.4s; + animation-direction: reverse; +} +.pre-loader .loader-item:nth-child(23) { + animation-delay: -5.75s; + animation-direction: alternate; +} +.pre-loader .loader-item:nth-child(24) { + animation-delay: 4.8s; + animation-direction: reverse; +} +.pre-loader .loader-item:nth-child(25) { + animation-delay: -5s; + animation-direction: alternate; +} .pre-loader .loader-item { animation: flickering 0.4s linear infinite alternate; } diff --git a/apps/static/src/preloader.scss b/apps/static/src/preloader.scss new file mode 100644 index 000000000..b19dc6c59 --- /dev/null +++ b/apps/static/src/preloader.scss @@ -0,0 +1,58 @@ +.pre-loader { + display: flex; + width: 100%; + min-height: 100vh; + justify-content: center; + align-items: center; + .loader-item { + width: 10px; + height: 10px; + background: black; + } + .pre-loader-center { + align-items: center; + display: flex; + flex-direction: column; + } + .pre-loader-wrapper { + width: 50px; + height: 50px; + display: flex; + flex-wrap: wrap; + } + @for $i from 0 through 25 { + .loader-item:nth-child(#{$i}) { + @if $i % 2 == 0 { + animation-delay: #{$i * 50 * random(5)}ms; + animation-direction: reverse; + } @else { + animation-delay: #{$i * -50 * random(5)}ms; + animation-direction: alternate; + } + } + } + .loader-item { + animation: flickering 0.4s linear alternate infinite; + } + @keyframes flickering { + 0% { + opacity: 1; + } + 25% { + opacity: 1; + } + 26% { + opacity: 0; + } + 100% { + opacity: 0; + } + } +} +html.dark { + .pre-loader { + .loader-item { + background: white; + } + } +} diff --git a/libs/ui-toolkit/src/components/loader/loader.module.scss b/libs/ui-toolkit/src/components/loader/loader.module.scss index 3eb793b8f..61523e50d 100644 --- a/libs/ui-toolkit/src/components/loader/loader.module.scss +++ b/libs/ui-toolkit/src/components/loader/loader.module.scss @@ -1,4 +1,4 @@ -@for $i from 0 through 16 { +@for $i from 0 through 25 { .loader-item:nth-child(#{$i}) { @if $i % 2 == 0 { animation-delay: #{$i * 50 * random(5)}ms; diff --git a/libs/ui-toolkit/src/components/loader/loader.tsx b/libs/ui-toolkit/src/components/loader/loader.tsx index 475940d5b..045e1338c 100644 --- a/libs/ui-toolkit/src/components/loader/loader.tsx +++ b/libs/ui-toolkit/src/components/loader/loader.tsx @@ -25,7 +25,7 @@ export const Loader = ({ size = 'large', forceTheme }: LoaderProps) => { }); const wrapperClasses = size === 'small' ? 'w-[15px] h-[15px]' : 'w-[50px] h-[50px]'; - const items = size === 'small' ? 9 : 16; + const items = size === 'small' ? 9 : 25; const generate = useMemo(() => pseudoRandom(1), []); From 866e7232db109d157b307833fcbc0ceddea7fcf9 Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Thu, 8 Jun 2023 18:22:36 -0700 Subject: [PATCH 28/49] chore(trading): release v0.20.18-core-0.71.8 --- apps/trading/.env.mainnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/trading/.env.mainnet b/apps/trading/.env.mainnet index 52ac6cdec..e227063a2 100644 --- a/apps/trading/.env.mainnet +++ b/apps/trading/.env.mainnet @@ -14,4 +14,4 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_VEGA_CONSOLE_URL=https://console.vega.xyz # TAG name of the current app version - TODO: bump to the latest upon release -NX_APP_VERSION=v0.20.16-core-0.71.5 +NX_APP_VERSION=v0.20.18-core-0.71.8 From f66e976b668f638311fc5a9be23185c8b31d87be Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Thu, 8 Jun 2023 18:22:36 -0700 Subject: [PATCH 29/49] revert: "chore(trading): release v0.20.18-core-0.71.8" --- apps/trading/.env.mainnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/trading/.env.mainnet b/apps/trading/.env.mainnet index e227063a2..52ac6cdec 100644 --- a/apps/trading/.env.mainnet +++ b/apps/trading/.env.mainnet @@ -14,4 +14,4 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_VEGA_CONSOLE_URL=https://console.vega.xyz # TAG name of the current app version - TODO: bump to the latest upon release -NX_APP_VERSION=v0.20.18-core-0.71.8 +NX_APP_VERSION=v0.20.16-core-0.71.5 From b6286cd0f3a769ae61f2f359b8fcbc49c21b4d55 Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Thu, 8 Jun 2023 22:30:25 -0700 Subject: [PATCH 30/49] fix(deal-ticket): make ticket submit button always enabled (#4055) --- .../trading-deal-ticket-basics.cy.ts | 3 ++- .../trading-deal-ticket-order.cy.ts | 6 +++-- .../trading-deal-ticket-submit-account.cy.ts | 5 +++-- ...trading-deal-ticket-submit-suspended.cy.ts | 6 ++--- .../deal-ticket-validation/margin-warning.tsx | 1 + .../zero-balance-error.tsx | 10 ++++++--- .../deal-ticket/deal-ticket-button.tsx | 14 ++---------- .../deal-ticket/deal-ticket.spec.tsx | 3 +++ .../components/deal-ticket/deal-ticket.tsx | 5 +++-- .../src/utils/validate-market-state.ts | 22 +++++++++---------- .../src/utils/validate-market-trading-mode.ts | 6 ++--- 11 files changed, 42 insertions(+), 39 deletions(-) diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket-basics.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket-basics.cy.ts index f3af62e90..aa5fa7a0c 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket-basics.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket-basics.cy.ts @@ -88,7 +88,8 @@ describe( .pop() ?.toLowerCase()} and not accepting orders` ); - cy.getByTestId('place-order').should('be.disabled'); + // 7002-SORD-060 + cy.getByTestId('place-order').should('be.enabled'); }); }); }); diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts index 5ac9b5133..9e3030f10 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts @@ -77,7 +77,8 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => { it('must warn if order size input has too many digits after the decimal place', function () { // 7002-SORD-016 cy.getByTestId(orderSizeField).clear().type('1.234'); - cy.getByTestId(placeOrderBtn).should('be.disabled'); + // 7002-SORD-060 + cy.getByTestId(placeOrderBtn).should('be.enabled'); cy.getByTestId('dealticket-error-message-size-market').should( 'have.text', 'Size must be whole numbers for this market' @@ -86,12 +87,13 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => { it('must warn if order size is set to 0', function () { cy.getByTestId(orderSizeField).clear().type('0'); - cy.getByTestId(placeOrderBtn).should('be.disabled'); + cy.getByTestId(placeOrderBtn).should('be.enabled'); cy.getByTestId('dealticket-error-message-size-market').should( 'have.text', 'Size cannot be lower than 1' ); }); + it('must have total margin available', () => { // 7001-COLL-011 cy.getByTestId('tab-ticket') diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket-submit-account.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket-submit-account.cy.ts index dd6c8f04e..fb703229b 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket-submit-account.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket-submit-account.cy.ts @@ -23,13 +23,14 @@ describe( }); it('should show an error if your balance is zero', () => { - cy.getByTestId('place-order').should('be.disabled'); + // 7002-SORD-060 + cy.getByTestId('place-order').should('be.enabled'); // 7002-SORD-003 cy.getByTestId('dealticket-error-message-zero-balance').should( 'have.text', 'You need ' + 'tDAI' + - ' in your wallet to trade in this market.See all your collateral.Make a deposit' + ' in your wallet to trade in this market. See all your collateral.Make a deposit' ); cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist'); }); diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket-submit-suspended.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket-submit-suspended.cy.ts index e766410e9..18405b4e9 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket-submit-suspended.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket-submit-suspended.cy.ts @@ -34,9 +34,9 @@ describe('suspended market validation', { tags: '@regression' }, () => { it('should show warning for market order', function () { cy.getByTestId(toggleMarket).click(); - cy.getByTestId(placeOrderBtn).should('not.be.disabled'); + // 7002-SORD-060 + cy.getByTestId(placeOrderBtn).should('be.enabled'); cy.getByTestId(placeOrderBtn).click(); - cy.getByTestId(placeOrderBtn).should('be.disabled'); cy.getByTestId('dealticket-error-message-type').should( 'have.text', 'This market is in auction until it reaches sufficient liquidity. Only limit orders are permitted when market is in auction' @@ -59,7 +59,7 @@ describe('suspended market validation', { tags: '@regression' }, () => { cy.getByTestId(orderTIFDropDown).select( TIFlist.filter((item) => item.code === 'FOK')[0].value ); - cy.getByTestId(placeOrderBtn).should('be.disabled'); + cy.getByTestId(placeOrderBtn).should('be.enabled'); cy.getByTestId('dealticket-error-message-tif').should( 'have.text', 'This market is in auction until it reaches sufficient liquidity. Until the auction ends, you can only place GFA, GTT, or GTC limit orders' diff --git a/libs/deal-ticket/src/components/deal-ticket-validation/margin-warning.tsx b/libs/deal-ticket/src/components/deal-ticket-validation/margin-warning.tsx index 6fca7ea94..86efe1982 100644 --- a/libs/deal-ticket/src/components/deal-ticket-validation/margin-warning.tsx +++ b/libs/deal-ticket/src/components/deal-ticket-validation/margin-warning.tsx @@ -31,6 +31,7 @@ export const MarginWarning = ({ margin, balance, asset }: Props) => { text: t(`Deposit ${asset.symbol}`), action: () => openDepositDialog(asset.id), dataTestId: 'deal-ticket-deposit-dialog-button', + size: 'sm', }} /> ); diff --git a/libs/deal-ticket/src/components/deal-ticket-validation/zero-balance-error.tsx b/libs/deal-ticket/src/components/deal-ticket-validation/zero-balance-error.tsx index aca37b255..5866a2ea6 100644 --- a/libs/deal-ticket/src/components/deal-ticket-validation/zero-balance-error.tsx +++ b/libs/deal-ticket/src/components/deal-ticket-validation/zero-balance-error.tsx @@ -21,10 +21,14 @@ export const ZeroBalanceError = ({ testId="dealticket-error-message-zero-balance" message={ <> - You need {asset.symbol} in your wallet to trade in this market. + {t( + 'You need %s in your wallet to trade in this market. ', + asset.symbol + )} {onClickCollateral && ( <> - See all your collateral. + {t('See all your')}{' '} + collateral. )} @@ -33,7 +37,7 @@ export const ZeroBalanceError = ({ text: t(`Make a deposit`), action: () => openDepositDialog(asset.id), dataTestId: 'deal-ticket-deposit-dialog-button', - size: 'md', + size: 'sm', }} /> ); diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx index a7e84fcfa..361b2fc0c 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx @@ -1,25 +1,15 @@ import { t } from '@vegaprotocol/i18n'; import type { ButtonVariant } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; interface Props { - disabled: boolean; variant: ButtonVariant; } -export const DealTicketButton = ({ disabled, variant }: Props) => { - const { pubKey, isReadOnly } = useVegaWallet(); - const isDisabled = !pubKey || isReadOnly || disabled; +export const DealTicketButton = ({ variant }: Props) => { return (
-
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 548b04679..89c0b6c00 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 @@ -42,6 +42,9 @@ describe('DealTicket', () => { it('should display ticket defaults', () => { const { container } = render(generateJsx()); + // place order button should always be enabled + expect(screen.getByTestId('place-order')).toBeEnabled(); + // Assert defaults are used expect( screen.getByTestId(`order-type-${Schema.OrderType.TYPE_MARKET}`) diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx index 708b67d6e..d568afb57 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -217,6 +217,8 @@ export const DealTicket = ({ }); return; } + + // No error found above clear the error in case it was active on a previous render clearErrors('summary'); }, [ marketState, @@ -480,7 +482,6 @@ export const DealTicket = ({ onClickCollateral={onClickCollateral} /> = 1 || isReadOnly} variant={ order.side === Schema.Side.SIDE_BUY ? 'ternary' : 'secondary' } @@ -562,7 +563,7 @@ const SummaryMessage = memo( text: t('Connect wallet'), action: openVegaWalletDialog, dataTestId: 'order-connect-wallet', - size: 'md', + size: 'sm', }} />
diff --git a/libs/deal-ticket/src/utils/validate-market-state.ts b/libs/deal-ticket/src/utils/validate-market-state.ts index 0208ab973..4e87c55d2 100644 --- a/libs/deal-ticket/src/utils/validate-market-state.ts +++ b/libs/deal-ticket/src/utils/validate-market-state.ts @@ -1,14 +1,14 @@ import { t } from '@vegaprotocol/i18n'; -import * as Schema from '@vegaprotocol/types'; +import { MarketState, MarketStateMapping } from '@vegaprotocol/types'; -export const validateMarketState = (state: Schema.MarketState) => { +export const validateMarketState = (state: MarketState) => { if ( [ - Schema.MarketState.STATE_SETTLED, - Schema.MarketState.STATE_REJECTED, - Schema.MarketState.STATE_TRADING_TERMINATED, - Schema.MarketState.STATE_CANCELLED, - Schema.MarketState.STATE_CLOSED, + MarketState.STATE_SETTLED, + MarketState.STATE_REJECTED, + MarketState.STATE_TRADING_TERMINATED, + MarketState.STATE_CANCELLED, + MarketState.STATE_CLOSED, ].includes(state) ) { return t( @@ -16,7 +16,7 @@ export const validateMarketState = (state: Schema.MarketState) => { ); } - if (state === Schema.MarketState.STATE_PROPOSED) { + if (state === MarketState.STATE_PROPOSED) { return t( `This market is ${marketTranslations( state @@ -27,11 +27,11 @@ export const validateMarketState = (state: Schema.MarketState) => { return true; }; -const marketTranslations = (marketState: Schema.MarketState) => { +const marketTranslations = (marketState: MarketState) => { switch (marketState) { - case Schema.MarketState.STATE_TRADING_TERMINATED: + case MarketState.STATE_TRADING_TERMINATED: return t('terminated'); default: - return t(Schema.MarketStateMapping[marketState]).toLowerCase(); + return t(MarketStateMapping[marketState]).toLowerCase(); } }; diff --git a/libs/deal-ticket/src/utils/validate-market-trading-mode.ts b/libs/deal-ticket/src/utils/validate-market-trading-mode.ts index 687de02d0..7bff037a6 100644 --- a/libs/deal-ticket/src/utils/validate-market-trading-mode.ts +++ b/libs/deal-ticket/src/utils/validate-market-trading-mode.ts @@ -1,10 +1,10 @@ import { t } from '@vegaprotocol/i18n'; -import * as Schema from '@vegaprotocol/types'; +import { MarketTradingMode } from '@vegaprotocol/types'; export const validateMarketTradingMode = ( - marketTradingMode: Schema.MarketTradingMode + marketTradingMode: MarketTradingMode ) => { - if (marketTradingMode === Schema.MarketTradingMode.TRADING_MODE_NO_TRADING) { + if (marketTradingMode === MarketTradingMode.TRADING_MODE_NO_TRADING) { return t('Trading terminated'); } From e47b868c532ff136d69952e25155e9f50afeeb2d Mon Sep 17 00:00:00 2001 From: Maciek Date: Fri, 9 Jun 2023 08:58:16 +0200 Subject: [PATCH 31/49] fix(withdraws): clear eth network errors (#4044) --- libs/utils/src/index.ts | 1 + libs/utils/src/lib/resolve-network-name.ts | 6 ++ .../use-ethereum-withdraw-approval-toasts.tsx | 67 +++++++++++++------ ...hereum-withdraw-approvals-manager.spec.tsx | 31 +++++++-- ...se-ethereum-withdraw-approvals-manager.tsx | 20 ++++-- .../use-ethereum-withdraw-approvals-store.tsx | 10 +++ .../src/lib/withdrawal-approval-status.tsx | 56 +++++++++++++++- 7 files changed, 162 insertions(+), 29 deletions(-) create mode 100644 libs/utils/src/lib/resolve-network-name.ts diff --git a/libs/utils/src/index.ts b/libs/utils/src/index.ts index d260e0cff..163506667 100644 --- a/libs/utils/src/index.ts +++ b/libs/utils/src/index.ts @@ -13,3 +13,4 @@ export * from './lib/remove-0x'; export * from './lib/remove-pagination-wrapper'; export * from './lib/time'; export * from './lib/validate'; +export * from './lib/resolve-network-name'; diff --git a/libs/utils/src/lib/resolve-network-name.ts b/libs/utils/src/lib/resolve-network-name.ts new file mode 100644 index 000000000..ccab8f237 --- /dev/null +++ b/libs/utils/src/lib/resolve-network-name.ts @@ -0,0 +1,6 @@ +const NETWORK_NAME_MAP: Readonly> = { + '1': 'Ethereum Mainnet', + '11155111': 'Sepolia test network', +}; +export const resolveNetworkName = (chainId?: string): string => + NETWORK_NAME_MAP[chainId || ''] || `(chainID: ${chainId})`; diff --git a/libs/web3/src/lib/use-ethereum-withdraw-approval-toasts.tsx b/libs/web3/src/lib/use-ethereum-withdraw-approval-toasts.tsx index 0471eb3cc..077768bc2 100644 --- a/libs/web3/src/lib/use-ethereum-withdraw-approval-toasts.tsx +++ b/libs/web3/src/lib/use-ethereum-withdraw-approval-toasts.tsx @@ -9,7 +9,10 @@ import { Intent } from '@vegaprotocol/ui-toolkit'; import { useCallback } from 'react'; import compact from 'lodash/compact'; import type { EthWithdrawalApprovalState } from './use-ethereum-withdraw-approvals-store'; -import { useEthWithdrawApprovalsStore } from './use-ethereum-withdraw-approvals-store'; +import { + useEthWithdrawApprovalsStore, + WithdrawalFailure, +} from './use-ethereum-withdraw-approvals-store'; import { ApprovalStatus } from './use-ethereum-withdraw-approvals-store'; import { VerificationStatus } from './withdrawal-approval-status'; @@ -26,12 +29,23 @@ const EthWithdrawalApprovalToastContent = ({ }: { tx: EthWithdrawalApprovalState; }) => { + const isConnectionFailure = + tx.failureReason && + [ + WithdrawalFailure.WrongConnection, + WithdrawalFailure.NoConnection, + ].includes(tx.failureReason); + let title = ''; if (tx.status === ApprovalStatus.Error) { title = t('Error occurred'); } if (tx.status === ApprovalStatus.Pending) { - title = t('Pending approval'); + if (tx.failureReason && isConnectionFailure) { + title = tx.message || t('Withdraw failure'); + } else { + title = t('Pending approval'); + } } if (tx.status === ApprovalStatus.Delayed) { title = t('Delayed'); @@ -39,11 +53,12 @@ const EthWithdrawalApprovalToastContent = ({ if (tx.status === ApprovalStatus.Ready) { title = t('Approved'); } + const num = formatNumber( toBigNum(tx.withdrawal.amount, tx.withdrawal.asset.decimals), tx.withdrawal.asset.decimals ); - const details = ( + const details = isConnectionFailure ? null : ( {t('Withdraw')} {num} {tx.withdrawal.asset.symbol} @@ -61,7 +76,8 @@ const EthWithdrawalApprovalToastContent = ({ }; const isFinal = (tx: EthWithdrawalApprovalState) => - [ApprovalStatus.Ready, ApprovalStatus.Error].includes(tx.status); + [ApprovalStatus.Ready, ApprovalStatus.Error].includes(tx.status) && + !tx.failureReason; export const useEthereumWithdrawApprovalsToasts = () => { const [setToast, remove] = useToasts((state) => [ @@ -74,21 +90,34 @@ export const useEthereumWithdrawApprovalsToasts = () => { ]); const fromWithdrawalApproval = useCallback( - (tx: EthWithdrawalApprovalState): Toast => ({ - id: `withdrawal-${tx.id}`, - intent: intentMap[tx.status], - onClose: () => { - if ([ApprovalStatus.Error, ApprovalStatus.Ready].includes(tx.status)) { - deleteTx(tx.id); - } else { - dismissTx(tx.id); - } - remove(`withdrawal-${tx.id}`); - }, - loader: tx.status === ApprovalStatus.Pending, - content: , - closeAfter: isFinal(tx) ? CLOSE_AFTER : undefined, - }), + (tx: EthWithdrawalApprovalState): Toast => { + const loader = + tx.status === ApprovalStatus.Pending && + !( + tx.failureReason && + [ + WithdrawalFailure.WrongConnection, + WithdrawalFailure.NoConnection, + ].includes(tx.failureReason) + ); + return { + id: `withdrawal-${tx.id}`, + intent: intentMap[tx.status], + onClose: () => { + if ( + [ApprovalStatus.Error, ApprovalStatus.Ready].includes(tx.status) + ) { + deleteTx(tx.id); + } else { + dismissTx(tx.id); + } + remove(`withdrawal-${tx.id}`); + }, + loader, + content: , + closeAfter: isFinal(tx) ? CLOSE_AFTER : undefined, + }; + }, [deleteTx, dismissTx, remove] ); diff --git a/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.spec.tsx b/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.spec.tsx index f9321ea35..03ae3f7a4 100644 --- a/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.spec.tsx +++ b/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.spec.tsx @@ -5,7 +5,10 @@ import type { ReactNode } from 'react'; import { MockedProvider } from '@apollo/client/testing'; import waitForNextTick from 'flush-promises'; import * as Schema from '@vegaprotocol/types'; -import { ApprovalStatus } from './use-ethereum-withdraw-approvals-store'; +import { + ApprovalStatus, + WithdrawalFailure, +} from './use-ethereum-withdraw-approvals-store'; import BigNumber from 'bignumber.js'; import type { EthWithdrawApprovalStore, @@ -21,7 +24,7 @@ import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; const mockWeb3Provider = jest.fn(); -let mockChainId = 111111; +let mockChainId: number | undefined = 111111; jest.mock('@web3-react/core', () => ({ useWeb3React: () => ({ provider: mockWeb3Provider(), @@ -314,9 +317,29 @@ describe('useEthWithdrawApprovalsManager', () => { update, }); render(); - expect(update.mock.calls[0][1].status).toEqual(ApprovalStatus.Error); + expect(update.mock.calls[0][1].status).toEqual(ApprovalStatus.Pending); + expect(update.mock.calls[0][1].message).toEqual('Change network'); + expect(update.mock.calls[0][1].failureReason).toEqual( + WithdrawalFailure.WrongConnection + ); + mockChainId = 111111; + }); + + it('detect no chainId', () => { + mockChainId = undefined; + const transaction = createWithdrawTransaction(); + mockEthTransactionStoreState.mockReturnValue({ create }); + mockEthWithdrawApprovalsStoreState.mockReturnValue({ + transactions: [transaction], + update, + }); + render(); + expect(update.mock.calls[0][1].status).toEqual(ApprovalStatus.Pending); expect(update.mock.calls[0][1].message).toEqual( - 'You are on the wrong network' + 'Connect wallet to withdraw' + ); + expect(update.mock.calls[0][1].failureReason).toEqual( + WithdrawalFailure.NoConnection ); mockChainId = 111111; }); diff --git a/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.tsx b/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.tsx index 89d9947cd..d06abcddf 100644 --- a/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.tsx +++ b/libs/web3/src/lib/use-ethereum-withdraw-approvals-manager.tsx @@ -1,6 +1,6 @@ import { useApolloClient } from '@apollo/client'; import BigNumber from 'bignumber.js'; -import { useRef, useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { addDecimal } from '@vegaprotocol/utils'; import { useGetWithdrawThreshold } from './use-get-withdraw-threshold'; import { useGetWithdrawDelay } from './use-get-withdraw-delay'; @@ -21,8 +21,9 @@ import { WithdrawalApprovalDocument } from '@vegaprotocol/wallet'; import { useEthTransactionStore } from './use-ethereum-transaction-store'; import { - useEthWithdrawApprovalsStore, ApprovalStatus, + useEthWithdrawApprovalsStore, + WithdrawalFailure, } from './use-ethereum-withdraw-approvals-store'; export const useEthWithdrawApprovalsManager = () => { @@ -55,16 +56,27 @@ export const useEthWithdrawApprovalsManager = () => { message: t( `Invalid asset source: ${withdrawal.asset.source.__typename}` ), + failureReason: WithdrawalFailure.InvalidAsset, + }); + return; + } + if (!chainId) { + update(transaction.id, { + status: ApprovalStatus.Pending, + message: t(`Connect wallet to withdraw`), + failureReason: WithdrawalFailure.NoConnection, }); return; } if (chainId?.toString() !== config?.chain_id) { update(transaction.id, { - status: ApprovalStatus.Error, - message: t(`You are on the wrong network`), + status: ApprovalStatus.Pending, + message: t(`Change network`), + failureReason: WithdrawalFailure.WrongConnection, }); return; } + update(transaction.id, { status: ApprovalStatus.Pending, message: t('Verifying withdrawal approval'), diff --git a/libs/web3/src/lib/use-ethereum-withdraw-approvals-store.tsx b/libs/web3/src/lib/use-ethereum-withdraw-approvals-store.tsx index 4287dda7f..6949a8716 100644 --- a/libs/web3/src/lib/use-ethereum-withdraw-approvals-store.tsx +++ b/libs/web3/src/lib/use-ethereum-withdraw-approvals-store.tsx @@ -14,6 +14,13 @@ export enum ApprovalStatus { Error = 'Error', Ready = 'Ready', } + +export enum WithdrawalFailure { + InvalidAsset, + NoConnection, + WrongConnection, +} + export interface EthWithdrawalApprovalState { id: number; createdAt: Date; @@ -24,6 +31,7 @@ export interface EthWithdrawalApprovalState { dialogOpen?: boolean; withdrawal: WithdrawalBusEventFieldsFragment; approval?: WithdrawalApprovalQuery['erc20WithdrawalApproval']; + failureReason?: WithdrawalFailure; } export interface EthWithdrawApprovalStore { transactions: (EthWithdrawalApprovalState | undefined)[]; @@ -42,6 +50,7 @@ export interface EthWithdrawApprovalStore { | 'threshold' | 'completeTimestamp' | 'dialogOpen' + | 'failureReason' > > ) => void; @@ -86,6 +95,7 @@ export const useEthWithdrawApprovalsStore = create()( | 'threshold' | 'completeTimestamp' | 'dialogOpen' + | 'failureReason' > > ) => diff --git a/libs/web3/src/lib/withdrawal-approval-status.tsx b/libs/web3/src/lib/withdrawal-approval-status.tsx index 63527b439..61f233ef9 100644 --- a/libs/web3/src/lib/withdrawal-approval-status.tsx +++ b/libs/web3/src/lib/withdrawal-approval-status.tsx @@ -1,17 +1,69 @@ import { t } from '@vegaprotocol/i18n'; -import { getDateTimeFormat } from '@vegaprotocol/utils'; +import { + getDateTimeFormat, + resolveNetworkName, + truncateByChars, +} from '@vegaprotocol/utils'; import type { EthWithdrawalApprovalState } from './use-ethereum-withdraw-approvals-store'; -import { ApprovalStatus } from './use-ethereum-withdraw-approvals-store'; +import { + ApprovalStatus, + useEthWithdrawApprovalsStore, + WithdrawalFailure, +} from './use-ethereum-withdraw-approvals-store'; +import { useEthereumConfig } from './use-ethereum-config'; +import { Button, useToasts } from '@vegaprotocol/ui-toolkit'; +import { useWeb3ConnectStore } from './web3-connect-store'; export const VerificationStatus = ({ state, }: { state: EthWithdrawalApprovalState; }) => { + const { config } = useEthereumConfig(); + const openDialog = useWeb3ConnectStore((state) => state.open); + const remove = useToasts((state) => state.remove); + const deleteTx = useEthWithdrawApprovalsStore((state) => state.delete); + if (state.status === ApprovalStatus.Error) { return

{state.message || t('Something went wrong')}

; } + if ( + state.failureReason && + [ + WithdrawalFailure.WrongConnection, + WithdrawalFailure.NoConnection, + ].includes(state.failureReason) + ) { + return state.failureReason === WithdrawalFailure.NoConnection ? ( + <> +

+ {t('To complete this withdrawal, connect the Ethereum wallet %s', [ + truncateByChars(state.withdrawal.details?.receiverAddress || ' '), + ])} +

+ + + ) : ( + <> +

{t('Your Ethereum wallet is connected to the wrong network.')}

+

+ {t('Go to your Ethereum wallet and connect to the network %s', [ + resolveNetworkName(config?.chain_id), + ])} +

+ + ); + } + if (state.status === ApprovalStatus.Pending) { return

{t('Verifying...')}

; } From 5cab040189f3b36dc0ed3f913ef6e2a24931372c Mon Sep 17 00:00:00 2001 From: Sam Keen Date: Fri, 9 Jun 2023 16:13:44 +0100 Subject: [PATCH 32/49] fix(governance): temporarily skipped flaky staking tests (#4068) --- apps/governance-e2e/src/integration/flow/staking-flow.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/governance-e2e/src/integration/flow/staking-flow.cy.ts b/apps/governance-e2e/src/integration/flow/staking-flow.cy.ts index dfa8b01bd..21e757e69 100644 --- a/apps/governance-e2e/src/integration/flow/staking-flow.cy.ts +++ b/apps/governance-e2e/src/integration/flow/staking-flow.cy.ts @@ -63,7 +63,7 @@ context( vegaWalletSetSpecifiedApprovalAmount('1000'); }); - describe('Eth wallet - contains VEGA tokens', function () { + describe.skip('Eth wallet - contains VEGA tokens', function () { beforeEach( 'teardown wallet & drill into a specific validator', function () { From f13e456a29318b49eed3917512d78f392c5557e3 Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Fri, 9 Jun 2023 10:56:42 -0700 Subject: [PATCH 33/49] ci(trading,governance,explorer): remove affected project text manipulation --- .github/workflows/ci-cd-trigger.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci-cd-trigger.yml b/.github/workflows/ci-cd-trigger.yml index da5d340ea..7ce11f714 100644 --- a/.github/workflows/ci-cd-trigger.yml +++ b/.github/workflows/ci-cd-trigger.yml @@ -151,7 +151,6 @@ jobs: fi projects_e2e=${projects_e2e%?} projects_e2e=[${projects_e2e// /,}] - projects=${projects%?} projects=[${projects// /,}] echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV echo PROJECTS=$projects >> $GITHUB_ENV From a6f3a08d2fe5cf3613a3b1abcfe7179fc33af9dc Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Fri, 9 Jun 2023 15:50:54 -0700 Subject: [PATCH 34/49] chore(trading,governance,explorer): release v0.20.18-core-0.71.8 --- apps/trading/.env.mainnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/trading/.env.mainnet b/apps/trading/.env.mainnet index 52ac6cdec..e227063a2 100644 --- a/apps/trading/.env.mainnet +++ b/apps/trading/.env.mainnet @@ -14,4 +14,4 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_VEGA_CONSOLE_URL=https://console.vega.xyz # TAG name of the current app version - TODO: bump to the latest upon release -NX_APP_VERSION=v0.20.16-core-0.71.5 +NX_APP_VERSION=v0.20.18-core-0.71.8 From bf6c13f5237e63d9f63c4d1bb9a54d8817dd9159 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 10 Jun 2023 01:37:50 +0100 Subject: [PATCH 35/49] test(trading): 3502-POSI-e2e-tests (#4064) --- .../src/integration/trading-positions.cy.ts | 490 ++++++++++++------ libs/positions/src/lib/positions.mock.ts | 22 +- package.json | 2 +- yarn.lock | 8 +- 4 files changed, 346 insertions(+), 176 deletions(-) diff --git a/apps/trading-e2e/src/integration/trading-positions.cy.ts b/apps/trading-e2e/src/integration/trading-positions.cy.ts index 4e9ba75d7..b8f926f82 100644 --- a/apps/trading-e2e/src/integration/trading-positions.cy.ts +++ b/apps/trading-e2e/src/integration/trading-positions.cy.ts @@ -3,6 +3,18 @@ import { aliasGQLQuery } from '@vegaprotocol/cypress'; import { marketsDataQuery } from '@vegaprotocol/mock'; import { positionsQuery } from '@vegaprotocol/mock'; +// #region consts +const closePosition = 'close-position'; +const dialogCloseX = 'dialog-close'; +const dialogContent = 'dialog-content'; +const dropDownMenu = 'dropdown-menu'; +const marketActionsContent = 'market-actions-content'; +const positions = 'Positions'; +const tabPositions = 'tab-positions'; +const toastContent = 'toast-content'; +const tooltipContent = 'tooltip-content'; +// #endregion + beforeEach(() => { cy.mockTradingPage(); cy.mockSubscription(); @@ -11,8 +23,9 @@ beforeEach(() => { describe('positions', { tags: '@smoke', testIsolation: true }, () => { it('renders positions on trading page', () => { - cy.visit('/#/markets/market-0'); - cy.getByTestId('Positions').click(); + visitAndClickPositions(); + // 7004-POSI-001 + // 7004-POSI-002 validatePositionsDisplayed(); }); @@ -35,174 +48,313 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => { } aliasGQLQuery(req, 'Positions', positions); }); - cy.visit('/#/portfolio'); - cy.getByTestId('Positions').click(); + visitAndClickPositions(); + // 7004-POSI-001 + // 7004-POSI-002 validatePositionsDisplayed(true); }); - describe('renders position among some graphql errors', () => { - it('rows should be displayed despite errors', () => { - const errors = [ - { - message: - 'no market data for market: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a', - path: ['marketsConnection', 'edges'], - extensions: { - code: 13, - type: 'Internal', - }, - }, - ]; - const marketData = marketsDataQuery(); - const edges = marketData.marketsConnection?.edges.map((market) => { - const replace = - market.node.data?.market.id === 'market-2' ? null : market.node.data; - return { ...market, node: { ...market.node, data: replace } }; - }); - const overrides = { - ...marketData, - marketsConnection: { ...marketData.marketsConnection, edges }, - }; - cy.mockGQL((req) => { - aliasGQLQuery(req, 'MarketsData', overrides, errors); - }); - cy.visit('/#/markets/market-0'); - const emptyCells = [ - 'notional', - 'markPrice', - 'currentLeverage', - 'averageEntryPrice', - ]; - cy.getByTestId('tab-positions') - .first() - .within(() => { - cy.get( - '[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]' - ) - .eq(1) - .within(() => { - emptyCells.forEach((cell) => { - cy.get(`[col-id="${cell}"]`).should('contain.text', '-'); - }); - }); - }); - }); - it('error message should be displayed', () => { - const errors = [ - { - message: - 'no market data for asset: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a', - path: ['assets', 'edges'], - extensions: { - code: 13, - type: 'Internal', - }, - }, - ]; - const overrides = { - marketsConnection: { edges: [] }, - }; - cy.mockGQL((req) => { - aliasGQLQuery(req, 'MarketsData', overrides, errors); - }); - cy.visit('/#/markets/market-0'); - cy.get('[data-testid="tab-positions"]').contains('no market data'); - }); + + it('Close my position', () => { + visitAndClickPositions(); + cy.getByTestId(closePosition).first().click(); + // 7004-POSI-010 + cy.getByTestId(toastContent).should( + 'contain.text', + 'Awaiting confirmation' + ); }); - - describe('sorting by ag-grid columns should work well', () => { - it('sorting by Market', () => { - cy.visit('/#/markets/market-0'); - const marketsSortedDefault = [ - 'ACTIVE MARKET', - 'Apple Monthly (30 Jun 2022)', - 'SUSPENDED MARKET', - ]; - const marketsSortedAsc = ['ACTIVE MARKET', 'Apple Monthly (30 Jun 2022)']; - const marketsSortedDesc = [ - 'SUSPENDED MARKET', - 'Apple Monthly (30 Jun 2022)', - 'ACTIVE MARKET', - ]; - cy.getByTestId('Positions').click(); - checkSorting( - 'marketName', - marketsSortedDefault, - marketsSortedAsc, - marketsSortedDesc - ); - }); - it('sorting by notional', () => { - cy.visit('/#/markets/market-0'); - const marketsSortedDefault = [ - '276,761.40348', - '46,126.90058', - '1,688.20', - ]; - const marketsSortedAsc = ['1,688.20', '46,126.90058', '276,761.40348']; - const marketsSortedDesc = ['276,761.40348', '46,126.90058', '1,688.20']; - cy.getByTestId('Positions').click(); - checkSorting( - 'notional', - marketsSortedDefault, - marketsSortedAsc, - marketsSortedDesc - ); - }); - it('sorting by unrealisedPNL', () => { - cy.visit('/#/markets/market-0'); - const marketsSortedDefault = ['8.95', '-0.22519', '8.95']; - const marketsSortedAsc = ['-0.22519', '8.95', '8.95']; - const marketsSortedDesc = ['8.95', '8.95', '-0.22519']; - cy.getByTestId('Positions').click(); - checkSorting( - 'unrealisedPNL', - marketsSortedDefault, - marketsSortedAsc, - marketsSortedDesc - ); - }); - }); - - function validatePositionsDisplayed(multiKey = false) { - cy.getByTestId('tab-positions').should('be.visible'); - cy.getByTestId('tab-positions').within(() => { - cy.get('[col-id="marketName"]') - .should('be.visible') - .each(($marketSymbol) => { - cy.wrap($marketSymbol).invoke('text').should('not.be.empty'); - }); - - cy.get('.ag-center-cols-container [col-id="openVolume"]').each( - ($openVolume) => { - cy.wrap($openVolume).invoke('text').should('not.be.empty'); - } - ); - - // includes average entry price, mark price, realised PNL & leverage - cy.getByTestId('flash-cell').each(($prices) => { - cy.wrap($prices).invoke('text').should('not.be.empty'); - }); - - if (!multiKey) { - cy.get('[col-id="currentLeverage"]').should('contain.text', '2.846.1'); - cy.get('[col-id="marginAccountBalance"]') // margin allocated - .should('contain.text', '0.01'); - } - - cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => { - cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty'); - }); - - cy.get('[col-id="notional"]').should('contain.text', '276,761.40348'); // Total tDAI position - cy.get('[col-id="realisedPNL"]').should('contain.text', '2.30'); // Total Realised PNL - cy.get('[col-id="unrealisedPNL"]').should('contain.text', '8.95'); // Total Unrealised PNL - - cy.get('.ag-header-row [col-id="notional"]') - .should('contain.text', 'Notional') - .realHover(); - cy.get('.ag-popup').should('contain.text', 'Mark price x open volume'); - }); - - cy.getByTestId('close-position').should('be.visible').and('have.length', 3); - } }); +describe('positions', { tags: '@regression', testIsolation: true }, () => { + it('rows should be displayed despite errors', () => { + const errors = [ + { + message: + 'no market data for market: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a', + path: ['marketsConnection', 'edges'], + extensions: { + code: 13, + type: 'Internal', + }, + }, + ]; + const marketData = marketsDataQuery(); + const edges = marketData.marketsConnection?.edges.map((market) => { + const replace = + market.node.data?.market.id === 'market-2' ? null : market.node.data; + return { ...market, node: { ...market.node, data: replace } }; + }); + const overrides = { + ...marketData, + marketsConnection: { ...marketData.marketsConnection, edges }, + }; + cy.mockGQL((req) => { + aliasGQLQuery(req, 'MarketsData', overrides, errors); + }); + cy.visit('/#/markets/market-0'); + const emptyCells = [ + 'notional', + 'markPrice', + 'currentLeverage', + 'averageEntryPrice', + ]; + cy.getByTestId(tabPositions) + .first() + .within(() => { + cy.get( + '[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]' + ) + .eq(1) + .within(() => { + emptyCells.forEach((cell) => { + cy.get(`[col-id="${cell}"]`).should('contain.text', '-'); + }); + }); + }); + }); + + it('error message should be displayed', () => { + const errors = [ + { + message: + 'no market data for asset: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a', + path: ['assets', 'edges'], + extensions: { + code: 13, + type: 'Internal', + }, + }, + ]; + const overrides = { + marketsConnection: { edges: [] }, + }; + cy.mockGQL((req) => { + aliasGQLQuery(req, 'MarketsData', overrides, errors); + }); + cy.visit('/#/markets/market-0'); + cy.getByTestId(tabPositions).contains('no market data'); + }); + + it('sorting by Market', () => { + visitAndClickPositions(); + const marketsSortedDefault = [ + 'ACTIVE MARKET', + 'Apple Monthly (30 Jun 2022)', + 'ETHBTC Quarterly (30 Jun 2022)', + 'SUSPENDED MARKET', + ]; + const marketsSortedAsc = [ + 'ACTIVE MARKET', + 'Apple Monthly (30 Jun 2022)', + 'ETHBTC Quarterly (30 Jun 2022)', + 'SUSPENDED MARKET', + ]; + const marketsSortedDesc = [ + 'SUSPENDED MARKET', + 'ETHBTC Quarterly (30 Jun 2022)', + 'Apple Monthly (30 Jun 2022)', + 'ACTIVE MARKET', + ]; + cy.getByTestId(positions).click(); + // 7004-POSI-003 + checkSorting( + 'marketName', + marketsSortedDefault, + marketsSortedAsc, + marketsSortedDesc + ); + }); + + it('Resize column', () => { + let elementWidth: number; + visitAndClickPositions(); + cy.get('.ag-overlay-loading-wrapper').should('not.be.visible'); + cy.get('.ag-header-container').within(() => { + cy.get(`[col-id="marketName"]`) + .find('.ag-header-cell-resize') + .realMouseDown() + .realMouseMove(250, 0) + .realMouseUp(); + }); + + // 7004-POSI-006 + cy.get(`[col-id="marketName"]`) + .invoke('width') + .should('be.greaterThan', 250); + cy.get(`[col-id="marketName"]`) + .invoke('width') + .then((width) => { + elementWidth = width as number; + }) + .then(() => { + let localStorageCopy: Record; + cy.window().then((win) => { + localStorageCopy = { ...win.localStorage }; + }); + + cy.reload(); + cy.window().then((win) => { + Object.keys(localStorageCopy).forEach((key) => { + win.localStorage.setItem(key, localStorageCopy[key]); + }); + }); + + // 7004-POSI-012 + cy.get('[col-id="marketName"]') + .invoke('width') + .should('equal', elementWidth); + }); + }); + + it('Scroll horizontally', () => { + visitAndClickPositions(); + + cy.get('.ag-header-container').within(() => { + cy.get(`[col-id="marketName"]`) + .find('.ag-header-cell-resize') + .realMouseDown() + .realMouseMove(400, 0) + .realMouseUp(); + }); + cy.get('[col-id="marketName"]').should('be.visible'); + cy.get('.ag-body-horizontal-scroll-viewport').realMouseWheel({ + deltaX: 500, + }); + // 7004-POSI-004 + cy.get('[col-id="updatedAt"]').should('be.visible'); + }); + + it('Drag and drop columns', () => { + visitAndClickPositions(); + cy.get('.ag-overlay-loading-wrapper').should('not.be.visible'); + cy.get('[col-id="marketName"]') + .realMouseDown() + .realMouseMove(700, 15) + .realMouseUp(); + + // 7004-POSI-005 + cy.get('[col-id="marketName"]').should(($element) => { + const attributeValue = $element.attr('aria-colindex'); + expect(attributeValue).not.to.equal('1'); + }); + }); + + it('I can see warnings', () => { + visitAndClickPositions(); + + cy.get('[col-id="openVolume"]').within(() => { + cy.get('[aria-label="warning-sign icon"]') + .should('be.visible') + .realHover(); + }); + // 7004-POSI-011 + cy.getByTestId(tooltipContent).should('be.visible'); + }); + + it('Positive and Negative color change', () => { + cy.visit('/#/markets/market-0'); + cy.getByTestId(positions).click(); + // 7004-POSI-007 + cy.get('.ag-center-cols-container').within(() => { + assertPNLColor( + '[col-id="realisedPNL"]', + 'text-vega-green', + 'text-vega-pink' + ); + }); + cy.get('.ag-center-cols-container').within(() => { + assertPNLColor( + '[col-id="unrealisedPNL"]', + 'text-vega-green', + 'text-vega-pink' + ); + }); + cy.get('.ag-center-cols-container').within(() => { + assertPNLColor( + '[col-id="openVolume"]', + 'text-vega-green', + 'text-vega-pink' + ); + }); + }); + + it('View settlement asset', () => { + visitAndClickPositions(); + cy.get('[col-id="asset"]').within(() => { + cy.get('button[type="button"]').first().click(); + }); + // 7004-POSI-008 + cy.getByTestId(dialogContent).should('be.visible'); + cy.getByTestId(dialogCloseX).click(); + cy.getByTestId(dropDownMenu).first().click(); + cy.getByTestId(marketActionsContent).click(); + // 7004-POSI-009 + cy.getByTestId(dialogContent).should('be.visible'); + }); +}); +function validatePositionsDisplayed(multiKey = false) { + cy.getByTestId('tab-positions').should('be.visible'); + cy.getByTestId('tab-positions').within(() => { + cy.get('[col-id="marketName"]') + .should('be.visible') + .each(($marketSymbol) => { + cy.wrap($marketSymbol).invoke('text').should('not.be.empty'); + }); + + cy.get('.ag-center-cols-container [col-id="openVolume"]').each( + ($openVolume) => { + cy.wrap($openVolume).invoke('text').should('not.be.empty'); + } + ); + + // includes average entry price, mark price, realised PNL & leverage + cy.getByTestId('flash-cell').each(($prices) => { + cy.wrap($prices).invoke('text').should('not.be.empty'); + }); + + if (!multiKey) { + cy.get('[col-id="currentLeverage"]').should('contain.text', '2.846.1'); + cy.get('[col-id="marginAccountBalance"]') // margin allocated + .should('contain.text', '0.01'); + } + + cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => { + cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty'); + }); + + cy.get('[col-id="notional"]').should('contain.text', '276,761.40348'); // Total tDAI position + cy.get('[col-id="realisedPNL"]').should('contain.text', '2.30'); // Total Realised PNL + cy.get('[col-id="unrealisedPNL"]').should('contain.text', '8.95'); // Total Unrealised PNL + + cy.get('.ag-header-row [col-id="notional"]') + .should('contain.text', 'Notional') + .realHover(); + cy.get('.ag-popup').should('contain.text', 'Mark price x open volume'); + }); + + cy.getByTestId('close-position').should('be.visible').and('have.length', 3); +} +function assertPNLColor( + pnlSelector: string, + positiveClass: string, + negativeClass: string +) { + cy.get(pnlSelector).each(($el) => { + const value = parseFloat($el.text()); + + if (value > 0) { + cy.wrap($el).invoke('attr', 'class').should('contain', positiveClass); + } else if (value < 0) { + cy.wrap($el).invoke('attr', 'class').should('contain', negativeClass); + } else if (value == 0) { + cy.wrap($el) + .invoke('attr', 'class') + .should('not.contain', negativeClass, positiveClass); + } else { + throw new Error('Unexpected value'); + } + }); +} +function visitAndClickPositions() { + cy.visit('/#/markets/market-0'); + cy.getByTestId(positions).click(); +} diff --git a/libs/positions/src/lib/positions.mock.ts b/libs/positions/src/lib/positions.mock.ts index 458747ff1..340dfa479 100644 --- a/libs/positions/src/lib/positions.mock.ts +++ b/libs/positions/src/lib/positions.mock.ts @@ -47,7 +47,7 @@ const positionFields: PositionFieldsFragment[] = [ { __typename: 'Position', realisedPNL: '230000', - openVolume: '6', + openVolume: '-6', unrealisedPNL: '895000', averageEntryPrice: '1129935', updatedAt: '2022-07-28T15:09:34.441143Z', @@ -82,7 +82,7 @@ const positionFields: PositionFieldsFragment[] = [ }, { __typename: 'Position', - realisedPNL: '230000', + realisedPNL: '-230000', openVolume: '1', unrealisedPNL: '-22519', averageEntryPrice: '84400088', @@ -98,6 +98,24 @@ const positionFields: PositionFieldsFragment[] = [ lossSocializationAmount: '0', positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED, }, + { + __typename: 'Position', + realisedPNL: '-303295252', + openVolume: '0', + unrealisedPNL: '0', + averageEntryPrice: '6126312', + updatedAt: '2022-07-28T14:53:54.725477Z', + positionStatus: PositionStatus.POSITION_STATUS_CLOSED_OUT, + lossSocializationAmount: '261', + market: { + id: 'market-3', + __typename: 'Market', + }, + party: { + id: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65', + __typename: 'Party', + }, + }, ]; const marginsFields: MarginFieldsFragment[] = [ diff --git a/package.json b/package.json index d884f7d92..0409e62c6 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "babel-loader": "8.1.0", "cypress": "^12.9.0", "cypress-mochawesome-reporter": "^3.3.0", - "cypress-real-events": "^1.7.6", + "cypress-real-events": "^1.8.1", "dotenv": "^16.0.1", "eslint": "8.15.0", "eslint-config-next": "12.2.3", diff --git a/yarn.lock b/yarn.lock index 927ff4304..240b6bd3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11882,10 +11882,10 @@ cypress-mochawesome-reporter@^3.3.0: mochawesome-merge "^4.2.1" mochawesome-report-generator "^6.2.0" -cypress-real-events@^1.7.6: - version "1.7.6" - resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.6.tgz#6f17e0b2ceea1d6dc60f6737d8f84cc517bbbb4c" - integrity sha512-yP6GnRrbm6HK5q4DH6Nnupz37nOfZu/xn1xFYqsE2o4G73giPWQOdu6375QYpwfU1cvHNCgyD2bQ2hPH9D7NMw== +cypress-real-events@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.8.1.tgz#d00c7fe93124bbe7c0f27296684838614d24a840" + integrity sha512-8fFnA8EzS3EVbAmpSEUf3A8yZCmfU3IPOSGUDVFCdE1ke1gYL1A+gvXXV6HKUbTPRuvKKt2vpaMbUwYLpDRswQ== cypress@^12.9.0: version "12.9.0" From 2e7a6a645895c746144c9d0b52e1585c22dc67f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Sat, 10 Jun 2023 04:02:23 +0200 Subject: [PATCH 36/49] chore(trading): cleanup paginated data solution, improve performance (#4036) --- .../src/generic-data-provider.spec.ts | 177 +--------------- .../src/generic-data-provider.ts | 113 +++------- libs/data-provider/src/use-data-provider.ts | 21 +- .../components/deal-ticket/deal-ticket.tsx | 2 +- libs/fills/src/index.ts | 1 - libs/fills/src/lib/Fills.graphql | 46 ++-- libs/fills/src/lib/__generated__/Fills.ts | 51 +++-- libs/fills/src/lib/fills-data-provider.ts | 160 ++++++++------ libs/fills/src/lib/fills-manager.tsx | 50 +++-- libs/fills/src/lib/use-fills-list.spec.ts | 95 --------- libs/fills/src/lib/use-fills-list.ts | 123 ----------- .../src/lib/ledger-entries-data-provider.ts | 199 ++++-------------- libs/ledger/src/lib/ledger-manager.tsx | 34 +-- .../order-data-provider.spec.ts | 103 ++++----- .../order-data-provider.ts | 125 ++++++----- .../order-list-manager.spec.tsx | 1 - .../order-list-manager/order-list-manager.tsx | 2 +- libs/trades/src/lib/Trades.graphql | 43 ++-- libs/trades/src/lib/__generated__/Trades.ts | 52 ++--- libs/trades/src/lib/trades-container.tsx | 4 +- libs/trades/src/lib/trades-data-provider.ts | 132 +++++++----- libs/trades/src/lib/trades.mock.ts | 32 ++- 22 files changed, 539 insertions(+), 1027 deletions(-) delete mode 100644 libs/fills/src/lib/use-fills-list.spec.ts delete mode 100644 libs/fills/src/lib/use-fills-list.ts diff --git a/libs/data-provider/src/generic-data-provider.spec.ts b/libs/data-provider/src/generic-data-provider.spec.ts index 8dd6bb874..3bf82bd50 100644 --- a/libs/data-provider/src/generic-data-provider.spec.ts +++ b/libs/data-provider/src/generic-data-provider.spec.ts @@ -38,7 +38,6 @@ type Data = Item[]; type QueryData = { data: Data; pageInfo?: PageInfo; - totalCount?: number; }; type CombinedData = { @@ -115,7 +114,6 @@ const paginatedSubscribe = makeDataProvider< first, append: defaultAppend, getPageInfo: (r) => r?.pageInfo ?? null, - getTotalCount: (r) => r?.totalCount, }, }); @@ -373,32 +371,10 @@ describe('data provider', () => { subscription.unsubscribe(); }); - it('fills data with nulls if pagination is enabled', async () => { - const totalCount = 1000; - const data: Item[] = new Array(first).fill(null).map((v, i) => ({ - cursor: i.toString(), - node: { - id: i.toString(), - }, - })); - const subscription = paginatedSubscribe(callback, client, variables); - await resolveQuery({ - data, - totalCount, - pageInfo: { - hasNextPage: true, - }, - }); - expect(callback.mock.calls[1][0].data?.length).toBe(totalCount); - subscription.unsubscribe(); - }); - - it('loads requested data blocks and inserts data with total count', async () => { - const totalCount = 1000; + it('loads requested data blocks', async () => { const subscription = paginatedSubscribe(callback, client, variables); await resolveQuery({ data: generateData(), - totalCount, pageInfo: { hasNextPage: true, endCursor: '100', @@ -407,168 +383,25 @@ describe('data provider', () => { // load next page subscription.load && subscription.load(); - let lastQueryArgs = + const lastQueryArgs = clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; - expect(lastQueryArgs?.variables?.pagination).toEqual({ + expect(lastQueryArgs?.variables?.['pagination']).toEqual({ after: '100', first, }); await resolveQuery({ data: generateData(100), pageInfo: { - hasNextPage: true, + hasNextPage: false, endCursor: '200', }, }); - // load page with skip - subscription.load && subscription.load(500, 600); - lastQueryArgs = - clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; - expect(lastQueryArgs?.variables?.pagination).toEqual({ - after: '200', - first, - skip: 300, - }); - await resolveQuery({ - data: generateData(500), - pageInfo: { - hasNextPage: true, - endCursor: '600', - }, - }); - - // load in the gap - subscription.load && subscription.load(400, 500); - lastQueryArgs = - clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; - expect(lastQueryArgs?.variables?.pagination).toEqual({ - after: '200', - first, - skip: 200, - }); - await resolveQuery({ - data: generateData(400), - pageInfo: { - hasNextPage: true, - endCursor: '500', - }, - }); - - // load page after last block - subscription.load && subscription.load(700, 800); - lastQueryArgs = - clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; - expect(lastQueryArgs?.variables?.pagination).toEqual({ - after: '600', - first, - skip: 100, - }); - await resolveQuery({ - data: generateData(700), - pageInfo: { - hasNextPage: true, - endCursor: '800', - }, - }); - - // load last page shorter than expected - subscription.load && subscription.load(950, 1050); - lastQueryArgs = - clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; - expect(lastQueryArgs?.variables?.pagination).toEqual({ - after: '800', - first, - skip: 150, - }); - await resolveQuery({ - data: generateData(950, 20), - pageInfo: { - hasNextPage: false, - endCursor: '970', - }, - }); - let lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1]; - expect(lastCallbackArgs[0].totalCount).toBe(970); - // load next page when pageInfo.hasNextPage === false const clientQueryCallsLength = clientQuery.mock.calls.length; subscription.load && subscription.load(); expect(clientQuery.mock.calls.length).toBe(clientQueryCallsLength); - // load last page longer than expected - subscription.load && subscription.load(960, 1000); - lastQueryArgs = - clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; - expect(lastQueryArgs?.variables?.pagination).toEqual({ - after: '960', - first, - }); - await resolveQuery({ - data: generateData(960, 40), - pageInfo: { - hasNextPage: true, - endCursor: '1000', - }, - }); - lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1]; - expect(lastCallbackArgs[0].totalCount).toBe(1000); - - subscription.unsubscribe(); - }); - - it('loads requested data blocks and inserts data without totalCount', async () => { - const totalCount = undefined; - const subscription = paginatedSubscribe(callback, client, variables); - await resolveQuery({ - data: generateData(), - totalCount, - pageInfo: { - hasNextPage: true, - endCursor: '100', - }, - }); - let lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1]; - expect(lastCallbackArgs[0].totalCount).toBe(undefined); - - // load next page - subscription.load && subscription.load(); - await resolveQuery({ - data: generateData(100), - pageInfo: { - hasNextPage: true, - endCursor: '200', - }, - }); - lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1]; - expect(lastCallbackArgs[0].totalCount).toBe(undefined); - - // load last page - subscription.load && subscription.load(); - await resolveQuery({ - data: generateData(200, 50), - pageInfo: { - hasNextPage: false, - endCursor: '250', - }, - }); - lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1]; - expect(lastCallbackArgs[0].totalCount).toBe(250); - subscription.unsubscribe(); - }); - - it('sets total count when first page has no next page', async () => { - const subscription = paginatedSubscribe(callback, client, variables); - await resolveQuery({ - data: generateData(), - pageInfo: { - hasNextPage: false, - endCursor: '100', - }, - }); - const lastCallbackArgs = - callback.mock.calls[callback.mock.calls.length - 1]; - expect(lastCallbackArgs[0].totalCount).toBe(100); subscription.unsubscribe(); }); @@ -752,7 +585,7 @@ describe('derived data provider', () => { subscription.load && subscription.load(); const lastQueryArgs = clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; - expect(lastQueryArgs?.variables?.pagination).toEqual({ + expect(lastQueryArgs?.variables?.['pagination']).toEqual({ after: '100', first, }); diff --git a/libs/data-provider/src/generic-data-provider.ts b/libs/data-provider/src/generic-data-provider.ts index dc0a2d26b..5895ba3cc 100644 --- a/libs/data-provider/src/generic-data-provider.ts +++ b/libs/data-provider/src/generic-data-provider.ts @@ -27,7 +27,6 @@ export interface UpdateCallback { loading: boolean; loaded: boolean; pageInfo: PageInfo | null; - totalCount?: number; } ): void; } @@ -40,9 +39,7 @@ export interface Reload { (forceReset?: boolean): void; } -type Pagination = Schema.Pagination & { - skip?: number; -}; +type Pagination = Schema.Pagination; export interface PageInfo { startCursor?: string; @@ -83,12 +80,8 @@ export interface Append { data: Data | null, insertionData: Data | null, insertionPageInfo: PageInfo | null, - pagination?: Pagination, - totalCount?: number - ): { - data: Data | null; - totalCount?: number; - }; + pagination?: Pagination + ): Data | null; } interface GetData { @@ -99,10 +92,6 @@ interface GetPageInfo { (queryData: QueryData): PageInfo | null; } -interface GetTotalCount { - (queryData: QueryData): number | undefined; -} - interface GetDelta { ( subscriptionData: SubscriptionData, @@ -119,44 +108,32 @@ export interface Edge extends Cursor { node: T; } -export function defaultAppend( - data: Data | null, - insertionData: Data | null, +export function defaultAppend( + data: T[] | null, + insertionData: T[] | null, insertionPageInfo: PageInfo | null, - pagination?: Pagination, - totalCount?: number + pagination?: Pagination ) { if (data && insertionData && insertionPageInfo) { if (!(data instanceof Array) || !(insertionData instanceof Array)) { throw new Error( - 'data needs to be instance of Edge[] when using pagination' + 'data needs to be instance of Array[] when using pagination' ); } if (pagination?.after) { + if (data[data.length - 1].cursor === pagination?.after) { + return [...data, ...insertionData]; + } const cursors = data.map((item) => item && item.cursor); const startIndex = cursors.lastIndexOf(pagination.after); if (startIndex !== -1) { - const start = startIndex + 1 + (pagination.skip ?? 0); - const end = start + insertionData.length; - let updatedData = [ - ...data.slice(0, start), - ...insertionData, - ...data.slice(end), - ]; - if (!insertionPageInfo.hasNextPage && end !== (totalCount ?? 0)) { - // adjust totalCount if last page is shorter or longer than expected - totalCount = end; - updatedData = updatedData.slice(0, end); - } - return { - data: updatedData, - // increase totalCount if last page is longer than expected - totalCount: totalCount && Math.max(updatedData.length, totalCount), - }; + const start = startIndex + 1; + const updatedData = [...data.slice(0, start), ...insertionData]; + return updatedData; } } } - return { data, totalCount }; + return data; } interface DataProviderParams< @@ -175,7 +152,6 @@ interface DataProviderParams< getDelta?: GetDelta; pagination?: { getPageInfo: GetPageInfo; - getTotalCount?: GetTotalCount; append: Append; first: number; }; @@ -245,7 +221,6 @@ function makeDataProviderInternal< let client: ApolloClient; let subscription: Subscription[] | undefined; let pageInfo: PageInfo | null = null; - let totalCount: number | undefined; // notify single callback about current state, delta is passes optionally only if notify was invoked onNext const notify = ( @@ -258,7 +233,6 @@ function makeDataProviderInternal< loading, loaded, pageInfo, - totalCount, ...updateData, }); }; @@ -301,59 +275,41 @@ function makeDataProviderInternal< } }); - const load = async (start?: number) => { + const load = async () => { if (!pagination) { return Promise.reject(); } + if (!pageInfo?.hasNextPage) { + return null; + } const paginationVariables: Pagination = { first: pagination.first, - after: pageInfo?.endCursor, }; - if (start !== undefined && data instanceof Array) { - if (!start) { - paginationVariables.after = undefined; - } else if (data && data[start - 1]) { - paginationVariables.after = (data[start - 1] as Cursor).cursor; - } else { - let skip = 1; - while (!data[start - 1 - skip] && skip <= start) { - skip += 1; - } - paginationVariables.skip = skip; - if (skip === start) { - paginationVariables.after = undefined; - } else { - paginationVariables.after = (data[start - 1 - skip] as Cursor).cursor; - } + if (data) { + const endCursor = (data as Cursor[])[(data as Cursor[]).length - 1] + .cursor; + if (endCursor) { + paginationVariables.after = endCursor; } - } else if (!pageInfo?.hasNextPage) { - return null; } const res = await call(paginationVariables); const insertionData = getData(res.data, variables); const insertionPageInfo = pagination.getPageInfo(res.data); - ({ data, totalCount } = pagination.append( + data = pagination.append( data, insertionData, insertionPageInfo, - paginationVariables, - totalCount - )); + paginationVariables + ); pageInfo = insertionPageInfo; - totalCount = - (pagination.getTotalCount && pagination.getTotalCount(res.data)) ?? - totalCount; notifyAll({ insertionData, isInsert: true }); return insertionData; }; const setData = (updatedData: Data | null) => { data = updatedData; - if (totalCount !== undefined && data instanceof Array) { - totalCount = data.length; - } }; const subscriptionSubscribe = () => { @@ -400,16 +356,6 @@ function makeDataProviderInternal< ); } pageInfo = pagination.getPageInfo(res.data); - if (pageInfo && !pageInfo.hasNextPage) { - totalCount = data.length; - } else { - totalCount = - pagination.getTotalCount && pagination.getTotalCount(res.data); - } - - if (data && totalCount && data.length < totalCount) { - data.push(...new Array(totalCount - data.length).fill(null)); - } } // if there was some updates received from subscription during initial query loading apply them on just received data if (update && data && updateQueue && updateQueue.length > 0) { @@ -417,9 +363,6 @@ function makeDataProviderInternal< const delta = updateQueue.shift(); if (delta) { setData(update(data, delta, reload, variables)); - if (totalCount !== undefined && data instanceof Array) { - totalCount = data.length; - } } } } @@ -590,7 +533,7 @@ const memoize = < * @param update Update function that will be executed on each onNext, it should update data base on delta, it can reload data provider * @param getData transforms received query data to format that will be stored in data provider * @param getDelta transforms delta data to format that will be stored in data provider - * @param pagination pagination related functions { getPageInfo, getTotalCount, append, first } + * @param pagination pagination related functions { getPageInfo, append, first } * @returns Subscribe subscribe function * @example * const marketMidPriceProvider = makeDataProvider({ diff --git a/libs/data-provider/src/use-data-provider.ts b/libs/data-provider/src/use-data-provider.ts index 6bba93907..55f335a41 100644 --- a/libs/data-provider/src/use-data-provider.ts +++ b/libs/data-provider/src/use-data-provider.ts @@ -12,23 +12,13 @@ export interface useDataProviderParams< Variables extends OperationVariables | undefined = undefined > { dataProvider: Subscribe; - update?: ({ - delta, - data, - totalCount, - }: { - delta?: Delta; - data: Data | null; - totalCount?: number; - }) => boolean; + update?: ({ delta, data }: { delta?: Delta; data: Data | null }) => boolean; insert?: ({ insertionData, data, - totalCount, }: { insertionData?: Data | null; data: Data | null; - totalCount?: number; }) => boolean; variables: Variables; skipUpdates?: boolean; @@ -56,7 +46,6 @@ export const useDataProvider = < }: useDataProviderParams) => { const client = useApolloClient(); const [data, setData] = useState(null); - const [totalCount, setTotalCount] = useState(); const [loading, setLoading] = useState(!skip); const [error, setError] = useState(undefined); const flushRef = useRef<(() => void) | undefined>(undefined); @@ -101,7 +90,6 @@ export const useDataProvider = < error, loading, insertionData, - totalCount, isInsert, isUpdate, loaded, @@ -116,19 +104,18 @@ export const useDataProvider = < (skipUpdatesRef.current || (!skipUpdatesRef.current && updateRef.current && - updateRef.current({ delta, data, totalCount }))) + updateRef.current({ delta, data }))) ) { return; } if ( isInsert && insertRef.current && - insertRef.current({ insertionData, data, totalCount }) + insertRef.current({ insertionData, data }) ) { return; } } - setTotalCount(totalCount); setData(data); if (!loading && !isUpdate && updateRef.current) { updateRef.current({ data }); @@ -150,7 +137,6 @@ export const useDataProvider = < useEffect(() => { setData(null); setError(undefined); - setTotalCount(undefined); if (updateRef.current) { updateRef.current({ data: null }); } @@ -184,7 +170,6 @@ export const useDataProvider = < flush, reload, load, - totalCount, }; }; diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx index d568afb57..a57c2b911 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -146,7 +146,7 @@ export const DealTicket = ({ }); const openVolume = useOpenVolume(pubKey, market.id) ?? '0'; const orders = activeOrders - ? activeOrders.map(({ node: order }) => ({ + ? activeOrders.map((order) => ({ isMarketOrder: order.type === OrderType.TYPE_MARKET, price: order.price, remaining: order.remaining, diff --git a/libs/fills/src/index.ts b/libs/fills/src/index.ts index a7260afe7..75a360902 100644 --- a/libs/fills/src/index.ts +++ b/libs/fills/src/index.ts @@ -1,4 +1,3 @@ export * from './lib/fills-container'; -export * from './lib/use-fills-list'; export * from './lib/fills-data-provider'; export * from './lib/__generated__/Fills'; diff --git a/libs/fills/src/lib/Fills.graphql b/libs/fills/src/lib/Fills.graphql index 9503e7a20..e93d2925a 100644 --- a/libs/fills/src/lib/Fills.graphql +++ b/libs/fills/src/lib/Fills.graphql @@ -48,28 +48,32 @@ query Fills($filter: TradesFilter, $pagination: Pagination) { } } +fragment FillUpdateFields on TradeUpdate { + id + marketId + buyOrder + sellOrder + buyerId + sellerId + aggressor + price + size + createdAt + type + buyerFee { + makerFee + infrastructureFee + liquidityFee + } + sellerFee { + makerFee + infrastructureFee + liquidityFee + } +} + subscription FillsEvent($filter: TradesSubscriptionFilter!) { tradesStream(filter: $filter) { - id - marketId - buyOrder - sellOrder - buyerId - sellerId - aggressor - price - size - createdAt - type - buyerFee { - makerFee - infrastructureFee - liquidityFee - } - sellerFee { - makerFee - infrastructureFee - liquidityFee - } + ...FillUpdateFields } } diff --git a/libs/fills/src/lib/__generated__/Fills.ts b/libs/fills/src/lib/__generated__/Fills.ts index 07d11fd83..f399b81fa 100644 --- a/libs/fills/src/lib/__generated__/Fills.ts +++ b/libs/fills/src/lib/__generated__/Fills.ts @@ -15,6 +15,8 @@ export type FillsQueryVariables = Types.Exact<{ export type FillsQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null }; +export type FillUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, marketId: string, buyOrder: string, sellOrder: string, buyerId: string, sellerId: string, aggressor: Types.Side, price: string, size: string, createdAt: any, type: Types.TradeType, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } }; + export type FillsEventSubscriptionVariables = Types.Exact<{ filter: Types.TradesSubscriptionFilter; }>; @@ -60,6 +62,31 @@ export const FillEdgeFragmentDoc = gql` cursor } ${FillFieldsFragmentDoc}`; +export const FillUpdateFieldsFragmentDoc = gql` + fragment FillUpdateFields on TradeUpdate { + id + marketId + buyOrder + sellOrder + buyerId + sellerId + aggressor + price + size + createdAt + type + buyerFee { + makerFee + infrastructureFee + liquidityFee + } + sellerFee { + makerFee + infrastructureFee + liquidityFee + } +} + `; export const FillsDocument = gql` query Fills($filter: TradesFilter, $pagination: Pagination) { trades(filter: $filter, pagination: $pagination) { @@ -107,30 +134,10 @@ export type FillsQueryResult = Apollo.QueryResult { - return produce(data, (draft) => { - orderBy(delta, 'createdAt').forEach((node) => { - if (draft === null) { - return; - } - const index = draft.findIndex((edge) => edge?.node.id === node.id); - if (index !== -1) { - if (draft[index]?.node) { - Object.assign(draft[index]?.node as FillFieldsFragment, node); - } - } else { - const firstNode = draft[0]?.node; - if ( - (firstNode && node.createdAt >= firstNode.createdAt) || - !firstNode - ) { - const { buyerId, sellerId, marketId, ...trade } = node; - draft.unshift({ - node: { - ...trade, - __typename: 'Trade', - market: { - __typename: 'Market', - id: marketId, - }, - buyer: { id: buyerId, __typename: 'Party' }, - seller: { id: buyerId, __typename: 'Party' }, - }, - cursor: '', - __typename: 'TradeEdge', - }); - } - } - }); - }); -}; - export type Trade = Omit & { market?: Market; isLastPlaceholder?: boolean; }; -export type TradeEdge = Edge; -const getData = (responseData: FillsQuery | null): FillEdgeFragment[] => - responseData?.trades?.edges || []; +const getData = ( + responseData: FillsQuery | null +): (FillFieldsFragment & Cursor)[] => + responseData?.trades?.edges.map((edge) => ({ + ...edge.node, + cursor: edge.cursor, + })) || []; const getPageInfo = (responseData: FillsQuery | null): PageInfo | null => responseData?.trades?.pageInfo || null; @@ -76,16 +36,65 @@ const getPageInfo = (responseData: FillsQuery | null): PageInfo | null => const getDelta = (subscriptionData: FillsEventSubscription) => subscriptionData.tradesStream || []; +const mapFillUpdateToFill = ( + fillUpdate: FillUpdateFieldsFragment +): FillFieldsFragment => { + const { buyerId, sellerId, marketId, ...fill } = fillUpdate; + return { + ...fill, + __typename: 'Trade', + market: { + __typename: 'Market', + id: marketId, + }, + buyer: { id: buyerId, __typename: 'Party' }, + seller: { id: buyerId, __typename: 'Party' }, + }; +}; + +const mapFillUpdateToFillWithMarket = + (markets: Record) => + (fillUpdate: FillUpdateFieldsFragment): Trade => { + const { market, ...fill } = mapFillUpdateToFill(fillUpdate); + return { + ...fill, + market: markets[market.id], + }; + }; + +const update = & Cursor>( + data: T[] | null, + delta: ReturnType, + variables: FillsQueryVariables, + mapDeltaToData: (delta: FillUpdateFieldsFragment) => T +): T[] => { + const updatedData = data ? [...data] : ([] as T[]); + orderBy(delta, 'createdAt', 'desc').forEach((fillUpdate) => { + const index = data?.findIndex((fill) => fill.id === fillUpdate.id) ?? -1; + if (index !== -1) { + updatedData[index] = { + ...updatedData[index], + ...mapDeltaToData(fillUpdate), + }; + } else if (!data?.length || fillUpdate.createdAt >= data[0].createdAt) { + updatedData.unshift(mapDeltaToData(fillUpdate)); + } + }); + return updatedData; +}; + export const fillsProvider = makeDataProvider< Parameters['0'], ReturnType, Parameters['0'], ReturnType, - FillsQueryVariables + FillsQueryVariables, + FillsEventSubscriptionVariables >({ query: FillsDocument, subscriptionQuery: FillsEventDocument, - update, + update: (data, delta, reload, variables) => + update(data, delta, variables, mapFillUpdateToFill), getData, getDelta, pagination: { @@ -93,30 +102,41 @@ export const fillsProvider = makeDataProvider< append, first: 100, }, + getSubscriptionVariables: ({ filter }) => { + const variables: FillsEventSubscriptionVariables = { filter: {} }; + if (filter) { + variables.filter = { + partyIds: filter.partyIds, + marketIds: filter.marketIds, + }; + } + return variables; + }, }); export const fillsWithMarketProvider = makeDerivedDataProvider< - (TradeEdge | null)[], Trade[], + never, FillsQueryVariables >( [ fillsProvider, - (callback, client) => marketsProvider(callback, client, undefined), + (callback, client) => marketsMapProvider(callback, client, undefined), ], - (partsData): (TradeEdge | null)[] => - (partsData[0] as ReturnType)?.map( - (edge) => - edge && { - cursor: edge.cursor, - node: { - ...edge.node, - market: (partsData[1] as Market[]).find( - (market) => market.id === edge.node.market.id - ), - }, - } - ) || null, - combineDelta['0']>, - combineInsertionData + (partsData, variables, prevData, parts): Trade[] | null => { + if (prevData && parts[0].isUpdate) { + return update( + prevData, + parts[0].delta as ReturnType, + variables, + mapFillUpdateToFillWithMarket(partsData[1] as Record) + ); + } + return ((partsData[0] as ReturnType) || []).map( + (trade) => ({ + ...trade, + market: (partsData[1] as Record)[trade.market.id], + }) + ); + } ); diff --git a/libs/fills/src/lib/fills-manager.tsx b/libs/fills/src/lib/fills-manager.tsx index 700ec9550..c69861135 100644 --- a/libs/fills/src/lib/fills-manager.tsx +++ b/libs/fills/src/lib/fills-manager.tsx @@ -1,10 +1,11 @@ -import compact from 'lodash/compact'; import type { AgGridReact } from 'ag-grid-react'; import { useRef } from 'react'; import { t } from '@vegaprotocol/i18n'; import { FillsTable } from './fills-table'; -import { useFillsList } from './use-fills-list'; import { useBottomPlaceholder } from '@vegaprotocol/datagrid'; +import { useDataProvider } from '@vegaprotocol/data-provider'; +import type * as Schema from '@vegaprotocol/types'; +import { fillsWithMarketProvider } from './fills-data-provider'; interface FillsManagerProps { partyId: string; @@ -20,31 +21,36 @@ export const FillsManager = ({ storeKey, }: FillsManagerProps) => { const gridRef = useRef(null); - const scrolledToTop = useRef(true); - const { data, error } = useFillsList({ - partyId, - marketId, - gridRef, - scrolledToTop, + const filter: Schema.TradesFilter | Schema.TradesSubscriptionFilter = { + partyIds: [partyId], + }; + if (marketId) { + filter.marketIds = [marketId]; + } + const { data, error } = useDataProvider({ + dataProvider: fillsWithMarketProvider, + update: ({ data }) => { + if (data?.length && gridRef.current?.api) { + gridRef.current?.api.setRowData(data); + return true; + } + return false; + }, + variables: { filter }, }); - const bottomPlaceholderProps = useBottomPlaceholder({ gridRef, }); - const fills = compact(data).map((e) => e.node); - return ( -
- -
+ ); }; diff --git a/libs/fills/src/lib/use-fills-list.spec.ts b/libs/fills/src/lib/use-fills-list.spec.ts deleted file mode 100644 index dfd0935de..000000000 --- a/libs/fills/src/lib/use-fills-list.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { AgGridReact } from 'ag-grid-react'; -import { MockedProvider } from '@apollo/client/testing'; -import { renderHook } from '@testing-library/react'; -import { useFillsList } from './use-fills-list'; -import type { TradeEdge } from './fills-data-provider'; - -let mockData = null; -let mockDataProviderData = { - data: mockData as (TradeEdge | null)[] | null, - error: undefined, - loading: true, -}; - -let updateMock: jest.Mock; -const mockDataProvider = jest.fn((args) => { - updateMock = args.update; - return mockDataProviderData; -}); -jest.mock('@vegaprotocol/data-provider', () => ({ - ...jest.requireActual('@vegaprotocol/data-provider'), - useDataProvider: jest.fn((args) => mockDataProvider(args)), -})); - -describe('useFillsList Hook', () => { - const mockRefreshAgGridApi = jest.fn(); - const partyId = 'partyId'; - const gridRef = { - current: { - api: { - refreshInfiniteCache: mockRefreshAgGridApi, - getModel: () => ({ getType: () => 'infinite' }), - }, - } as unknown as AgGridReact, - }; - const scrolledToTop = { - current: false, - }; - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should return proper dataProvider results', () => { - const { result } = renderHook( - () => useFillsList({ partyId, gridRef, scrolledToTop }), - { - wrapper: MockedProvider, - } - ); - expect(result.current).toMatchObject({ - data: null, - error: undefined, - loading: true, - addNewRows: expect.any(Function), - getRows: expect.any(Function), - }); - }); - - it('return proper mocked results', () => { - mockData = [ - { - node: { - id: 'data_id_1', - }, - } as unknown as TradeEdge, - { - node: { - id: 'data_id_2', - }, - } as unknown as TradeEdge, - ]; - mockDataProviderData = { - ...mockDataProviderData, - data: mockData, - loading: false, - }; - const { result } = renderHook( - () => useFillsList({ partyId, gridRef, scrolledToTop }), - { - wrapper: MockedProvider, - } - ); - expect(result.current).toMatchObject({ - data: mockData, - error: undefined, - loading: false, - addNewRows: expect.any(Function), - getRows: expect.any(Function), - }); - updateMock({ data: mockData }); - expect(mockRefreshAgGridApi).not.toHaveBeenCalled(); - updateMock({ data: mockData }); - expect(mockRefreshAgGridApi).toHaveBeenCalled(); - }); -}); diff --git a/libs/fills/src/lib/use-fills-list.ts b/libs/fills/src/lib/use-fills-list.ts deleted file mode 100644 index 2a10ab2dd..000000000 --- a/libs/fills/src/lib/use-fills-list.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { RefObject } from 'react'; -import type { AgGridReact } from 'ag-grid-react'; -import { useCallback, useRef } from 'react'; -import { makeInfiniteScrollGetRows } from '@vegaprotocol/data-provider'; -import type * as Types from '@vegaprotocol/types'; -import { updateGridData } from '@vegaprotocol/datagrid'; -import { useDataProvider } from '@vegaprotocol/data-provider'; -import type { Trade, TradeEdge } from './fills-data-provider'; -import { fillsWithMarketProvider } from './fills-data-provider'; - -interface Props { - partyId: string; - marketId?: string; - gridRef: RefObject; - scrolledToTop: RefObject; -} - -export const useFillsList = ({ - partyId, - marketId, - gridRef, - scrolledToTop, -}: Props) => { - const dataRef = useRef<(TradeEdge | null)[] | null>(null); - const totalCountRef = useRef(undefined); - const newRows = useRef(0); - const placeholderAdded = useRef(-1); - - const makeBottomPlaceholders = useCallback((trade?: Trade) => { - if (!trade) { - if (placeholderAdded.current >= 0) { - dataRef.current?.splice(placeholderAdded.current, 1); - } - placeholderAdded.current = -1; - } else if (placeholderAdded.current === -1) { - dataRef.current?.push({ - node: { ...trade, id: `${trade?.id}-1`, isLastPlaceholder: true }, - }); - placeholderAdded.current = (dataRef.current?.length || 0) - 1; - } - }, []); - - const addNewRows = useCallback(() => { - if (newRows.current === 0) { - return; - } - if (totalCountRef.current !== undefined) { - totalCountRef.current += newRows.current; - } - newRows.current = 0; - gridRef.current?.api?.refreshInfiniteCache(); - }, [gridRef]); - - const update = useCallback( - ({ - data, - delta, - }: { - data: (TradeEdge | null)[] | null; - delta?: Trade[]; - }) => { - if (dataRef.current?.length) { - if (!scrolledToTop.current) { - const createdAt = dataRef.current?.[0]?.node.createdAt; - if (createdAt) { - newRows.current += (delta || []).filter( - (trade) => trade.createdAt > createdAt - ).length; - } - } - return updateGridData(dataRef, data, gridRef); - } - dataRef.current = data; - return false; - }, - [gridRef, scrolledToTop] - ); - - const insert = useCallback( - ({ - data, - totalCount, - }: { - data: (TradeEdge | null)[] | null; - totalCount?: number; - }) => { - totalCountRef.current = totalCount; - return updateGridData(dataRef, data, gridRef); - }, - [gridRef] - ); - - const filter: Types.TradesFilter & Types.TradesSubscriptionFilter = { - partyIds: [partyId], - }; - if (marketId) { - filter.marketIds = [marketId]; - } - - const { data, error, loading, load, totalCount, reload } = useDataProvider({ - dataProvider: fillsWithMarketProvider, - update, - insert, - variables: { filter }, - }); - totalCountRef.current = totalCount; - - const getRows = makeInfiniteScrollGetRows( - dataRef, - totalCountRef, - load, - newRows - ); - return { - data, - error, - loading, - addNewRows, - getRows, - reload, - makeBottomPlaceholders, - }; -}; diff --git a/libs/ledger/src/lib/ledger-entries-data-provider.ts b/libs/ledger/src/lib/ledger-entries-data-provider.ts index 051c6efcc..2eeaa07ec 100644 --- a/libs/ledger/src/lib/ledger-entries-data-provider.ts +++ b/libs/ledger/src/lib/ledger-entries-data-provider.ts @@ -1,22 +1,12 @@ import type { Asset } from '@vegaprotocol/assets'; -import { assetsProvider } from '@vegaprotocol/assets'; +import { assetsMapProvider } from '@vegaprotocol/assets'; import type { Market } from '@vegaprotocol/markets'; -import { marketsProvider } from '@vegaprotocol/markets'; -import { makeInfiniteScrollGetRows } from '@vegaprotocol/data-provider'; -import { updateGridData } from '@vegaprotocol/datagrid'; +import { marketsMapProvider } from '@vegaprotocol/markets'; import { makeDataProvider, makeDerivedDataProvider, - useDataProvider, } from '@vegaprotocol/data-provider'; -import type * as Schema from '@vegaprotocol/types'; -import type { AgGridReact } from 'ag-grid-react'; -import produce from 'immer'; -import orderBy from 'lodash/orderBy'; -import uniqBy from 'lodash/uniqBy'; -import type { RefObject } from 'react'; -import { useCallback, useMemo, useRef } from 'react'; -import type { Filter } from './ledger-manager'; + import type { LedgerEntriesQuery, LedgerEntriesQueryVariables, @@ -30,171 +20,58 @@ export type LedgerEntry = LedgerEntryFragment & { marketReceiver: Market | null | undefined; }; -export type AggregatedLedgerEntriesEdge = Schema.AggregatedLedgerEntriesEdge; -export type AggregatedLedgerEntriesNode = Omit< - AggregatedLedgerEntriesEdge, - 'node' -> & { - node: LedgerEntry; -}; +type Edge = LedgerEntriesQuery['ledgerEntries']['edges'][number]; + +const isLedgerEntryEdge = (entry: Edge): entry is NonNullable => + entry !== null; const getData = (responseData: LedgerEntriesQuery | null) => { - return responseData?.ledgerEntries?.edges || []; + return ( + responseData?.ledgerEntries?.edges + .filter(isLedgerEntryEdge) + .map((edge) => edge.node) || [] + ); }; -export const update = ( - data: ReturnType | null, - delta: ReturnType, - reload: () => void, - variables: LedgerEntriesQueryVariables -) => { - if (!data) { - return data; - } - return produce(data, (draft) => { - // A single update can contain the same order with multiple updates, so we need to find - // the latest version of the order and only update using that - const incoming = uniqBy( - orderBy(delta, (entry) => entry?.node.vegaTime, 'desc'), - 'id' - ); - - // Add or update incoming orders - incoming.reverse().forEach((node) => { - const index = draft.findIndex( - (edge) => edge?.node.vegaTime === node?.node.vegaTime - ); - const newer = - draft.length === 0 || node?.node.vegaTime >= draft[0]?.node.vegaTime; - let doesFilterPass = true; - if ( - doesFilterPass && - variables?.dateRange?.start && - new Date(node?.node.vegaTime) <= new Date(variables?.dateRange?.start) - ) { - doesFilterPass = false; - } - if ( - doesFilterPass && - variables?.dateRange?.end && - new Date(node?.node.vegaTime) >= new Date(variables?.dateRange?.end) - ) { - doesFilterPass = false; - } - if (index !== -1) { - if (doesFilterPass) { - // Object.assign(draft[index]?.node, node?.node); - if (newer) { - draft.unshift(...draft.splice(index, 1)); - } - } else { - draft.splice(index, 1); - } - } else if (newer && doesFilterPass) { - draft.unshift(node); - } - }); - }); -}; - -const ledgerEntriesOnlyProvider = makeDataProvider({ +const ledgerEntriesOnlyProvider = makeDataProvider< + LedgerEntriesQuery, + ReturnType, + never, + never, + LedgerEntriesQueryVariables +>({ query: LedgerEntriesDocument, getData, - getDelta: getData, - update, additionalContext: { isEnlargedTimeout: true, }, }); export const ledgerEntriesProvider = makeDerivedDataProvider< - AggregatedLedgerEntriesNode[], - AggregatedLedgerEntriesNode[], + LedgerEntry[], + never, LedgerEntriesQueryVariables >( [ ledgerEntriesOnlyProvider, - (callback, client) => assetsProvider(callback, client, undefined), - (callback, client) => marketsProvider(callback, client, undefined), + (callback, client) => assetsMapProvider(callback, client, undefined), + (callback, client) => marketsMapProvider(callback, client, undefined), ], - ([entries, assets, markets]) => { - return entries.map((edge: AggregatedLedgerEntriesEdge) => { - const entry = edge.node; - const asset = assets.find((asset: Asset) => asset.id === entry.assetId); - const marketSender = markets.find( - (market: Market) => market.id === entry.fromAccountMarketId - ); - const marketReceiver = markets.find( - (market: Market) => market.id === entry.toAccountMarketId - ); - const cursor = edge?.cursor; - return { - node: { ...entry, asset, marketSender, marketReceiver }, - cursor, - }; + (partsData) => { + const entries = partsData[0] as ReturnType; + const assets = partsData[1] as Record; + const markets = partsData[1] as Record; + return entries.map((entry) => { + const asset = entry.assetId + ? (assets as Record)[entry.assetId] + : null; + const marketSender = entry.fromAccountMarketId + ? markets[entry.fromAccountMarketId] + : null; + const marketReceiver = entry.toAccountMarketId + ? markets[entry.toAccountMarketId] + : null; + return { ...entry, asset, marketSender, marketReceiver }; }); } ); - -interface Props { - partyId: string; - filter?: Filter; - gridRef: RefObject; -} - -export const useLedgerEntriesDataProvider = ({ - partyId, - filter, - gridRef, -}: Props) => { - const dataRef = useRef(null); - const totalCountRef = useRef(); - - const variables = useMemo( - () => ({ - partyId, - dateRange: filter?.vegaTime?.value, - pagination: { - first: 5000, - }, - }), - [partyId, filter?.vegaTime?.value] - ); - - const update = useCallback( - ({ data }: { data: AggregatedLedgerEntriesEdge[] | null }) => { - return updateGridData(dataRef, data, gridRef); - }, - [gridRef] - ); - - const insert = useCallback( - ({ - data, - totalCount, - }: { - data: AggregatedLedgerEntriesEdge[] | null; - totalCount?: number; - }) => { - totalCountRef.current = totalCount; - return updateGridData(dataRef, data, gridRef); - }, - [gridRef] - ); - - const { data, error, loading, load, totalCount, reload } = useDataProvider({ - dataProvider: ledgerEntriesProvider, - update, - insert, - variables, - skip: !variables.partyId, - }); - totalCountRef.current = totalCount; - - const getRows = makeInfiniteScrollGetRows( - dataRef, - totalCountRef, - load - ); - return { loading, error, data, getRows, reload }; -}; diff --git a/libs/ledger/src/lib/ledger-manager.tsx b/libs/ledger/src/lib/ledger-manager.tsx index 5da40809f..5fd8320c2 100644 --- a/libs/ledger/src/lib/ledger-manager.tsx +++ b/libs/ledger/src/lib/ledger-manager.tsx @@ -2,10 +2,12 @@ import { t } from '@vegaprotocol/i18n'; import type * as Schema from '@vegaprotocol/types'; import type { FilterChangedEvent } from 'ag-grid-community'; import type { AgGridReact } from 'ag-grid-react'; -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useRef, useState, useMemo } from 'react'; import { subDays, formatRFC3339 } from 'date-fns'; -import { useLedgerEntriesDataProvider } from './ledger-entries-data-provider'; +import { ledgerEntriesProvider } from './ledger-entries-data-provider'; +import type { LedgerEntriesQueryVariables } from './__generated__/LedgerEntries'; import { LedgerTable } from './ledger-table'; +import { useDataProvider } from '@vegaprotocol/data-provider'; import type * as Types from '@vegaprotocol/types'; import { LedgerExportLink } from './ledger-export-link'; @@ -26,10 +28,21 @@ export const LedgerManager = ({ partyId }: { partyId: string }) => { const gridRef = useRef(null); const [filter, setFilter] = useState(defaultFilter); - const { data, error } = useLedgerEntriesDataProvider({ - partyId, - filter, - gridRef, + const variables = useMemo( + () => ({ + partyId, + dateRange: filter?.vegaTime?.value, + pagination: { + first: 5000, + }, + }), + [partyId, filter?.vegaTime?.value] + ); + + const { data, error } = useDataProvider({ + dataProvider: ledgerEntriesProvider, + variables, + skip: !variables.partyId, }); const onFilterChanged = useCallback((event: FilterChangedEvent) => { @@ -37,20 +50,15 @@ export const LedgerManager = ({ partyId }: { partyId: string }) => { setFilter(updatedFilter); }, []); - // allow passing undefined to grid so that loading state is shown - const extractedData = data?.map((item) => item.node); - return (
- {extractedData && ( - - )} + {data && }
); }; diff --git a/libs/orders/src/lib/components/order-data-provider/order-data-provider.spec.ts b/libs/orders/src/lib/components/order-data-provider/order-data-provider.spec.ts index dd10b1d3f..b47cb5a1e 100644 --- a/libs/orders/src/lib/components/order-data-provider/order-data-provider.spec.ts +++ b/libs/orders/src/lib/components/order-data-provider/order-data-provider.spec.ts @@ -1,23 +1,23 @@ -import { update } from './order-data-provider'; +import { + update, + mapOrderUpdateToOrder, + filterOrderUpdates, +} from './order-data-provider'; import type { OrderUpdateFieldsFragment, OrderFieldsFragment } from '../'; -import type { Edge } from '@vegaprotocol/data-provider'; describe('order data provider', () => { it('puts incoming data in proper place', () => { const data = [ { - node: { - id: '2', - createdAt: new Date('2022-01-29').toISOString(), - }, + id: '2', + createdAt: new Date('2022-01-29').toISOString(), }, + { - node: { - id: '1', - createdAt: new Date('2022-01-28').toISOString(), - }, + id: '1', + createdAt: new Date('2022-01-28').toISOString(), }, - ] as Edge[]; + ] as OrderFieldsFragment[]; const delta = [ // this one should be dropped because id don't exits and it's older than newest @@ -52,39 +52,41 @@ describe('order data provider', () => { createdAt: new Date('2022-02-05').toISOString(), }, ] as OrderUpdateFieldsFragment[]; - const updatedData = update(data, delta, () => null, { partyId: '0x123' }); + const updatedData = update( + data, + filterOrderUpdates(delta), + { partyId: '0x123' }, + mapOrderUpdateToOrder + ); + expect(updatedData?.findIndex((node) => node.id === delta[0].id)).toEqual( + -1 + ); + expect(updatedData && updatedData[3].id).toEqual(delta[2].id); + expect(updatedData && updatedData[3].updatedAt).toEqual(delta[2].updatedAt); + expect(updatedData && updatedData[0].id).toEqual(delta[5].id); + expect(updatedData && updatedData[1].id).toEqual(delta[3].id); + expect(updatedData && updatedData[2].id).toEqual(delta[4].id); + expect(updatedData && updatedData[2].updatedAt).toEqual(delta[4].updatedAt); expect( - updatedData?.findIndex((edge) => edge.node.id === delta[0].id) - ).toEqual(-1); - expect(updatedData && updatedData[3].node.id).toEqual(delta[2].id); - expect(updatedData && updatedData[3].node.updatedAt).toEqual( - delta[2].updatedAt - ); - expect(updatedData && updatedData[0].node.id).toEqual(delta[5].id); - expect(updatedData && updatedData[1].node.id).toEqual(delta[3].id); - expect(updatedData && updatedData[2].node.id).toEqual(delta[4].id); - expect(updatedData && updatedData[2].node.updatedAt).toEqual( - delta[4].updatedAt - ); - expect(update([], delta, () => null, { partyId: '0x123' })?.length).toEqual( - 5 - ); + update( + [], + filterOrderUpdates(delta), + { partyId: '0x123' }, + mapOrderUpdateToOrder + )?.length + ).toEqual(5); }); it('add only data matching date range filter', () => { const data = [ { - node: { - id: '1', - createdAt: new Date('2022-01-29').toISOString(), - }, + id: '1', + createdAt: new Date('2022-01-29').toISOString(), }, { - node: { - id: '2', - createdAt: new Date('2022-01-30').toISOString(), - }, + id: '2', + createdAt: new Date('2022-01-30').toISOString(), }, - ] as Edge[]; + ] as OrderFieldsFragment[]; const delta = [ // this one should be ignored because it does not match date range @@ -105,22 +107,23 @@ describe('order data provider', () => { }, ] as OrderUpdateFieldsFragment[]; - const updatedData = update(data, delta, () => null, { - partyId: '0x123', - filter: { - dateRange: { end: new Date('2022-02-01').toISOString() }, + const updatedData = update( + data, + filterOrderUpdates(delta), + { + partyId: '0x123', + filter: { + dateRange: { end: new Date('2022-02-01').toISOString() }, + }, }, - }); - expect( - updatedData?.findIndex((edge) => edge.node.id === delta[0].id) - ).toEqual(-1); - expect(updatedData && updatedData[0].node.id).toEqual(delta[2].id); - expect(updatedData && updatedData[0].node.updatedAt).toEqual( - delta[2].updatedAt + mapOrderUpdateToOrder ); - expect(updatedData && updatedData[2].node.id).toEqual(delta[1].id); - expect(updatedData && updatedData[2].node.updatedAt).toEqual( - delta[1].updatedAt + expect(updatedData?.findIndex((node) => node.id === delta[0].id)).toEqual( + -1 ); + expect(updatedData && updatedData[0].id).toEqual(delta[2].id); + expect(updatedData && updatedData[0].updatedAt).toEqual(delta[2].updatedAt); + expect(updatedData && updatedData[2].id).toEqual(delta[1].id); + expect(updatedData && updatedData[2].updatedAt).toEqual(delta[1].updatedAt); }); }); diff --git a/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts b/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts index c2355b86c..97b379323 100644 --- a/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts +++ b/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts @@ -6,8 +6,8 @@ import { defaultAppend as append, } from '@vegaprotocol/data-provider'; import type { Market } from '@vegaprotocol/markets'; -import { marketsProvider } from '@vegaprotocol/markets'; -import type { PageInfo, Edge } from '@vegaprotocol/data-provider'; +import { marketsMapProvider } from '@vegaprotocol/markets'; +import type { PageInfo, Edge, Cursor } from '@vegaprotocol/data-provider'; import { OrderStatus } from '@vegaprotocol/types'; import type { OrderFieldsFragment, @@ -15,6 +15,7 @@ import type { OrdersQuery, OrdersUpdateSubscription, OrdersQueryVariables, + OrdersUpdateSubscriptionVariables, } from './__generated__/Orders'; import { OrdersDocument, OrdersUpdateDocument } from './__generated__/Orders'; import type { ApolloClient } from '@apollo/client'; @@ -37,18 +38,21 @@ const orderMatchFilters = ( if (!order) { return true; } + if ( variables?.filter?.status && !(order.status && variables.filter.status.includes(order.status)) ) { return false; } + if ( variables?.filter?.liveOnly && !(order.status && liveOnlyOrderStatuses.includes(order.status)) ) { return false; } + if ( variables?.filter?.types && !(order.type && variables.filter.types.includes(order.type)) @@ -76,10 +80,11 @@ const orderMatchFilters = ( ) { return false; } + return true; }; -const mapOrderUpdateToOrder = ( +export const mapOrderUpdateToOrder = ( orderUpdate: OrderUpdateFieldsFragment ): OrderFieldsFragment => { const { marketId, liquidityProvisionId, ...order } = orderUpdate; @@ -101,10 +106,36 @@ const mapOrderUpdateToOrder = ( }; }; +const mapOrderUpdateToOrderWithMarket = + (markets: Record) => + (orderUpdate: OrderUpdateFieldsFragment): Order => { + const { market, ...order } = mapOrderUpdateToOrder(orderUpdate); + return { + ...order, + market: markets[market.id], + }; + }; + const getData = ( responseData: OrdersQuery | null -): Edge[] => - responseData?.party?.ordersConnection?.edges || []; +): (OrderFieldsFragment & Cursor)[] => + responseData?.party?.ordersConnection?.edges?.map< + OrderFieldsFragment & Cursor + >((edge) => ({ ...edge.node, cursor: edge.cursor })) || []; + +export const filterOrderUpdates = ( + orders: OrdersUpdateSubscription['orders'] +) => { + // A single update can contain the same order with multiple updates, so we need to find + // the latest version of the order and only update using that + return orderBy( + uniqBy( + orderBy(orders, (order) => order.updatedAt || order.createdAt, 'desc'), + 'id' + ), + 'createdAt' + ); +}; const getDelta = ( subscriptionData: OrdersUpdateSubscription, @@ -114,49 +145,32 @@ const getDelta = ( if (!subscriptionData.orders) { return []; } - return subscriptionData.orders; + return filterOrderUpdates(subscriptionData.orders); }; -export const update = ( - data: ReturnType | null, +export const update = & Cursor>( + data: T[] | null, delta: ReturnType, - reload: () => void, - variables?: OrdersQueryVariables -) => { - if (!data) { - return data; - } - // A single update can contain the same order with multiple updates, so we need to find - // the latest version of the order and only update using that - const incoming = orderBy( - uniqBy( - orderBy(delta, (order) => order.updatedAt || order.createdAt, 'desc'), - 'id' - ), - 'createdAt' - ); - - const updatedData = [...data]; - incoming.forEach((orderUpdate) => { - const index = data.findIndex((edge) => edge.node.id === orderUpdate.id); - const newer = - data.length === 0 || orderUpdate.createdAt >= data[0].node.createdAt; + variables: OrdersQueryVariables, + mapDeltaToData: (delta: OrderUpdateFieldsFragment) => T +): T[] => { + const updatedData = data ? [...data] : ([] as T[]); + delta.forEach((orderUpdate) => { + const index = data?.findIndex((order) => order.id === orderUpdate.id) ?? -1; + const newer = !data?.length || orderUpdate.createdAt >= data[0].createdAt; const doesFilterPass = !variables || orderMatchFilters(orderUpdate, variables); if (index !== -1) { if (doesFilterPass) { updatedData[index] = { ...updatedData[index], - node: mapOrderUpdateToOrder(orderUpdate), + ...mapDeltaToData(orderUpdate), }; } else { updatedData.splice(index, 1); } } else if (newer && doesFilterPass) { - updatedData.unshift({ - node: mapOrderUpdateToOrder(orderUpdate), - cursor: '', - }); + updatedData.unshift(mapDeltaToData(orderUpdate)); } }); return updatedData; @@ -170,11 +184,13 @@ export const ordersProvider = makeDataProvider< ReturnType, OrdersUpdateSubscription, ReturnType, - OrdersQueryVariables + OrdersQueryVariables, + OrdersUpdateSubscriptionVariables >({ query: OrdersDocument, subscriptionQuery: OrdersUpdateDocument, - update, + update: (data, delta, reload, variables) => + update(data, delta, variables, mapOrderUpdateToOrder), getData, getDelta, pagination: { @@ -185,6 +201,10 @@ export const ordersProvider = makeDataProvider< resetDelay: 1000, additionalContext: { isEnlargedTimeout: true }, fetchPolicy: 'no-cache', + getSubscriptionVariables: ({ partyId, marketIds }) => ({ + partyId, + marketIds, + }), }); export const activeOrdersProvider = makeDerivedDataProvider< @@ -208,27 +228,36 @@ export const activeOrdersProvider = makeDerivedDataProvider< } const orders = partsData[0] as ReturnType; return variables.marketId - ? orders.filter((edge) => variables.marketId === edge.node.market.id) + ? orders.filter((order) => variables.marketId === order.market.id) : orders; } ); export const ordersWithMarketProvider = makeDerivedDataProvider< - (Order | null)[], - Order[], + (Order & Cursor)[], + never, OrdersQueryVariables >( [ ordersProvider, - (callback, client) => marketsProvider(callback, client, undefined), + (callback, client) => marketsMapProvider(callback, client, undefined), ], - (partsData): Order[] => - ((partsData[0] as ReturnType) || []).map((edge) => ({ - ...edge.node, - market: (partsData[1] as Market[]).find( - (market) => market.id === edge.node.market.id - ), - })) + (partsData, variables, prevData, parts): Order[] => { + if (prevData && parts[0].isUpdate) { + return update( + prevData, + parts[0].delta, + variables, + mapOrderUpdateToOrderWithMarket(partsData[1] as Record) + ); + } + return ((partsData[0] as ReturnType) || []).map( + (order) => ({ + ...order, + market: (partsData[1] as Record)[order.market.id], + }) + ); + } ); export const hasActiveOrderProvider = makeDerivedDataProvider< @@ -244,7 +273,7 @@ export const hasAmendableOrderProvider = makeDerivedDataProvider< >([activeOrdersProvider], (parts) => { const activeOrders = parts[0] as ReturnType; const hasAmendableOrder = activeOrders.some( - (edge) => !(edge.node.liquidityProvision || edge.node.peggedOrder) + (order) => !(order.liquidityProvision || order.peggedOrder) ); return hasAmendableOrder; }); diff --git a/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx b/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx index 666070b2a..fe8f1822a 100644 --- a/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx +++ b/libs/orders/src/lib/components/order-list-manager/order-list-manager.spec.tsx @@ -32,7 +32,6 @@ describe('OrderListManager', () => { flush: jest.fn(), reload: jest.fn(), load: jest.fn(), - totalCount: undefined, }); await act(async () => { render(generateJsx()); diff --git a/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx b/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx index c4833c828..0d897ded5 100644 --- a/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx +++ b/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx @@ -87,7 +87,7 @@ export const OrderListManager = ({ gridRef.current.api.setRowData(data); return true; } - return true; + return false; }, }); diff --git a/libs/trades/src/lib/Trades.graphql b/libs/trades/src/lib/Trades.graphql index 232939b9d..d17f84154 100644 --- a/libs/trades/src/lib/Trades.graphql +++ b/libs/trades/src/lib/Trades.graphql @@ -10,32 +10,33 @@ fragment TradeFields on Trade { } query Trades($marketId: ID!, $pagination: Pagination) { - market(id: $marketId) { - id - tradesConnection(pagination: $pagination) { - edges { - node { - ...TradeFields - } - cursor - } - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage + trades(filter: { marketIds: [$marketId] }, pagination: $pagination) { + edges { + node { + ...TradeFields } + cursor + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage } } } +fragment TradeUpdateFields on TradeUpdate { + id + price + size + createdAt + marketId + aggressor +} + subscription TradesUpdate($marketId: ID!) { - trades(marketId: $marketId) { - id - price - size - createdAt - marketId - aggressor + tradesStream(filter: { marketIds: [$marketId] }) { + ...TradeUpdateFields } } diff --git a/libs/trades/src/lib/__generated__/Trades.ts b/libs/trades/src/lib/__generated__/Trades.ts index 2aa1d880b..d47d6097b 100644 --- a/libs/trades/src/lib/__generated__/Trades.ts +++ b/libs/trades/src/lib/__generated__/Trades.ts @@ -11,14 +11,16 @@ export type TradesQueryVariables = Types.Exact<{ }>; -export type TradesQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, tradesConnection?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, market: { __typename?: 'Market', id: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null } | null }; +export type TradesQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, market: { __typename?: 'Market', id: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null }; + +export type TradeUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side }; export type TradesUpdateSubscriptionVariables = Types.Exact<{ marketId: Types.Scalars['ID']; }>; -export type TradesUpdateSubscription = { __typename?: 'Subscription', trades?: Array<{ __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side }> | null }; +export type TradesUpdateSubscription = { __typename?: 'Subscription', tradesStream?: Array<{ __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side }> | null }; export const TradeFieldsFragmentDoc = gql` fragment TradeFields on Trade { @@ -32,23 +34,30 @@ export const TradeFieldsFragmentDoc = gql` } } `; +export const TradeUpdateFieldsFragmentDoc = gql` + fragment TradeUpdateFields on TradeUpdate { + id + price + size + createdAt + marketId + aggressor +} + `; export const TradesDocument = gql` query Trades($marketId: ID!, $pagination: Pagination) { - market(id: $marketId) { - id - tradesConnection(pagination: $pagination) { - edges { - node { - ...TradeFields - } - cursor - } - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage + trades(filter: {marketIds: [$marketId]}, pagination: $pagination) { + edges { + node { + ...TradeFields } + cursor + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage } } } @@ -84,16 +93,11 @@ export type TradesLazyQueryHookResult = ReturnType; export type TradesQueryResult = Apollo.QueryResult; export const TradesUpdateDocument = gql` subscription TradesUpdate($marketId: ID!) { - trades(marketId: $marketId) { - id - price - size - createdAt - marketId - aggressor + tradesStream(filter: {marketIds: [$marketId]}) { + ...TradeUpdateFields } } - `; + ${TradeUpdateFieldsFragmentDoc}`; /** * __useTradesUpdateSubscription__ diff --git a/libs/trades/src/lib/trades-container.tsx b/libs/trades/src/lib/trades-container.tsx index 3396728d7..318ae5fbc 100644 --- a/libs/trades/src/lib/trades-container.tsx +++ b/libs/trades/src/lib/trades-container.tsx @@ -1,4 +1,3 @@ -import compact from 'lodash/compact'; import { useDataProvider } from '@vegaprotocol/data-provider'; import type { AgGridReact } from 'ag-grid-react'; import { useRef } from 'react'; @@ -19,12 +18,11 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => { dataProvider: tradesWithMarketProvider, variables: { marketId }, }); - const trades = compact(data).map((d) => d.node); return ( { if (price) { updateOrder(marketId, { price }); diff --git a/libs/trades/src/lib/trades-data-provider.ts b/libs/trades/src/lib/trades-data-provider.ts index 47102c68c..720813b29 100644 --- a/libs/trades/src/lib/trades-data-provider.ts +++ b/libs/trades/src/lib/trades-data-provider.ts @@ -3,75 +3,95 @@ import { makeDerivedDataProvider, defaultAppend as append, } from '@vegaprotocol/data-provider'; -import type { PageInfo, Edge } from '@vegaprotocol/data-provider'; +import type { PageInfo, Cursor } from '@vegaprotocol/data-provider'; import type { Market } from '@vegaprotocol/markets'; -import { marketsProvider } from '@vegaprotocol/markets'; +import { marketsMapProvider } from '@vegaprotocol/markets'; import type { TradesQuery, TradesQueryVariables, TradeFieldsFragment, TradesUpdateSubscription, + TradeUpdateFieldsFragment, + TradesUpdateSubscriptionVariables, } from './__generated__/Trades'; import { TradesDocument, TradesUpdateDocument } from './__generated__/Trades'; import orderBy from 'lodash/orderBy'; -import produce from 'immer'; export const MAX_TRADES = 500; const getData = ( responseData: TradesQuery | null -): ({ - cursor: string; - node: TradeFieldsFragment; -} | null)[] => responseData?.market?.tradesConnection?.edges || []; +): (TradeFieldsFragment & Cursor)[] => + responseData?.trades?.edges.map((edge) => ({ + ...edge.node, + cursor: edge.cursor, + })) || []; const getDelta = (subscriptionData: TradesUpdateSubscription) => - subscriptionData?.trades || []; + subscriptionData?.tradesStream || []; -const update = ( - data: ReturnType | null, - delta: ReturnType -) => { - if (!data) return data; - return produce(data, (draft) => { - // for each incoming trade add it to the beginning and remove oldest trade - orderBy(delta, 'createdAt', 'desc').forEach((node) => { - const { marketId, ...nodeData } = node; - draft.unshift({ - node: { - ...nodeData, - __typename: 'Trade', - market: { - __typename: 'Market', - id: marketId, - }, - }, - cursor: '', - }); +const mapTradeUpdateToTrade = ( + tradeUpdate: TradeUpdateFieldsFragment +): TradeFieldsFragment => { + const { marketId, ...trade } = tradeUpdate; + return { + ...trade, + __typename: 'Trade', + market: { + __typename: 'Market', + id: marketId, + }, + }; +}; - if (draft.length > MAX_TRADES) { - draft.pop(); - } - }); +const mapTradeUpdateToTradeWithMarket = + (markets: Record) => + (tradeUpdate: TradeUpdateFieldsFragment): Trade => { + const { market, ...trade } = mapTradeUpdateToTrade(tradeUpdate); + return { + ...trade, + market: markets[market.id], + }; + }; + +const update = & Cursor>( + data: T[] | null, + delta: ReturnType, + variables: TradesQueryVariables, + mapDeltaToData: (delta: TradeUpdateFieldsFragment) => T +): T[] => { + const updatedData = data ? [...data] : ([] as T[]); + orderBy(delta, 'createdAt', 'desc').forEach((tradeUpdate) => { + const index = data?.findIndex((trade) => trade.id === tradeUpdate.id) ?? -1; + if (index !== -1) { + updatedData[index] = { + ...updatedData[index], + ...mapDeltaToData(tradeUpdate), + }; + } else if (!data?.length || tradeUpdate.createdAt >= data[0].createdAt) { + updatedData.unshift(mapDeltaToData(tradeUpdate)); + } }); + return updatedData.slice(0, MAX_TRADES); }; export type Trade = Omit & { market?: Market }; -export type TradeEdge = Edge; const getPageInfo = (responseData: TradesQuery | null): PageInfo | null => - responseData?.market?.tradesConnection?.pageInfo || null; + responseData?.trades?.pageInfo || null; export const tradesProvider = makeDataProvider< Parameters['0'], ReturnType, Parameters['0'], ReturnType, - TradesQueryVariables + TradesQueryVariables, + TradesUpdateSubscriptionVariables >({ query: TradesDocument, subscriptionQuery: TradesUpdateDocument, - update, + update: (data, delta, reload, variables) => + update(data, delta, variables, mapTradeUpdateToTrade), getData, getDelta, pagination: { @@ -79,34 +99,32 @@ export const tradesProvider = makeDataProvider< append, first: MAX_TRADES, }, + getSubscriptionVariables: ({ marketId }) => ({ marketId }), }); export const tradesWithMarketProvider = makeDerivedDataProvider< - (TradeEdge | null)[], - Trade[], + (Trade & Cursor)[], + never, TradesQueryVariables >( [ tradesProvider, - (callback, client) => marketsProvider(callback, client, undefined), + (callback, client) => marketsMapProvider(callback, client, undefined), ], - (partsData): (TradeEdge | null)[] | null => { - const edges = partsData[0] as ReturnType; - return edges.map((edge) => { - if (edge === null) { - return null; - } - const node = { - ...edge.node, - market: (partsData[1] as Market[]).find( - (market) => market.id === edge.node.market.id - ), - }; - const cursor = edge?.cursor || ''; - return { - cursor, - node, - }; - }); + (partsData, variables, prevData, parts): Trade[] | null => { + if (prevData && parts[0].isUpdate) { + return update( + prevData, + parts[0].delta as ReturnType, + variables, + mapTradeUpdateToTradeWithMarket(partsData[1] as Record) + ); + } + return ((partsData[0] as ReturnType) || []).map( + (trade) => ({ + ...trade, + market: (partsData[1] as Record)[trade.market.id], + }) + ); } ); diff --git a/libs/trades/src/lib/trades.mock.ts b/libs/trades/src/lib/trades.mock.ts index ae07b6eff..5201874c4 100644 --- a/libs/trades/src/lib/trades.mock.ts +++ b/libs/trades/src/lib/trades.mock.ts @@ -11,24 +11,20 @@ export const tradesQuery = ( override?: PartialDeep ): TradesQuery => { const defaultResult: TradesQuery = { - market: { - id: 'market-0', - tradesConnection: { - __typename: 'TradeConnection', - edges: trades.map((node, i) => ({ - __typename: 'TradeEdge', - node, - cursor: (i + 1).toString(), - })), - pageInfo: { - __typename: 'PageInfo', - startCursor: '0', - endCursor: trades.length.toString(), - hasNextPage: false, - hasPreviousPage: false, - }, + trades: { + __typename: 'TradeConnection', + edges: trades.map((node, i) => ({ + __typename: 'TradeEdge', + node, + cursor: (i + 1).toString(), + })), + pageInfo: { + __typename: 'PageInfo', + startCursor: '0', + endCursor: trades.length.toString(), + hasNextPage: false, + hasPreviousPage: false, }, - __typename: 'Market', }, }; @@ -40,7 +36,7 @@ export const tradesUpdateSubscription = ( ): TradesUpdateSubscription => { const defaultResult: TradesUpdateSubscription = { __typename: 'Subscription', - trades: [ + tradesStream: [ { __typename: 'TradeUpdate', id: '1234567890', From 37247a504ad2028f8999c7f99e8afe5dd2b0d263 Mon Sep 17 00:00:00 2001 From: Maciek Date: Sat, 10 Jun 2023 16:48:48 +0200 Subject: [PATCH 37/49] fix(trading): update click area in network switcher (#4057) Co-authored-by: Matthew Russell --- .../network-switcher.spec.tsx | 137 ++++++++++++------ .../network-switcher/network-switcher.tsx | 37 ++--- 2 files changed, 109 insertions(+), 65 deletions(-) diff --git a/libs/environment/src/components/network-switcher/network-switcher.spec.tsx b/libs/environment/src/components/network-switcher/network-switcher.spec.tsx index a5b0470d7..c7014e7db 100644 --- a/libs/environment/src/components/network-switcher/network-switcher.spec.tsx +++ b/libs/environment/src/components/network-switcher/network-switcher.spec.tsx @@ -13,6 +13,24 @@ import { Networks } from '../../'; jest.mock('../../hooks/use-environment'); describe('Network switcher', () => { + const { location } = window; + let hrefSetSpy: jest.SpyInstance; + + beforeEach(() => { + hrefSetSpy = jest.fn(); + // @ts-ignore can't set location as optional + delete window.location; + window.location = {} as Location; + Object.defineProperty(window.location, 'href', { + // @ts-ignore set cannot take SpyInstance + set: hrefSetSpy, + }); + }); + + afterEach(() => { + window.location = location; + }); + it.each` network | label ${Networks.CUSTOM} | ${envTriggerMapping[Networks.CUSTOM]} @@ -49,21 +67,29 @@ describe('Network switcher', () => { render(); await userEvent.click(screen.getByRole('button')); + let links = screen.getAllByRole('link'); + + expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]); + expect(links[1]).toHaveTextContent(envNameMapping[Networks.TESTNET]); + expect(links[0]).not.toHaveTextContent(t('current')); + expect(links[1]).not.toHaveTextContent(t('current')); + expect(links[0]).not.toHaveTextContent(t('not available')); + expect(links[1]).not.toHaveTextContent(t('not available')); + expect(links[2]).toHaveTextContent(t('Propose a network parameter change')); const menuitems = screen.getAllByRole('menuitem'); - expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]); - expect(menuitems[1]).toHaveTextContent(envNameMapping[Networks.TESTNET]); - expect(menuitems[0]).not.toHaveTextContent(t('current')); - expect(menuitems[1]).not.toHaveTextContent(t('current')); - expect(menuitems[0]).not.toHaveTextContent(t('not available')); - expect(menuitems[1]).not.toHaveTextContent(t('not available')); - expect(menuitems[2]).toHaveTextContent(t('Advanced')); + expect(menuitems[0]).toHaveTextContent('Advanced'); - const links = screen.getAllByRole('link'); + await userEvent.click(links[0]); + expect(hrefSetSpy).toHaveBeenCalledWith(mainnetUrl); - expect(links[0]).toHaveAttribute('href', mainnetUrl); - expect(links[1]).toHaveAttribute('href', testnetUrl); + // re open dropdown as clicking an item will close it + await userEvent.click(screen.getByRole('button')); + links = screen.getAllByRole('link'); + + await userEvent.click(links[1]); + expect(hrefSetSpy).toHaveBeenCalledWith(testnetUrl); }); it('displays the correct selected network on the default dropdown view', async () => { @@ -82,10 +108,10 @@ describe('Network switcher', () => { await userEvent.click(screen.getByRole('button')); - const menuitems = screen.getAllByRole('menuitem'); + const links = screen.getAllByRole('link'); - expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]); - expect(menuitems[0]).toHaveTextContent(t('current')); + expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]); + expect(links[0]).toHaveTextContent(t('current')); }); it('displays the correct selected network on the default dropdown view when it does not have an associated url', async () => { @@ -103,10 +129,10 @@ describe('Network switcher', () => { await userEvent.click(screen.getByRole('button')); - const menuitems = screen.getAllByRole('menuitem'); + const links = screen.getAllByRole('link'); - expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]); - expect(menuitems[0]).toHaveTextContent(t('current')); + expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]); + expect(links[0]).toHaveTextContent(t('current')); }); it('displays the correct state for a network without url on the default dropdown view', async () => { @@ -123,44 +149,59 @@ describe('Network switcher', () => { render(); await userEvent.click(screen.getByRole('button')); + const links = screen.getAllByRole('link'); - const menuitems = screen.getAllByRole('menuitem'); - - expect(menuitems[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]); - expect(menuitems[0]).toHaveTextContent(t('not available')); + expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]); + expect(links[0]).toHaveTextContent(t('not available')); }); - it('displays the advanced view in the correct state', async () => { - const VEGA_NETWORKS: Record = { - [Networks.CUSTOM]: undefined, - [Networks.MAINNET]: 'https://main.net', - [Networks.TESTNET]: 'https://test.net', - [Networks.VALIDATOR_TESTNET]: 'https://validator-test.net', - [Networks.DEVNET]: 'https://dev.net', - [Networks.STAGNET1]: 'https://stag1.net', - }; - // @ts-ignore Typescript doesn't know about this module being mocked - useEnvironment.mockImplementation(() => ({ - VEGA_ENV: Networks.DEVNET, - VEGA_NETWORKS, - })); + it.each([Networks.MAINNET, Networks.TESTNET, Networks.DEVNET])( + 'displays the advanced view in the correct state', + async (network) => { + const VEGA_NETWORKS: Record = { + [Networks.CUSTOM]: undefined, + [Networks.MAINNET]: 'https://main.net', + [Networks.TESTNET]: 'https://test.net', + [Networks.VALIDATOR_TESTNET]: 'https://validator-test.net', + [Networks.DEVNET]: 'https://dev.net', + [Networks.STAGNET1]: 'https://stag1.net', + }; + // @ts-ignore Typescript doesn't know about this module being mocked + useEnvironment.mockImplementation(() => ({ + VEGA_ENV: Networks.DEVNET, + VEGA_NETWORKS, + })); - render(); + render(); - await userEvent.click(screen.getByRole('button')); - await userEvent.click( - screen.getByRole('menuitem', { name: t('Advanced') }) - ); + await userEvent.click(screen.getByTestId('network-switcher')); - [Networks.MAINNET, Networks.TESTNET, Networks.DEVNET].forEach((network) => { expect( - screen.getByRole('link', { name: envNameMapping[network] }) - ).toHaveAttribute('href', VEGA_NETWORKS[network]); - expect( - screen.getByText(envDescriptionMapping[network]) + await screen.findByRole('menuitem', { name: t('Advanced') }) ).toBeInTheDocument(); - }); - }); + + await userEvent.click( + screen.getByRole('menuitem', { name: t('Advanced') }) + ); + + expect( + await screen.findByText(envDescriptionMapping[network]) + ).toBeInTheDocument(); + expect( + screen.getByRole('link', { + name: new RegExp(`^${envNameMapping[network]}`), + }) + ).toBeInTheDocument(); + + await userEvent.click( + screen.getByRole('link', { + name: new RegExp(`^${envNameMapping[network]}`), + }) + ); + + expect(hrefSetSpy).toHaveBeenCalledWith(VEGA_NETWORKS[network]); + } + ); it('labels the selected network in the advanced view', async () => { const selectedNetwork = Networks.DEVNET; @@ -188,7 +229,7 @@ describe('Network switcher', () => { const label = screen.getByText(`(${t('current')})`); expect(label).toBeInTheDocument(); - expect(label.parentNode?.firstElementChild).toHaveTextContent( + expect(label.parentNode?.parentNode?.firstElementChild).toHaveTextContent( envNameMapping[selectedNetwork] ); }); @@ -217,7 +258,7 @@ describe('Network switcher', () => { const label = screen.getByText('(not available)'); expect(label).toBeInTheDocument(); - expect(label.parentNode?.firstElementChild).toHaveTextContent( + expect(label.parentNode?.parentNode?.firstElementChild).toHaveTextContent( envNameMapping[Networks.MAINNET] ); }); diff --git a/libs/environment/src/components/network-switcher/network-switcher.tsx b/libs/environment/src/components/network-switcher/network-switcher.tsx index c4442aca5..ee30cf01a 100644 --- a/libs/environment/src/components/network-switcher/network-switcher.tsx +++ b/libs/environment/src/components/network-switcher/network-switcher.tsx @@ -1,7 +1,6 @@ -import { useState, useCallback, useRef } from 'react'; +import { useState, useCallback } from 'react'; import { t } from '@vegaprotocol/i18n'; import { - Link, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -99,7 +98,6 @@ export const NetworkSwitcher = ({ }, [setOpen, setAdvancedView] ); - const menuRef = useRef(null); const current = currentNetwork || VEGA_ENV; @@ -110,7 +108,6 @@ export const NetworkSwitcher = ({ trigger={ } > - + {!isAdvancedView && ( <> {standardNetworkKeys.map((key) => ( @@ -134,14 +128,16 @@ export const NetworkSwitcher = ({ key={key} data-testid="network-item" disabled={!VEGA_NETWORKS[key]} + role="link" + onClick={() => + (window.location.href = VEGA_NETWORKS[key] || '') + } > - - {envNameMapping[key]} - - + {envNameMapping[key]} + ))} {advancedNetworkKeys.map((key) => ( - + + (window.location.href = VEGA_NETWORKS[key] || '') + } + >
- {envNameMapping[key]} + {envNameMapping[key]} Date: Sat, 10 Jun 2023 16:49:10 +0200 Subject: [PATCH 38/49] fix(markets): back with updating by grid api (#4059) Co-authored-by: Matthew Russell --- .../markets-container/market-list-table.tsx | 210 ++---------------- .../markets-container.spec.tsx | 107 +++++++++ .../markets-container/use-column-defs.tsx | 196 ++++++++++++++++ 3 files changed, 318 insertions(+), 195 deletions(-) create mode 100644 libs/markets/src/lib/components/markets-container/markets-container.spec.tsx create mode 100644 libs/markets/src/lib/components/markets-container/use-column-defs.tsx diff --git a/libs/markets/src/lib/components/markets-container/market-list-table.tsx b/libs/markets/src/lib/components/markets-container/market-list-table.tsx index 3f780ff4c..ae4aa792f 100644 --- a/libs/markets/src/lib/components/markets-container/market-list-table.tsx +++ b/libs/markets/src/lib/components/markets-container/market-list-table.tsx @@ -1,29 +1,14 @@ import { forwardRef } from 'react'; -import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils'; -import { t } from '@vegaprotocol/i18n'; -import type { - VegaValueGetterParams, - VegaValueFormatterParams, - VegaICellRendererParams, - TypedDataAgGrid, -} from '@vegaprotocol/datagrid'; -import { COL_DEFS } from '@vegaprotocol/datagrid'; +import type { TypedDataAgGrid } from '@vegaprotocol/datagrid'; import { AgGridLazy as AgGrid, PriceFlashCell, MarketNameCell, - SetFilter, } from '@vegaprotocol/datagrid'; -import { ButtonLink } from '@vegaprotocol/ui-toolkit'; -import { AgGridColumn } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react'; -import * as Schema from '@vegaprotocol/types'; import type { MarketMaybeWithData } from '../../markets-provider'; -import { MarketTableActions } from './market-table-actions'; import { OracleStatus } from './oracle-status'; -import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; - -const { MarketTradingMode, AuctionTrigger } = Schema; +import { useColumnDefs } from './use-column-defs'; export const getRowId = ({ data }: { data: { id: string } }) => data.id; @@ -51,199 +36,34 @@ const MarketName = (props: MarketNameCellProps) => ( ); +const defaultColDef = { + resizable: true, + sortable: true, + filter: true, + filterParams: { buttons: ['reset'] }, + minWidth: 100, +}; + export const MarketListTable = forwardRef< AgGridReact, TypedDataAgGrid & { onMarketClick: (marketId: string, metaKey?: boolean) => void; } >(({ onMarketClick, ...props }, ref) => { - const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); + const columnDefs = useColumnDefs({ onMarketClick }); + return ( - - - ) => { - if (!data?.data) return undefined; - const { trigger, marketTradingMode } = data.data; - return marketTradingMode === - MarketTradingMode.TRADING_MODE_MONITORING_AUCTION && - trigger && - trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED - ? `${Schema.MarketTradingModeMapping[marketTradingMode]} - - ${Schema.AuctionTriggerMapping[trigger]}` - : Schema.MarketTradingModeMapping[marketTradingMode]; - }} - filter={SetFilter} - filterParams={{ - set: Schema.MarketTradingModeMapping, - }} - /> - ) => { - return data?.state ? Schema.MarketStateMapping[data.state] : '-'; - }} - filter={SetFilter} - filterParams={{ - set: Schema.MarketStateMapping, - }} - /> - ) => { - return data?.data?.bestBidPrice === undefined - ? undefined - : toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber(); - }} - valueFormatter={({ - data, - }: VegaValueFormatterParams< - MarketMaybeWithData, - 'data.bestBidPrice' - >) => - data?.data?.bestBidPrice === undefined - ? undefined - : addDecimalsFormatNumber( - data.data.bestBidPrice, - data.decimalPlaces - ) - } - /> - ) => { - return data?.data?.bestOfferPrice === undefined - ? undefined - : toBigNum( - data?.data?.bestOfferPrice, - data.decimalPlaces - ).toNumber(); - }} - valueFormatter={({ - data, - }: VegaValueFormatterParams< - MarketMaybeWithData, - 'data.bestOfferPrice' - >) => - data?.data?.bestOfferPrice === undefined - ? undefined - : addDecimalsFormatNumber( - data.data.bestOfferPrice, - data.decimalPlaces - ) - } - /> - ) => { - return data?.data?.markPrice === undefined - ? undefined - : toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber(); - }} - valueFormatter={({ - data, - }: VegaValueFormatterParams) => - data?.data?.bestOfferPrice === undefined - ? undefined - : addDecimalsFormatNumber(data.data.markPrice, data.decimalPlaces) - } - /> - ) => { - const value = - data?.tradableInstrument.instrument.product.settlementAsset; - return value ? ( - { - openAssetDetailsDialog(value.id, e.target as HTMLElement); - }} - > - {value.symbol} - - ) : ( - '' - ); - }} - /> - ) => { - if (!data) return null; - return ( - - ); - }} - /> - + /> ); }); diff --git a/libs/markets/src/lib/components/markets-container/markets-container.spec.tsx b/libs/markets/src/lib/components/markets-container/markets-container.spec.tsx new file mode 100644 index 000000000..ce1ee5f5d --- /dev/null +++ b/libs/markets/src/lib/components/markets-container/markets-container.spec.tsx @@ -0,0 +1,107 @@ +import { render, screen, act, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import * as DataProviders from '@vegaprotocol/data-provider'; +import { MockedProvider } from '@apollo/react-testing'; +import type { MarketMaybeWithData } from '../../markets-provider'; +import { MarketsContainer } from './markets-container'; + +const market = { + id: 'id-1', + tradableInstrument: { + instrument: { + product: { settlementAsset: { id: 'assetId-1' } }, + }, + }, + decimalPlaces: 1, + positionDecimalPlaces: 1, + state: 'STATE_ACTIVE', + tradingMode: 'TRADING_MODE_OPENING_AUCTION', + data: { + bestBidPrice: 100, + }, +} as unknown as MarketMaybeWithData; + +describe('MarketsContainer', () => { + it('context menu should stay open', async () => { + const spyOnSelect = jest.fn(); + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(DataProviders, 'useDataProvider') + .mockImplementation(() => { + return { + error: null, + reload: jest.fn(), + data: [market], + }; + }); + + let rerenderRef: (ui: React.ReactElement) => void; + await act(async () => { + const { rerender } = render( + + + + ); + rerenderRef = rerender; + }); + + // make sure ag grid is finished initializaing + const rowContainer = await screen.findByRole('rowgroup', { + name: (_name, element) => + element.classList.contains('ag-center-cols-container'), + }); + expect(within(rowContainer).getAllByRole('row')).toHaveLength(1); + expect( + screen.getByRole('rowgroup', { + name: (_name, element) => + element.classList.contains('ag-pinned-right-cols-container'), + }) + ).toBeInTheDocument(); + + // open the dropdown + await userEvent.click( + screen.getByRole('button', { + name: (_name, element) => + (element.parentNode as Element)?.getAttribute('id') === + 'cell-market-actions-8', + }) + ); + + await checkDropdown(); + + // reset the mock and rerender so the component + // updates with new data + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(DataProviders, 'useDataProvider') + .mockImplementation(() => { + return { + error: null, + reload: jest.fn(), + data: [{ ...market, state: 'STATE_PENDING' }], + }; + }); + + // @ts-ignore we await the act above so rerenderRef is definitely defined + rerenderRef( + + + + ); + + // make sure dropdown is still open + await checkDropdown(); + + async function checkDropdown() { + const dropdownContent = await screen.findByTestId( + 'market-actions-content' + ); + expect(dropdownContent).toBeInTheDocument(); + expect( + within(dropdownContent).getByRole('menuitem', { + name: 'Copy Market ID', + }) + ).toBeInTheDocument(); + } + }); +}); diff --git a/libs/markets/src/lib/components/markets-container/use-column-defs.tsx b/libs/markets/src/lib/components/markets-container/use-column-defs.tsx new file mode 100644 index 000000000..0bd0a4bee --- /dev/null +++ b/libs/markets/src/lib/components/markets-container/use-column-defs.tsx @@ -0,0 +1,196 @@ +import { useMemo } from 'react'; +import type { ColDef } from 'ag-grid-community'; +import { t } from '@vegaprotocol/i18n'; +import type { + VegaICellRendererParams, + VegaValueFormatterParams, + VegaValueGetterParams, +} from '@vegaprotocol/datagrid'; +import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid'; +import * as Schema from '@vegaprotocol/types'; +import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils'; +import { ButtonLink } from '@vegaprotocol/ui-toolkit'; +import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; +import type { MarketMaybeWithData } from '../../markets-provider'; +import { MarketTableActions } from './market-table-actions'; + +interface Props { + onMarketClick: (marketId: string, metaKey?: boolean) => void; +} + +const { MarketTradingMode, AuctionTrigger } = Schema; + +export const useColumnDefs = ({ onMarketClick }: Props) => { + const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); + + return useMemo( + () => [ + { + headerName: t('Market'), + field: 'tradableInstrument.instrument.code', + cellRenderer: 'MarketName', + cellRendererParams: { onMarketClick }, + }, + { + headerName: t('Description'), + field: 'tradableInstrument.instrument.name', + }, + { + headerName: t('Trading mode'), + field: 'tradingMode', + minWidth: 170, + valueFormatter: ({ + data, + }: VegaValueFormatterParams) => { + if (!data?.data) return '-'; + const { trigger, marketTradingMode } = data.data; + return marketTradingMode === + MarketTradingMode.TRADING_MODE_MONITORING_AUCTION && + trigger && + trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED + ? `${Schema.MarketTradingModeMapping[marketTradingMode]} + - ${Schema.AuctionTriggerMapping[trigger]}` + : Schema.MarketTradingModeMapping[marketTradingMode]; + }, + filter: SetFilter, + filterParams: { + set: Schema.MarketTradingModeMapping, + }, + }, + { + headerName: t('Status'), + field: 'state', + valueFormatter: ({ + data, + }: VegaValueFormatterParams) => { + return data?.state ? Schema.MarketStateMapping[data.state] : '-'; + }, + filter: SetFilter, + filterParams: { + set: Schema.MarketStateMapping, + }, + }, + { + headerName: t('Best bid'), + field: 'data.bestBidPrice', + type: 'rightAligned', + cellRenderer: 'PriceFlashCell', + filter: 'agNumberColumnFilter', + valueGetter: ({ + data, + }: VegaValueGetterParams) => { + return data?.data?.bestBidPrice === undefined + ? undefined + : toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber(); + }, + valueFormatter: ({ + data, + }: VegaValueFormatterParams< + MarketMaybeWithData, + 'data.bestBidPrice' + >) => + data?.data?.bestBidPrice === undefined + ? '-' + : addDecimalsFormatNumber( + data.data.bestBidPrice, + data.decimalPlaces + ), + }, + { + headerName: t('Best offer'), + field: 'data.bestOfferPrice', + type: 'rightAligned', + cellRenderer: 'PriceFlashCell', + filter: 'agNumberColumnFilter', + valueGetter: ({ + data, + }: VegaValueGetterParams< + MarketMaybeWithData, + 'data.bestOfferPrice' + >) => { + return data?.data?.bestOfferPrice === undefined + ? undefined + : toBigNum( + data?.data?.bestOfferPrice, + data.decimalPlaces + ).toNumber(); + }, + valueFormatter: ({ + data, + }: VegaValueFormatterParams< + MarketMaybeWithData, + 'data.bestOfferPrice' + >) => + data?.data?.bestOfferPrice === undefined + ? '-' + : addDecimalsFormatNumber( + data.data.bestOfferPrice, + data.decimalPlaces + ), + }, + { + headerName: t('Mark price'), + field: 'data.markPrice', + type: 'rightAligned', + cellRenderer: 'PriceFlashCell', + filter: 'agNumberColumnFilter', + valueGetter: ({ + data, + }: VegaValueGetterParams) => { + return data?.data?.markPrice === undefined + ? undefined + : toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber(); + }, + valueFormatter: ({ + data, + }: VegaValueFormatterParams) => + data?.data?.bestOfferPrice === undefined + ? '-' + : addDecimalsFormatNumber(data.data.markPrice, data.decimalPlaces), + }, + { + headerName: t('Settlement asset'), + field: 'tradableInstrument.instrument.product.settlementAsset.symbol', + cellRenderer: ({ + data, + }: VegaICellRendererParams< + MarketMaybeWithData, + 'tradableInstrument.instrument.product.settlementAsset.symbol' + >) => { + const value = + data?.tradableInstrument.instrument.product.settlementAsset; + return value ? ( + { + openAssetDetailsDialog(value.id, e.target as HTMLElement); + }} + > + {value.symbol} + + ) : ( + '' + ); + }, + }, + { + colId: 'market-actions', + field: 'id', + ...COL_DEFS.actions, + cellRenderer: ({ + data, + }: VegaICellRendererParams) => { + if (!data) return null; + return ( + + ); + }, + }, + ], + [onMarketClick, openAssetDetailsDialog] + ); +}; From cc7f4037182fc1681c993d79aee04857e896ca4b Mon Sep 17 00:00:00 2001 From: Sam Keen Date: Mon, 12 Jun 2023 12:31:03 +0100 Subject: [PATCH 39/49] feat(governance): hide proposal details for freeform proposals (#4073) --- .../src/routes/proposals/components/proposal/proposal.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/governance/src/routes/proposals/components/proposal/proposal.tsx b/apps/governance/src/routes/proposals/components/proposal/proposal.tsx index 37bde8d85..c454d5cbd 100644 --- a/apps/governance/src/routes/proposals/components/proposal/proposal.tsx +++ b/apps/governance/src/routes/proposals/components/proposal/proposal.tsx @@ -102,7 +102,8 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
{proposal.terms.change.__typename !== 'NewMarket' && - proposal.terms.change.__typename !== 'UpdateMarket' && ( + proposal.terms.change.__typename !== 'UpdateMarket' && + proposal.terms.change.__typename !== 'NewFreeform' && (
From 8f966a4ba6bef915ac830123a1160e631b540ede Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Mon, 12 Jun 2023 12:38:51 -0700 Subject: [PATCH 40/49] chore: remove copied dir --- libs/markets/__mocks__ copy/react-markdown.js | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 libs/markets/__mocks__ copy/react-markdown.js diff --git a/libs/markets/__mocks__ copy/react-markdown.js b/libs/markets/__mocks__ copy/react-markdown.js deleted file mode 100644 index 70942790b..000000000 --- a/libs/markets/__mocks__ copy/react-markdown.js +++ /dev/null @@ -1,6 +0,0 @@ -function ReactMarkdown({ children }) { - // eslint-disable-next-line react/jsx-no-useless-fragment - return <>{children}; -} - -export default ReactMarkdown; From 17ba3eb3513448db93d8f8e936ba4dc6b1f30812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20M=C5=82odzikowski?= Date: Tue, 13 Jun 2023 13:29:05 +0200 Subject: [PATCH 41/49] feat(ci): setup static on s3 (#4075) --- .github/workflows/ci-cd-trigger.yml | 19 ++++++++++++++----- .github/workflows/publish-dist.yml | 4 ++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-cd-trigger.yml b/.github/workflows/ci-cd-trigger.yml index 7ce11f714..f58b29e54 100644 --- a/.github/workflows/ci-cd-trigger.yml +++ b/.github/workflows/ci-cd-trigger.yml @@ -136,19 +136,28 @@ jobs: preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug") fi projects="$(echo $projects_e2e | sed 's|-e2e||g')" - if echo "$affected" | grep -q multisig-signer; then - echo "Tools are affected" - # tools are only applicable to check previews or deploy from develop to mainnet - if [[ "${{ github.event_name }}" = "pull_request" ]]; then + if [[ "${{ github.event_name }}" = "pull_request" ]]; then + if echo "$affected" | grep -q multisig-signer; then + echo "Tools are affected" + # tools are only applicable to check previews or deploy from develop to mainnet echo "Deploying tools on preview" preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug") projects+=' "multisig-signer" ' fi - if [[ "${{ github.ref }}" =~ .*develop$ ]]; then + elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then + if echo "$affected" | grep -q multisig-signer; then + echo "Tools are affected" + # tools are only applicable to check previews or deploy from develop to mainnet echo "Deploying tools on s3" projects+=' "multisig-signer" ' fi + if echo "$affected" | grep -q static; then + echo "Static are affected" + echo "Deploying static on s3" + projects+=' "static" ' + fi fi + projects_e2e=${projects_e2e%?} projects_e2e=[${projects_e2e// /,}] projects=[${projects// /,}] diff --git a/.github/workflows/publish-dist.yml b/.github/workflows/publish-dist.yml index 1c0b7f4ad..127a5daf7 100644 --- a/.github/workflows/publish-dist.yml +++ b/.github/workflows/publish-dist.yml @@ -74,6 +74,10 @@ jobs: envName="mainnet" bucketName="tools.vega.xyz" fi + if [[ "${{ matrix.app }}" = "static" ]]; then + envName="mainnet" + bucketName="static.vega.xyz" + fi elif [[ "${{ github.ref }}" =~ .*main$ ]]; then envName="mainnet" elif [[ "${{ matrix.app}}" = "trading" ]] && [[ "${{ startsWith(github.ref, 'refs/tags/v') && 'true' || 'false' }}" = "true" ]]; then From 37340b4dc3696971a3f989a710b2818c184f203a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20M=C5=82odzikowski?= Date: Tue, 13 Jun 2023 13:43:54 +0200 Subject: [PATCH 42/49] feat(ci): modify static content to trigger push --- apps/static/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/static/README.md b/apps/static/README.md index b3cc64ff1..0a62bdc1c 100644 --- a/apps/static/README.md +++ b/apps/static/README.md @@ -1,3 +1,5 @@ # Static -A static CDN for Vega assets +A static CDN for Vega assets: `static.vega.xyz` + +prepare assets by running: `yarn nx build static` From f25a58a3c33e07d883cf5b8812572c44783ff9e6 Mon Sep 17 00:00:00 2001 From: "m.ray" <16125548+MadalinaRaicu@users.noreply.github.com> Date: Wed, 14 Jun 2023 00:24:29 +0300 Subject: [PATCH 43/49] fix(liquidity): update liquidity table row id (#4081) --- libs/liquidity/src/lib/liquidity-table.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/liquidity/src/lib/liquidity-table.tsx b/libs/liquidity/src/lib/liquidity-table.tsx index 427f4f580..5988dfc4c 100644 --- a/libs/liquidity/src/lib/liquidity-table.tsx +++ b/libs/liquidity/src/lib/liquidity-table.tsx @@ -55,7 +55,7 @@ export const LiquidityTable = forwardRef( data.id} + getRowId={({ data }) => `${data.party.id}-${data.status}`} ref={ref} tooltipShowDelay={500} defaultColDef={{ From 931ad75cc973381aa25af3dc834c3512125081b2 Mon Sep 17 00:00:00 2001 From: "m.ray" <16125548+MadalinaRaicu@users.noreply.github.com> Date: Wed, 14 Jun 2023 12:06:35 +0300 Subject: [PATCH 44/49] fix(trading): oracle markets matching (#4080) --- .../oracle-full-profile.tsx | 4 +- .../src/lib/hooks/use-market-oracle.spec.ts | 12 +- .../src/lib/hooks/use-market-oracle.ts | 50 +- .../src/lib/hooks/use-oracle-markets.spec.ts | 676 ++++++++++++++++++ .../src/lib/hooks/use-oracle-markets.ts | 27 +- 5 files changed, 718 insertions(+), 51 deletions(-) create mode 100644 libs/markets/src/lib/hooks/use-oracle-markets.spec.ts diff --git a/libs/markets/src/lib/components/oracle-full-profile/oracle-full-profile.tsx b/libs/markets/src/lib/components/oracle-full-profile/oracle-full-profile.tsx index bb1db0e9a..85a39797b 100644 --- a/libs/markets/src/lib/components/oracle-full-profile/oracle-full-profile.tsx +++ b/libs/markets/src/lib/components/oracle-full-profile/oracle-full-profile.tsx @@ -104,9 +104,7 @@ export const OracleFullProfile = ({
{message}
{showMore diff --git a/libs/markets/src/lib/hooks/use-market-oracle.spec.ts b/libs/markets/src/lib/hooks/use-market-oracle.spec.ts index 8981064f2..cc5f4f851 100644 --- a/libs/markets/src/lib/hooks/use-market-oracle.spec.ts +++ b/libs/markets/src/lib/hooks/use-market-oracle.spec.ts @@ -84,7 +84,7 @@ describe('useMarketOracle', () => { type: 'eth_address', }, ], - oracle: {}, + oracle: { eth_address: 'eth_address' }, } as Provider, { proofs: [ @@ -93,7 +93,7 @@ describe('useMarketOracle', () => { type: 'eth_address', }, ], - oracle: {}, + oracle: { eth_address: address }, } as Provider, ]; mockOracleProofs.mockReturnValueOnce({ @@ -113,7 +113,9 @@ describe('useMarketOracle', () => { type: 'public_key', }, ], - oracle: {}, + oracle: { + public_key: 'public_key', + }, } as Provider, { proofs: [ @@ -122,7 +124,9 @@ describe('useMarketOracle', () => { type: 'public_key', }, ], - oracle: {}, + oracle: { + public_key: key, + }, } as Provider, ]; mockOracleProofs.mockReturnValueOnce({ diff --git a/libs/markets/src/lib/hooks/use-market-oracle.ts b/libs/markets/src/lib/hooks/use-market-oracle.ts index a805c9ec7..fbedcf12e 100644 --- a/libs/markets/src/lib/hooks/use-market-oracle.ts +++ b/libs/markets/src/lib/hooks/use-market-oracle.ts @@ -9,32 +9,30 @@ import type { DataSourceSpecFragment } from '../__generated__'; export const getMatchingOracleProvider = ( dataSourceSpec: DataSourceSpecFragment, providers: Provider[] -) => - providers.find((provider) => - provider.proofs.some((proof) => { - if ( - proof.type === 'eth_address' && - dataSourceSpec.sourceType.__typename === 'DataSourceDefinitionExternal' - ) { - return dataSourceSpec.sourceType.sourceType.signers?.some( - (signer) => - signer.signer.__typename === 'ETHAddress' && - signer.signer.address === proof.eth_address - ); - } - if ( - proof.type === 'public_key' && - dataSourceSpec.sourceType.__typename === 'DataSourceDefinitionExternal' - ) { - return dataSourceSpec.sourceType.sourceType.signers?.some( - (signer) => - signer.signer.__typename === 'PubKey' && - signer.signer.key === proof.public_key - ); - } - return false; - }) - ); +) => { + return providers.find((provider) => { + let oracleSignature: string; + const oracle = provider.oracle; + if ('public_key' in oracle && oracle.public_key) { + oracleSignature = oracle.public_key; + } else if ('eth_address' in oracle && oracle.eth_address) { + oracleSignature = oracle.eth_address; + } + + if ( + dataSourceSpec.sourceType.__typename === 'DataSourceDefinitionExternal' + ) { + return dataSourceSpec.sourceType.sourceType.signers?.some( + (signer) => + (signer.signer.__typename === 'ETHAddress' && + signer.signer.address === oracleSignature) || + (signer.signer.__typename === 'PubKey' && + signer.signer.key === oracleSignature) + ); + } + return false; + }); +}; export const useMarketOracle = ( marketId: string, diff --git a/libs/markets/src/lib/hooks/use-oracle-markets.spec.ts b/libs/markets/src/lib/hooks/use-oracle-markets.spec.ts new file mode 100644 index 000000000..607e79b2d --- /dev/null +++ b/libs/markets/src/lib/hooks/use-oracle-markets.spec.ts @@ -0,0 +1,676 @@ +import { renderHook } from '@testing-library/react'; +import type { Provider } from '../oracle-schema'; +import { useOracleMarkets } from './use-oracle-markets'; +const mockMarkets = jest.fn<{ data: unknown | null }, unknown[]>(() => ({ + data: marketsData, +})); + +jest.mock('../__generated__/OracleMarketsSpec', () => ({ + useOracleMarketsSpecQuery: jest.fn((args) => mockMarkets()), +})); + +describe('useOracleMarkets', () => { + it('returns undefined if no market data present', () => { + mockMarkets.mockReturnValueOnce({ data: null }); + const { result } = renderHook(() => useOracleMarkets(mockProvider)); + expect(result.current).toBeUndefined(); + }); + + it('returns correct market list for the given provider', () => { + mockMarkets.mockReturnValueOnce({ data: marketsData }); + const { result } = renderHook(() => useOracleMarkets(mockProvider)); + console.log(JSON.stringify(result.current)); + expect(result.current).toStrictEqual(oracleMarkets); + }); +}); + +const mockProvider: Provider = { + name: 'Mock Oracle', + url: 'https://Mock.com', + description_markdown: 'mock oracle description', + oracle: { + status: 'GOOD', + status_reason: '', + first_verified: '2023-05-22T00:00:00.000Z', + last_verified: '2023-05-22T00:00:00.000Z', + type: 'eth_address', + eth_address: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC', + }, + proofs: [ + { + format: 'url', + available: true, + type: 'web', + url: 'https://web.archive.org/web/20200923175817/https://docs.pro.Mock.com/#oracle', + }, + ], + github_link: + 'https://github.com/vegaprotocol/well-known/blob/main/oracle-providers/eth_address-0xaddress.toml', +}; + +const marketsData = { + marketsConnection: { + __typename: 'MarketConnection', + edges: [ + { + __typename: 'MarketEdge', + node: { + __typename: 'Market', + id: '2dca7baa5f7269b08d053668bca03f97f72e9a162327eebd941c54f1f9fb8f80', + state: 'STATE_ACTIVE', + tradingMode: 'TRADING_MODE_CONTINUOUS', + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + id: '', + name: 'BTC/USDT expiry 2023 June 30th', + code: 'BTC/USDT-230630', + product: { + __typename: 'Future', + dataSourceSpecForSettlementData: { + __typename: 'DataSourceSpec', + id: '6eb55cdb9e3d1697d9df2eb2d97c4560da3519652fdf7e542f5801fc0919c32d', + data: { + __typename: 'DataSourceDefinition', + sourceType: { + __typename: 'DataSourceDefinitionExternal', + sourceType: { + __typename: 'DataSourceSpecConfiguration', + signers: [ + { + __typename: 'Signer', + signer: { + __typename: 'ETHAddress', + address: '0xaddress', + }, + }, + ], + filters: [ + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.BTC.value', + type: 'TYPE_INTEGER', + numberDecimalPlaces: 6, + }, + }, + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.BTC.timestamp', + type: 'TYPE_TIMESTAMP', + numberDecimalPlaces: null, + }, + }, + ], + }, + }, + }, + }, + dataSourceSpecForTradingTermination: { + __typename: 'DataSourceSpec', + id: '01d86d6182ee2e03cf02f9091734494932d39ab5ae6f23f7f5fd1fbe6668d422', + data: { + __typename: 'DataSourceDefinition', + sourceType: { + __typename: 'DataSourceDefinitionInternal', + }, + }, + }, + dataSourceSpecBinding: { + __typename: 'DataSourceSpecToFutureBinding', + settlementDataProperty: 'prices.BTC.value', + tradingTerminationProperty: 'vegaprotocol.builtin.timestamp', + }, + }, + }, + }, + }, + }, + { + __typename: 'MarketEdge', + node: { + __typename: 'Market', + id: '84025e68387cf61c2b91228d768dcdd4f10a9ee5cd2824fdea35b259976f59c1', + state: 'STATE_ACTIVE', + tradingMode: 'TRADING_MODE_CONTINUOUS', + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + id: '', + name: 'LINK/USDT expiry 2023 June 30th', + code: 'LINK/USDT-230630', + product: { + __typename: 'Future', + dataSourceSpecForSettlementData: { + __typename: 'DataSourceSpec', + id: 'bd709a4e6820d8714241f4c9576dffa71108179663c1f8442c991121fa1b0251', + data: { + __typename: 'DataSourceDefinition', + sourceType: { + __typename: 'DataSourceDefinitionExternal', + sourceType: { + __typename: 'DataSourceSpecConfiguration', + signers: [ + { + __typename: 'Signer', + signer: { + __typename: 'ETHAddress', + address: '0xaddress', + }, + }, + ], + filters: [ + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.LINK.value', + type: 'TYPE_INTEGER', + numberDecimalPlaces: 6, + }, + }, + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.LINK.timestamp', + type: 'TYPE_TIMESTAMP', + numberDecimalPlaces: null, + }, + }, + ], + }, + }, + }, + }, + dataSourceSpecForTradingTermination: { + __typename: 'DataSourceSpec', + id: '01d86d6182ee2e03cf02f9091734494932d39ab5ae6f23f7f5fd1fbe6668d422', + data: { + __typename: 'DataSourceDefinition', + sourceType: { + __typename: 'DataSourceDefinitionInternal', + }, + }, + }, + dataSourceSpecBinding: { + __typename: 'DataSourceSpecToFutureBinding', + settlementDataProperty: 'prices.LINK.value', + tradingTerminationProperty: 'vegaprotocol.builtin.timestamp', + }, + }, + }, + }, + }, + }, + { + __typename: 'MarketEdge', + node: { + __typename: 'Market', + id: '4507930a8c508eef6731f1342720adfa5f46096a8ef7a5848450740132ab78ab', + state: 'STATE_ACTIVE', + tradingMode: 'TRADING_MODE_CONTINUOUS', + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + id: '', + name: 'ETH/USDT expiry 2023 June 30th', + code: 'ETH/USDT-230630', + product: { + __typename: 'Future', + dataSourceSpecForSettlementData: { + __typename: 'DataSourceSpec', + id: '2687518113f63219a0b7594688dc78be62c86936a8ce306f50032ec70bdce493', + data: { + __typename: 'DataSourceDefinition', + sourceType: { + __typename: 'DataSourceDefinitionExternal', + sourceType: { + __typename: 'DataSourceSpecConfiguration', + signers: [ + { + __typename: 'Signer', + signer: { + __typename: 'ETHAddress', + address: '0xaddress', + }, + }, + ], + filters: [ + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.ETH.value', + type: 'TYPE_INTEGER', + numberDecimalPlaces: 6, + }, + }, + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.ETH.timestamp', + type: 'TYPE_TIMESTAMP', + numberDecimalPlaces: null, + }, + }, + ], + }, + }, + }, + }, + dataSourceSpecForTradingTermination: { + __typename: 'DataSourceSpec', + id: '01d86d6182ee2e03cf02f9091734494932d39ab5ae6f23f7f5fd1fbe6668d422', + data: { + __typename: 'DataSourceDefinition', + sourceType: { + __typename: 'DataSourceDefinitionInternal', + }, + }, + }, + dataSourceSpecBinding: { + __typename: 'DataSourceSpecToFutureBinding', + settlementDataProperty: 'prices.ETH.value', + tradingTerminationProperty: 'vegaprotocol.builtin.timestamp', + }, + }, + }, + }, + }, + }, + { + __typename: 'MarketEdge', + node: { + __typename: 'Market', + id: '074c929bba8faeeeba352b2569fc5360a59e12cdcbf60f915b492c4ac228b566', + state: 'STATE_PROPOSED', + tradingMode: 'TRADING_MODE_NO_TRADING', + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + id: '', + name: 'LINK/USDT expiry 2023 Sept 30th', + code: 'LINK/USDT-230930', + product: { + __typename: 'Future', + dataSourceSpecForSettlementData: { + __typename: 'DataSourceSpec', + id: 'cda7643a04cb45f62fdb06851a6fea2dc18d94931f2eab58f6918c6c13352fb6', + data: { + __typename: 'DataSourceDefinition', + sourceType: { + __typename: 'DataSourceDefinitionExternal', + sourceType: { + __typename: 'DataSourceSpecConfiguration', + signers: [ + { + __typename: 'Signer', + signer: { + __typename: 'ETHAddress', + address: '0xaddress', + }, + }, + ], + filters: [ + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.LINK.value', + type: 'TYPE_INTEGER', + numberDecimalPlaces: 6, + }, + }, + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.LINK.timestamp', + type: 'TYPE_TIMESTAMP', + numberDecimalPlaces: null, + }, + }, + ], + }, + }, + }, + }, + dataSourceSpecForTradingTermination: { + __typename: 'DataSourceSpec', + id: 'b3bf72d42d2938eceea05725949ecd24d7138a3cd7e29056a46b381efcbb4115', + data: { + __typename: 'DataSourceDefinition', + sourceType: { + __typename: 'DataSourceDefinitionInternal', + }, + }, + }, + dataSourceSpecBinding: { + __typename: 'DataSourceSpecToFutureBinding', + settlementDataProperty: 'prices.LINK.value', + tradingTerminationProperty: 'vegaprotocol.builtin.timestamp', + }, + }, + }, + }, + }, + }, + { + __typename: 'MarketEdge', + node: { + __typename: 'Market', + id: '2c2ea995d7366e423be7604f63ce047aa7186eb030ecc7b77395eae2fcbffcc5', + state: 'STATE_PROPOSED', + tradingMode: 'TRADING_MODE_NO_TRADING', + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + id: '', + name: 'ETH/USDT expiry 2023 Sept 30th', + code: 'ETH/USDT-230930', + product: { + __typename: 'Future', + dataSourceSpecForSettlementData: { + __typename: 'DataSourceSpec', + id: 'bb59cbdfbe167abc714954bf474354ac80b2feb798b907d6d86554fdd551f804', + data: { + __typename: 'DataSourceDefinition', + sourceType: { + __typename: 'DataSourceDefinitionExternal', + sourceType: { + __typename: 'DataSourceSpecConfiguration', + signers: [ + { + __typename: 'Signer', + signer: { + __typename: 'ETHAddress', + address: + '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC', + }, + }, + ], + filters: [ + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.ETH.value', + type: 'TYPE_INTEGER', + numberDecimalPlaces: 6, + }, + }, + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.ETH.timestamp', + type: 'TYPE_TIMESTAMP', + numberDecimalPlaces: null, + }, + }, + ], + }, + }, + }, + }, + dataSourceSpecForTradingTermination: { + __typename: 'DataSourceSpec', + id: 'b3bf72d42d2938eceea05725949ecd24d7138a3cd7e29056a46b381efcbb4115', + data: { + __typename: 'DataSourceDefinition', + sourceType: { + __typename: 'DataSourceDefinitionInternal', + }, + }, + }, + dataSourceSpecBinding: { + __typename: 'DataSourceSpecToFutureBinding', + settlementDataProperty: 'prices.ETH.value', + tradingTerminationProperty: 'vegaprotocol.builtin.timestamp', + }, + }, + }, + }, + }, + }, + { + __typename: 'MarketEdge', + node: { + __typename: 'Market', + id: '5b05109662e7434fea498c4a1c91d3179b80e9b8950d6106cec60e1f342fc604', + state: 'STATE_PROPOSED', + tradingMode: 'TRADING_MODE_NO_TRADING', + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + id: '', + name: 'BTC/USDT expiry 2023 Sept 30th', + code: 'BTC/USDT-230930', + product: { + __typename: 'Future', + dataSourceSpecForSettlementData: { + __typename: 'DataSourceSpec', + id: '99a1551b8cc7b75a3628a768e0772dde4c5a1ddf6c647507079c2e111d614a28', + data: { + __typename: 'DataSourceDefinition', + sourceType: { + __typename: 'DataSourceDefinitionExternal', + sourceType: { + __typename: 'DataSourceSpecConfiguration', + signers: [ + { + __typename: 'Signer', + signer: { + __typename: 'ETHAddress', + address: + '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC', + }, + }, + ], + filters: [ + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.BTC.value', + type: 'TYPE_INTEGER', + numberDecimalPlaces: 6, + }, + }, + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.BTC.timestamp', + type: 'TYPE_TIMESTAMP', + numberDecimalPlaces: null, + }, + }, + ], + }, + }, + }, + }, + dataSourceSpecForTradingTermination: { + __typename: 'DataSourceSpec', + id: 'b3bf72d42d2938eceea05725949ecd24d7138a3cd7e29056a46b381efcbb4115', + data: { + __typename: 'DataSourceDefinition', + sourceType: { + __typename: 'DataSourceDefinitionInternal', + }, + }, + }, + dataSourceSpecBinding: { + __typename: 'DataSourceSpecToFutureBinding', + settlementDataProperty: 'prices.BTC.value', + tradingTerminationProperty: 'vegaprotocol.builtin.timestamp', + }, + }, + }, + }, + }, + }, + ], + }, +}; + +const oracleMarkets = [ + { + __typename: 'Market', + id: '2c2ea995d7366e423be7604f63ce047aa7186eb030ecc7b77395eae2fcbffcc5', + state: 'STATE_PROPOSED', + tradingMode: 'TRADING_MODE_NO_TRADING', + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + id: '', + name: 'ETH/USDT expiry 2023 Sept 30th', + code: 'ETH/USDT-230930', + product: { + __typename: 'Future', + dataSourceSpecForSettlementData: { + __typename: 'DataSourceSpec', + id: 'bb59cbdfbe167abc714954bf474354ac80b2feb798b907d6d86554fdd551f804', + data: { + __typename: 'DataSourceDefinition', + sourceType: { + __typename: 'DataSourceDefinitionExternal', + sourceType: { + __typename: 'DataSourceSpecConfiguration', + signers: [ + { + __typename: 'Signer', + signer: { + __typename: 'ETHAddress', + address: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC', + }, + }, + ], + filters: [ + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.ETH.value', + type: 'TYPE_INTEGER', + numberDecimalPlaces: 6, + }, + }, + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.ETH.timestamp', + type: 'TYPE_TIMESTAMP', + numberDecimalPlaces: null, + }, + }, + ], + }, + }, + }, + }, + dataSourceSpecForTradingTermination: { + __typename: 'DataSourceSpec', + id: 'b3bf72d42d2938eceea05725949ecd24d7138a3cd7e29056a46b381efcbb4115', + data: { + __typename: 'DataSourceDefinition', + sourceType: { __typename: 'DataSourceDefinitionInternal' }, + }, + }, + dataSourceSpecBinding: { + __typename: 'DataSourceSpecToFutureBinding', + settlementDataProperty: 'prices.ETH.value', + tradingTerminationProperty: 'vegaprotocol.builtin.timestamp', + }, + }, + }, + }, + }, + { + __typename: 'Market', + id: '5b05109662e7434fea498c4a1c91d3179b80e9b8950d6106cec60e1f342fc604', + state: 'STATE_PROPOSED', + tradingMode: 'TRADING_MODE_NO_TRADING', + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + id: '', + name: 'BTC/USDT expiry 2023 Sept 30th', + code: 'BTC/USDT-230930', + product: { + __typename: 'Future', + dataSourceSpecForSettlementData: { + __typename: 'DataSourceSpec', + id: '99a1551b8cc7b75a3628a768e0772dde4c5a1ddf6c647507079c2e111d614a28', + data: { + __typename: 'DataSourceDefinition', + sourceType: { + __typename: 'DataSourceDefinitionExternal', + sourceType: { + __typename: 'DataSourceSpecConfiguration', + signers: [ + { + __typename: 'Signer', + signer: { + __typename: 'ETHAddress', + address: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC', + }, + }, + ], + filters: [ + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.BTC.value', + type: 'TYPE_INTEGER', + numberDecimalPlaces: 6, + }, + }, + { + __typename: 'Filter', + key: { + __typename: 'PropertyKey', + name: 'prices.BTC.timestamp', + type: 'TYPE_TIMESTAMP', + numberDecimalPlaces: null, + }, + }, + ], + }, + }, + }, + }, + dataSourceSpecForTradingTermination: { + __typename: 'DataSourceSpec', + id: 'b3bf72d42d2938eceea05725949ecd24d7138a3cd7e29056a46b381efcbb4115', + data: { + __typename: 'DataSourceDefinition', + sourceType: { __typename: 'DataSourceDefinitionInternal' }, + }, + }, + dataSourceSpecBinding: { + __typename: 'DataSourceSpecToFutureBinding', + settlementDataProperty: 'prices.BTC.value', + tradingTerminationProperty: 'vegaprotocol.builtin.timestamp', + }, + }, + }, + }, + }, +]; diff --git a/libs/markets/src/lib/hooks/use-oracle-markets.ts b/libs/markets/src/lib/hooks/use-oracle-markets.ts index da5b55a05..19879c2e8 100644 --- a/libs/markets/src/lib/hooks/use-oracle-markets.ts +++ b/libs/markets/src/lib/hooks/use-oracle-markets.ts @@ -5,9 +5,14 @@ import { useOracleMarketsSpecQuery } from '../__generated__/OracleMarketsSpec'; export const useOracleMarkets = ( provider: Provider ): OracleMarketSpecFieldsFragment[] | undefined => { - const signedProofs = provider.proofs.filter( - (proof) => proof.format === 'signed_message' && proof.available === true - ); + let oracleSignature: string; + const oracle = provider.oracle; + if ('public_key' in oracle && oracle.public_key) { + oracleSignature = oracle.public_key; + } + if ('eth_address' in oracle && oracle.eth_address) { + oracleSignature = oracle.eth_address; + } const { data: markets } = useOracleMarketsSpecQuery(); @@ -20,30 +25,16 @@ export const useOracleMarkets = ( return false; } const signers = sourceType?.sourceType.signers; - const signerKeys = signers?.filter(Boolean).map((signer) => { if (signer.signer.__typename === 'ETHAddress') { return signer.signer.address; } - if (signer.signer.__typename === 'PubKey') { return signer.signer.key; } - return undefined; }); - - const signedProofsKeys = signedProofs.map((proof) => { - if ('public_key' in proof && proof.public_key) { - return proof.public_key; - } - if ('eth_address' in proof && proof.eth_address) { - return proof.eth_address; - } - return undefined; - }); - - const key = signedProofsKeys.find((key) => signerKeys?.includes(key)); + const key = signerKeys?.find((key) => key === oracleSignature); return !!key; }); return oracleMarkets; From f6c0082f2d29cca6282edee2a40b15b37624665a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Szpiech?= Date: Wed, 14 Jun 2023 12:32:33 +0200 Subject: [PATCH 45/49] test(trading): #3506 orderbook AC coverage (#4088) --- .../src/integration/order-book.cy.ts | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 apps/trading-e2e/src/integration/order-book.cy.ts diff --git a/apps/trading-e2e/src/integration/order-book.cy.ts b/apps/trading-e2e/src/integration/order-book.cy.ts new file mode 100644 index 000000000..02c0b735d --- /dev/null +++ b/apps/trading-e2e/src/integration/order-book.cy.ts @@ -0,0 +1,102 @@ +const orderbookTab = 'Orderbook'; +const orderbookTable = 'tab-orderbook'; +const askPrice = 'price-9894585'; +const bidPrice = 'price-9889001'; +const askVolume = 'ask-vol-9894585'; +const bidVolume = 'bid-vol-9889001'; +const askCumulative = 'cumulative-vol-9894585'; +const bidCumulative = 'cumulative-vol-9889001'; +const midPrice = 'middle-mark-price-4612690000'; +const priceResolution = 'resolution'; +const dealTicketPrice = 'order-price'; +const resPrice = 'price-990'; + +describe('order book', { tags: '@smoke' }, () => { + before(() => { + cy.mockTradingPage(); + cy.mockSubscription(); + cy.visit('/#/markets/market-0'); + cy.wait('@Markets'); + }); + + beforeEach(() => { + cy.mockTradingPage(); + }); + + it('show order book', () => { + // 6003-ORDB-001 + // 6003-ORDB-002 + cy.getByTestId(orderbookTab).click(); + cy.getByTestId(orderbookTable).should('be.visible'); + cy.getByTestId(orderbookTable).should('not.be.empty'); + }); + + it('show orders prices', () => { + // 6003-ORDB-003 + cy.getByTestId(askPrice).should('have.text', '98.94585'); + cy.getByTestId(bidPrice).should('have.text', '98.89001'); + }); + + it('show prices volumes', () => { + // 6003-ORDB-004 + cy.getByTestId(askVolume).should('have.text', '1'); + cy.getByTestId(bidVolume).should('have.text', '1'); + }); + + it('show prices cumulative volumes', () => { + // 6003-ORDB-005 + cy.getByTestId(askCumulative).should('have.text', '39'); + cy.getByTestId(bidCumulative).should('have.text', '7'); + }); + + it('show mid price', () => { + // 6003-ORDB-006 + cy.getByTestId(midPrice).should('have.text', '46,126.90'); + }); + + it('sort prices descending', () => { + // 6003-ORDB-007 + const prices: number[] = []; + cy.getByTestId(orderbookTable).within(() => { + cy.get('[data-testid*=price]') + .each(($el) => { + prices.push(Number($el.text())); + }) + .then(() => { + expect(prices).to.deep.equal(prices.sort((a, b) => b - a)); + }); + }); + }); + + it('copy price to deal ticket form', () => { + // 6003-ORDB-009 + cy.getByTestId(askPrice).click(); + cy.getByTestId(dealTicketPrice).should('have.value', '98.94585'); + }); + + it('change price resolution', () => { + // 6003-ORDB-008 + const resolutions = [ + '0.00000', + '0.0000', + '0.000', + '0.00', + '0.0', + '0', + '10', + '100', + '1,000', + '10,000', + ]; + cy.getByTestId(priceResolution) + .find('option') + .each(($el, index) => { + expect($el.text()).to.equal(resolutions[index]); + }); + + cy.getByTestId(priceResolution).select('0.0'); + cy.getByTestId(resPrice).should('have.text', '99.0'); + cy.getByTestId(askPrice).should('not.exist'); + cy.getByTestId(bidPrice).should('not.exist'); + }); +}); From a9ebb36b9b266fae4d7f7f48355dd78d8bf22f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20M=C5=82odzikowski?= Date: Wed, 14 Jun 2023 12:37:41 +0200 Subject: [PATCH 46/49] feat(ci): deploy ui-toolkit to s3 (#4089) --- .github/workflows/ci-cd-trigger.yml | 7 +++++- .github/workflows/publish-dist.yml | 38 +++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd-trigger.yml b/.github/workflows/ci-cd-trigger.yml index f58b29e54..0200f9a9a 100644 --- a/.github/workflows/ci-cd-trigger.yml +++ b/.github/workflows/ci-cd-trigger.yml @@ -152,10 +152,15 @@ jobs: projects+=' "multisig-signer" ' fi if echo "$affected" | grep -q static; then - echo "Static are affected" + echo "static is affected" echo "Deploying static on s3" projects+=' "static" ' fi + if echo "$affected" | grep -q ui-toolkit; then + echo "ui-toolkit is affected" + echo "Deploying ui-toolkit on s3" + projects+=' "ui-toolkit" ' + fi fi projects_e2e=${projects_e2e%?} diff --git a/.github/workflows/publish-dist.yml b/.github/workflows/publish-dist.yml index 127a5daf7..0625fa342 100644 --- a/.github/workflows/publish-dist.yml +++ b/.github/workflows/publish-dist.yml @@ -78,6 +78,10 @@ jobs: envName="mainnet" bucketName="static.vega.xyz" fi + if [[ "${{ matrix.app }}" = "ui-toolkit" ]]; then + envName="mainnet" + bucketName="ui.vega.rocks" + fi elif [[ "${{ github.ref }}" =~ .*main$ ]]; then envName="mainnet" elif [[ "${{ matrix.app}}" = "trading" ]] && [[ "${{ startsWith(github.ref, 'refs/tags/v') && 'true' || 'false' }}" = "true" ]]; then @@ -118,6 +122,9 @@ jobs: if [ "${{ matrix.app }}" = "trading" ]; then yarn nx export trading $flags || (yarn install && yarn nx export trading $flags) DIST_LOCATION=dist/apps/trading/exported + elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then + NODE_ENV=production yarn nx run ui-toolkit:build-storybook + DIST_LOCATION=dist/storybook/ui-toolkit else yarn nx build ${{ matrix.app }} $flags || (yarn install && yarn nx build ${{ matrix.app }} $flags) DIST_LOCATION=dist/apps/${{ matrix.app }} @@ -152,6 +159,8 @@ jobs: - name: Publish dist as docker image (ghcr) uses: docker/build-push-action@v3 + continue-on-error: true + id: ghcr-push if: ${{ github.event_name == 'pull_request' || (matrix.app == 'trading' && github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') ) }} with: context: . @@ -165,6 +174,8 @@ jobs: - name: Publish dist as docker image (docker hub) uses: docker/build-push-action@v3 + continue-on-error: true + id: dockerhub-push if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }} with: context: . @@ -177,6 +188,33 @@ jobs: vegaprotocol/${{ matrix.app }}:${{ github.ref_name }} vegaprotocol/${{ matrix.app }}:mainnet + - name: Publish dist as docker image (ghcr - retry) + uses: docker/build-push-action@v3 + if: ${{ steps.ghcr-push.outcome == 'failure' }} + with: + context: . + file: docker/node-outside-docker.Dockerfile + push: true + build-args: | + APP=${{ matrix.app }} + ENV_NAME=${{ env.ENV_NAME }} + tags: | + ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }} + + - name: Publish dist as docker image (docker hub - retry) + uses: docker/build-push-action@v3 + if: ${{ steps.dockerhub-push.outcome == 'failure' }} + with: + context: . + file: docker/node-outside-docker.Dockerfile + push: true + build-args: | + APP=${{ matrix.app }} + ENV_NAME=${{ env.ENV_NAME }} + tags: | + vegaprotocol/${{ matrix.app }}:${{ github.ref_name }} + vegaprotocol/${{ matrix.app }}:mainnet + # bucket creation in github.com/vegaprotocol/terraform//frontend - name: Publish dist to s3 uses: jakejarvis/s3-sync-action@master From c0fdc8d57085a0d0f274fc53b5edc28ba97f0c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20M=C5=82odzikowski?= Date: Wed, 14 Jun 2023 13:35:50 +0200 Subject: [PATCH 47/49] feat(ci): add deploy & build info to ui-toolkit README --- libs/ui-toolkit/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libs/ui-toolkit/README.md b/libs/ui-toolkit/README.md index d5785e37d..bd225c7b7 100644 --- a/libs/ui-toolkit/README.md +++ b/libs/ui-toolkit/README.md @@ -5,3 +5,11 @@ This library was generated with [Nx](https://nx.dev). ## Running unit tests Run `nx test ui-toolkit` to execute the unit tests via [Jest](https://jestjs.io). + +## Build + +Run `yarn nx run ui-toolkit:build-storybook` + +## Deployment + +deployed at: `ui.vega.rocks` From d775573543f1765f2fafb700aa2c1195ca0ea2d3 Mon Sep 17 00:00:00 2001 From: daro-maj <119658839+daro-maj@users.noreply.github.com> Date: Wed, 14 Jun 2023 14:55:32 +0200 Subject: [PATCH 48/49] test(trading): add e2e tests for liquidity 5002-LIQP (#4082) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Głownia --- .../src/app/components/detail/detail.tsx | 21 +- .../src/integration/market-liquidity.cy.ts | 327 ++++++++++++++++++ apps/trading-e2e/src/support/trading.ts | 8 + .../client-pages/liquidity/liquidity.tsx | 6 +- libs/cypress/mock.ts | 1 + .../liquidity/src/lib/MarketLiquidity.graphql | 36 -- .../src/lib/__generated__/MarketLiquidity.ts | 70 ---- .../src/lib/liquidity-data-provider.spec.tsx | 99 +----- .../src/lib/liquidity-data-provider.ts | 30 +- libs/liquidity/src/lib/liquidity.mock.ts | 119 +++++++ 10 files changed, 475 insertions(+), 242 deletions(-) create mode 100644 apps/trading-e2e/src/integration/market-liquidity.cy.ts create mode 100644 libs/liquidity/src/lib/liquidity.mock.ts diff --git a/apps/liquidity-provision-dashboard/src/app/components/detail/detail.tsx b/apps/liquidity-provision-dashboard/src/app/components/detail/detail.tsx index 7a198d38e..b88eff8d0 100644 --- a/apps/liquidity-provision-dashboard/src/app/components/detail/detail.tsx +++ b/apps/liquidity-provision-dashboard/src/app/components/detail/detail.tsx @@ -7,31 +7,30 @@ import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { getFeeLevels, sumLiquidityCommitted, - marketLiquidityDataProvider, lpAggregatedDataProvider, } from '@vegaprotocol/liquidity'; -import type { MarketLpQuery } from '@vegaprotocol/liquidity'; +import { marketWithDataProvider } from '@vegaprotocol/markets'; +import type { MarketWithData } from '@vegaprotocol/markets'; import { Market } from './market'; import { Header } from './header'; import { LPProvidersGrid } from './providers'; -const formatMarket = (data: MarketLpQuery) => { +const formatMarket = (market: MarketWithData) => { return { - name: data?.market?.tradableInstrument.instrument.name, + name: market?.tradableInstrument.instrument.name, symbol: - data?.market?.tradableInstrument.instrument.product.settlementAsset - .symbol, + market?.tradableInstrument.instrument.product.settlementAsset.symbol, settlementAsset: - data?.market?.tradableInstrument.instrument.product.settlementAsset, - targetStake: data?.market?.data?.targetStake, - tradingMode: data?.market?.data?.marketTradingMode, - trigger: data?.market?.data?.trigger, + market?.tradableInstrument.instrument.product.settlementAsset, + targetStake: market?.data?.targetStake, + tradingMode: market?.data?.marketTradingMode, + trigger: market?.data?.trigger, }; }; export const lpDataProvider = makeDerivedDataProvider( - [marketLiquidityDataProvider, lpAggregatedDataProvider], + [marketWithDataProvider, lpAggregatedDataProvider], ([market, lpAggregatedData]) => ({ market: { ...formatMarket(market) }, liquidityProviders: lpAggregatedData || [], diff --git a/apps/trading-e2e/src/integration/market-liquidity.cy.ts b/apps/trading-e2e/src/integration/market-liquidity.cy.ts new file mode 100644 index 000000000..d625d12ed --- /dev/null +++ b/apps/trading-e2e/src/integration/market-liquidity.cy.ts @@ -0,0 +1,327 @@ +import { checkSorting } from '@vegaprotocol/cypress'; +import * as Schema from '@vegaprotocol/types'; + +const liquidityTab = 'Liquidity'; +const rowSelector = + '[data-testid="tab-liquidity"] .ag-center-cols-container .ag-row'; +const rowSelectorLiquidityActive = + '[data-testid="tab-active"] .ag-center-cols-container .ag-row'; +const rowSelectorLiquidityInactive = + '[data-testid="tab-inactive"] .ag-center-cols-container .ag-row'; +const marketSummaryBlock = 'header-summary'; +const itemValue = 'item-value'; +const itemHeader = 'item-header'; +const colCommitmentAmount = '[col-id="commitmentAmount"]'; +const colAverageEntryValuation = '[col-id="averageEntryValuation"]'; +const colEquityLikeShare = '[col-id="equityLikeShare"]'; +const colFee = '[col-id="fee"]'; +const colCommitmentAmount_1 = '[col-id="commitmentAmount_1"]'; +const colBalance = '[col-id="balance"]'; +const colStatus = '[col-id="status"]'; +const colCreatedAt = '[col-id="createdAt"] button'; +const colUpdatedAt = '[col-id="updatedAt"] button'; + +const headers = [ + 'Party', + 'Commitment (tDAI)', + 'Share', + 'Proposed fee', + 'Market valuation at entry', + 'Obligation', + 'Supplied', + 'Status', + 'Created', + 'Updated', +]; + +describe('liquidity table - trading', { tags: '@smoke' }, () => { + before(() => { + cy.mockSubscription(); + cy.mockTradingPage( + Schema.MarketState.STATE_ACTIVE, + Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION, + Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET + ); + cy.visit('/#/markets/market-0'); + cy.wait('@MarketData'); + cy.getByTestId(liquidityTab).click(); + cy.wait('@LiquidityProvisions'); + }); + + it('can see table headers', () => { + // 5002-LIQP-001 + cy.getByTestId('tab-liquidity').within(($headers) => { + cy.wrap($headers) + .get('.ag-header-cell-text') + .each(($header, i) => { + cy.wrap($header).should('have.text', headers[i]); + }); + }); + }); + + it('renders liquidity table correctly', () => { + // 5002-LIQP-002 + cy.get(rowSelector) + .first() + .find('[col-id="party.id"]') + .should( + 'have.text', + '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f' + ); + + cy.get(rowSelector) + .first() + .find(colCommitmentAmount) + .should('have.text', '4,000.00'); + + cy.get(rowSelector) + .first() + .find(colEquityLikeShare) + .should('have.text', '100.00%'); + + cy.get(rowSelector).first().find(colFee).should('have.text', '0.09%'); + + cy.get(rowSelector) + .first() + .find(colAverageEntryValuation) + .should('have.text', '685,852.93692'); + + cy.get(rowSelector) + .first() + .find(colCommitmentAmount_1) + .should('have.text', '4,000.00'); + + cy.get(rowSelector) + .first() + .find(colBalance) + .scrollIntoView() + .should('have.text', '4,000.00'); + + cy.get(rowSelector).first().find(colStatus).should('have.text', 'Active'); + + cy.get(rowSelector).first().find(colCreatedAt).should('not.be.empty'); + cy.get(rowSelector).first().find(colUpdatedAt).should('not.be.empty'); + }); + // #4079 + it.skip('liquidity status column should be sorted properly', () => { + // 5002-LIQP-003 + const liquidityColDefault = ['Active', 'Pending']; + const liquidityColAsc = ['Active', 'Pending']; + const liquidityColDesc = ['Pending', 'Active']; + checkSorting( + 'status', + liquidityColDefault, + liquidityColAsc, + liquidityColDesc + ); + }); +}); + +describe('liquidity table view', { tags: '@smoke' }, () => { + before(() => { + cy.mockSubscription(); + cy.mockTradingPage( + Schema.MarketState.STATE_ACTIVE, + Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION, + Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET + ); + cy.visit('/#/liquidity/market-0'); + cy.wait('@LiquidityProvisions'); + }); + + it('can see header title', () => { + // 5002-LIQP-004 + // 5002-LIQP-005 + cy.getByTestId('header-title') + .should('contain.text', 'BTCUSD.MF21 liquidity provision') + .and('contain.text', 'Go to trading'); + }); + + it('can see target stake', () => { + // 5002-LIQP-006 + cy.getByTestId(marketSummaryBlock).within(() => { + cy.getByTestId('target-stake').within(() => { + cy.getByTestId(itemHeader).should('have.text', 'Target stake'); + cy.getByTestId(itemValue).should('have.text', '10.00 tDAI').realHover(); + }); + }); + cy.getByTestId('tooltip-content').should( + 'contain.text', + `The market's liquidity requirement which is derived from the maximum open interest observed over a rolling time window.The market's liquidity requirement which is derived from the maximum open interest observed over a rolling time window.` + ); + }); + + it('can see supplied stake', () => { + // 5002-LIQP-007 + cy.getByTestId(marketSummaryBlock).within(() => { + cy.getByTestId('supplied-stake').within(() => { + cy.getByTestId(itemHeader).should('have.text', 'Supplied stake'); + cy.getByTestId(itemValue).should('have.text', '0.01 tDAI').realHover(); + }); + }); + cy.getByTestId('tooltip-content').should( + 'contain.text', + 'The current amount of liquidity supplied for this market.' + ); + }); + + it('can see liquidity supplied', () => { + //// 5002-LIQP-008 + cy.getByTestId(marketSummaryBlock).within(() => { + cy.getByTestId('liquidity-supplied').within(() => { + cy.getByTestId(itemHeader).should('have.text', 'Liquidity supplied'); + cy.getByTestId('indicator').should('be.visible'); + cy.getByTestId(itemValue).should('have.text', '0.10%').realHover(); + }); + }); + }); + + it('can see market id', () => { + // 5002-LIQP-009 + cy.getByTestId(marketSummaryBlock).within(() => { + cy.getByTestId('liquidity-market-id').within(() => { + cy.getByTestId(itemHeader).should('have.text', 'Market ID'); + cy.getByTestId(itemValue).should('have.text', 'market-0'); + }); + }); + }); + + it('can see market id', () => { + // 5002-LIQP-010 + cy.getByTestId(marketSummaryBlock).within(() => { + cy.getByTestId('liquidity-learn-more').within(() => { + cy.getByTestId(itemHeader).should('have.text', 'Learn more'); + cy.getByTestId(itemValue).should('have.text', 'Providing liquidity'); + cy.getByTestId('external-link') + .should('have.attr', 'href') + .and( + 'include', + 'https://docs.vega.xyz/testnet/concepts/liquidity/provision' + ); + }); + }); + }); + + describe('liquidity table view', { tags: '@smoke' }, () => { + it('can see table headers', () => { + cy.getByTestId('tab-active').within(($headers) => { + cy.wrap($headers) + .get('.ag-header-cell-text') + .each(($header, i) => { + cy.wrap($header).should('have.text', headers[i]); + }); + }); + }); + + it('renders liquidity active table correctly', () => { + // 5002-LIQP-011 + cy.get(rowSelectorLiquidityActive) + .first() + .find('[col-id="party.id"]') + .should( + 'have.text', + '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f' + ); + + cy.get(rowSelectorLiquidityActive) + .first() + .find(colCommitmentAmount) + .should('have.text', '4,000.00'); + + cy.get(rowSelectorLiquidityActive) + .first() + .find(colEquityLikeShare) + .should('have.text', '100.00%'); + + cy.get(rowSelectorLiquidityActive) + .first() + .find(colFee) + .should('have.text', '0.09%'); + + cy.get(rowSelectorLiquidityActive) + .first() + .find(colAverageEntryValuation) + .should('have.text', '685,852.93692'); + + cy.get(rowSelectorLiquidityActive) + .first() + .find(colCommitmentAmount_1) + .should('have.text', '4,000.00'); + + cy.get(rowSelectorLiquidityActive) + .first() + .find(colBalance) + .should('have.text', '4,000.00'); + + cy.get(rowSelectorLiquidityActive) + .first() + .find(colStatus) + .should('have.text', 'Active'); + + cy.get(rowSelectorLiquidityActive) + .first() + .find(colCreatedAt) + .should('not.be.empty'); + cy.get(rowSelectorLiquidityActive) + .first() + .find(colUpdatedAt) + .should('not.be.empty'); + }); + + it('renders liquidity inactive table correctly', () => { + //// 5002-LIQP-012 + cy.getByTestId('Inactive').click(); + cy.get(rowSelectorLiquidityInactive) + .first() + .find('[col-id="party.id"]') + .should( + 'have.text', + 'cc464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f' + ); + + cy.get(rowSelectorLiquidityInactive) + .first() + .find(colCommitmentAmount) + .should('have.text', '4,000.00'); + + cy.get(rowSelectorLiquidityInactive) + .first() + .find(colEquityLikeShare) + .should('have.text', '100.00%'); + + cy.get(rowSelectorLiquidityInactive) + .first() + .find(colFee) + .should('have.text', '0.40%'); + + cy.get(rowSelectorLiquidityInactive) + .first() + .find(colAverageEntryValuation) + .should('have.text', '685,852.93692'); + + cy.get(rowSelectorLiquidityInactive) + .first() + .find(colCommitmentAmount_1) + .should('have.text', '4,000.00'); + + cy.get(rowSelectorLiquidityInactive) + .first() + .find(colBalance) + .should('have.text', '2,000.00'); + + cy.get(rowSelectorLiquidityInactive) + .first() + .find(colStatus) + .should('have.text', 'Pending'); + + cy.get(rowSelectorLiquidityInactive) + .first() + .find(colCreatedAt) + .should('not.be.empty'); + cy.get(rowSelectorLiquidityInactive) + .first() + .find(colUpdatedAt) + .should('not.be.empty'); + }); + }); +}); diff --git a/apps/trading-e2e/src/support/trading.ts b/apps/trading-e2e/src/support/trading.ts index 011597092..ef7096a04 100644 --- a/apps/trading-e2e/src/support/trading.ts +++ b/apps/trading-e2e/src/support/trading.ts @@ -31,6 +31,8 @@ import { protocolUpgradeProposalsQuery, blockStatisticsQuery, networkParamQuery, + liquidityProvisionsQuery, + liquidityProviderFeeShareQuery, } from '@vegaprotocol/mock'; import type { PartialDeep } from 'type-fest'; import type { MarketDataQuery, MarketsQuery } from '@vegaprotocol/markets'; @@ -158,6 +160,12 @@ const mockTradingPage = ( ); aliasGQLQuery(req, 'Trades', tradesQuery()); aliasGQLQuery(req, 'Chart', chartQuery()); + aliasGQLQuery(req, 'LiquidityProvisions', liquidityProvisionsQuery()); + aliasGQLQuery( + req, + 'LiquidityProviderFeeShare', + liquidityProviderFeeShareQuery + ); aliasGQLQuery(req, 'Candles', candlesQuery()); aliasGQLQuery(req, 'Withdrawals', withdrawalsQuery()); aliasGQLQuery(req, 'NetworkParams', networkParamsQuery()); diff --git a/apps/trading/client-pages/liquidity/liquidity.tsx b/apps/trading/client-pages/liquidity/liquidity.tsx index 71924b40f..b7d5fac8b 100644 --- a/apps/trading/client-pages/liquidity/liquidity.tsx +++ b/apps/trading/client-pages/liquidity/liquidity.tsx @@ -150,6 +150,7 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
{targetStake @@ -163,6 +164,7 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
{suppliedStake @@ -178,10 +180,10 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => { {formatNumberPercentage(percentage, 2)} - +
{marketId}
- + {DocsLinks ? ( {t('Providing liquidity')} diff --git a/libs/cypress/mock.ts b/libs/cypress/mock.ts index a216a8d04..3b8d7fd8d 100644 --- a/libs/cypress/mock.ts +++ b/libs/cypress/mock.ts @@ -28,3 +28,4 @@ export * from '../trades/src/lib/trades.mock'; export * from '../withdraws/src/lib/withdrawal.mock'; export * from '../proposals/src/lib/protocol-upgrade-proposals/protocol-statistics-proposals.mock'; export * from '../proposals/src/lib/protocol-upgrade-proposals/block-statistics.mock'; +export * from '../liquidity/src/lib/liquidity.mock'; diff --git a/libs/liquidity/src/lib/MarketLiquidity.graphql b/libs/liquidity/src/lib/MarketLiquidity.graphql index 5c6186b58..d86469b55 100644 --- a/libs/liquidity/src/lib/MarketLiquidity.graphql +++ b/libs/liquidity/src/lib/MarketLiquidity.graphql @@ -1,39 +1,3 @@ -# MarketLp - -query MarketLp($marketId: ID!) { - market(id: $marketId) { - id - decimalPlaces - positionDecimalPlaces - tradableInstrument { - instrument { - code - name - product { - ... on Future { - settlementAsset { - id - symbol - decimals - } - } - } - } - } - data { - market { - id - } - marketTradingMode - suppliedStake - openInterest - targetStake - trigger - marketValueProxy - } - } -} - # Liquidity Provisions fragment LiquidityProvisionFields on LiquidityProvision { diff --git a/libs/liquidity/src/lib/__generated__/MarketLiquidity.ts b/libs/liquidity/src/lib/__generated__/MarketLiquidity.ts index c84e0de14..88b7d364f 100644 --- a/libs/liquidity/src/lib/__generated__/MarketLiquidity.ts +++ b/libs/liquidity/src/lib/__generated__/MarketLiquidity.ts @@ -3,13 +3,6 @@ import * as Types from '@vegaprotocol/types'; import { gql } from '@apollo/client'; import * as Apollo from '@apollo/client'; const defaultOptions = {} as const; -export type MarketLpQueryVariables = Types.Exact<{ - marketId: Types.Scalars['ID']; -}>; - - -export type MarketLpQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', code: string, name: string, product: { __typename?: 'Future', settlementAsset: { __typename?: 'Asset', id: string, symbol: string, decimals: number } } } }, data?: { __typename?: 'MarketData', marketTradingMode: Types.MarketTradingMode, suppliedStake?: string | null, openInterest: string, targetStake?: string | null, trigger: Types.AuctionTrigger, marketValueProxy: string, market: { __typename?: 'Market', id: string } } | null } | null }; - export type LiquidityProvisionFieldsFragment = { __typename?: 'LiquidityProvision', createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } }; export type LiquidityProvisionsQueryVariables = Types.Exact<{ @@ -65,69 +58,6 @@ export const LiquidityProviderFeeShareFieldsFragmentDoc = gql` averageEntryValuation } `; -export const MarketLpDocument = gql` - query MarketLp($marketId: ID!) { - market(id: $marketId) { - id - decimalPlaces - positionDecimalPlaces - tradableInstrument { - instrument { - code - name - product { - ... on Future { - settlementAsset { - id - symbol - decimals - } - } - } - } - } - data { - market { - id - } - marketTradingMode - suppliedStake - openInterest - targetStake - trigger - marketValueProxy - } - } -} - `; - -/** - * __useMarketLpQuery__ - * - * To run a query within a React component, call `useMarketLpQuery` and pass it any options that fit your needs. - * When your component renders, `useMarketLpQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useMarketLpQuery({ - * variables: { - * marketId: // value for 'marketId' - * }, - * }); - */ -export function useMarketLpQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(MarketLpDocument, options); - } -export function useMarketLpLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(MarketLpDocument, options); - } -export type MarketLpQueryHookResult = ReturnType; -export type MarketLpLazyQueryHookResult = ReturnType; -export type MarketLpQueryResult = Apollo.QueryResult; export const LiquidityProvisionsDocument = gql` query LiquidityProvisions($marketId: ID!) { market(id: $marketId) { diff --git a/libs/liquidity/src/lib/liquidity-data-provider.spec.tsx b/libs/liquidity/src/lib/liquidity-data-provider.spec.tsx index b926307ab..b2a4e59f4 100644 --- a/libs/liquidity/src/lib/liquidity-data-provider.spec.tsx +++ b/libs/liquidity/src/lib/liquidity-data-provider.spec.tsx @@ -1,10 +1,7 @@ import type { LiquidityProviderFeeShare } from '@vegaprotocol/types'; import { AccountType } from '@vegaprotocol/types'; import { getLiquidityProvision } from './liquidity-data-provider'; -import type { - LiquidityProvisionFieldsFragment, - MarketLpQuery, -} from './__generated__/MarketLiquidity'; +import type { LiquidityProvisionFieldsFragment } from './__generated__/MarketLiquidity'; const input = { liquidityProvisions: [ @@ -34,44 +31,6 @@ const input = { __typename: 'LiquidityProvision', } as LiquidityProvisionFieldsFragment, ], - marketLiquidity: { - market: { - id: 'ccbd651b4a1167fd73c4a0340ac759fa0a31ca487ad46a13254b741ad71947ed', - decimalPlaces: 5, - positionDecimalPlaces: 3, - tradableInstrument: { - instrument: { - code: 'UNIDAI.MF21', - name: 'UNIDAI Monthly (Dec 2022)', - product: { - settlementAsset: { - id: '16ae5dbb1fd7aa2ddef725703bfe66b3647a4da7b844bfdd04e985756f53d9d6', - symbol: 'tDAI', - decimals: 18, - __typename: 'Asset', - }, - __typename: 'Future', - }, - __typename: 'Instrument', - }, - __typename: 'TradableInstrument', - }, - data: { - market: { - id: 'ccbd651b4a1167fd73c4a0340ac759fa0a31ca487ad46a13254b741ad71947ed', - __typename: 'Market', - }, - marketTradingMode: 'TRADING_MODE_CONTINUOUS', - suppliedStake: '18003328918633596575000', - openInterest: '89660', - targetStake: '70159269843504000000', - trigger: 'AUCTION_TRIGGER_UNSPECIFIED', - marketValueProxy: '18003328918633596575000', - __typename: 'MarketData', - }, - __typename: 'Market', - }, - } as MarketLpQuery, liquidityFeeShare: [ { party: { @@ -88,7 +47,6 @@ const input = { const result = [ { __typename: 'LiquidityProvision', - assetDecimalPlaces: 18, averageEntryValuation: '12064118310408958216220.7224556301338111', balance: '1.8003328918633596575e+22', commitmentAmount: '18003328918633596575000', @@ -119,74 +77,25 @@ const result = [ describe('getLiquidityProvision', () => { it('should return an empty array when no data is provided', () => { - const data = getLiquidityProvision([], {}, []); + const data = getLiquidityProvision([], []); expect(data).toEqual([]); }); it('should return correct array when correct liquidity provision parameters are provided', () => { const data = getLiquidityProvision( input.liquidityProvisions, - input.marketLiquidity, input.liquidityFeeShare ); expect(data).toStrictEqual(result); }); it('should return empty array when no liquidity provision parameters are provided', () => { - const data = getLiquidityProvision( - [], - input.marketLiquidity, - input.liquidityFeeShare - ); + const data = getLiquidityProvision([], input.liquidityFeeShare); expect(data).toStrictEqual([]); }); - it('should return empty array when no market lp query parameter is provided', () => { - const data = getLiquidityProvision( - input.liquidityProvisions, - {}, - input.liquidityFeeShare - ); - const result = [ - { - __typename: 'LiquidityProvision', - assetDecimalPlaces: undefined, - averageEntryValuation: '12064118310408958216220.7224556301338111', - balance: '1.8003328918633596575e+22', - commitmentAmount: '18003328918633596575000', - createdAt: '2022-12-16T09:28:29.071781Z', - equityLikeShare: '1', - fee: '0.001', - party: { - __typename: 'Party', - accountsConnection: { - __typename: 'AccountsConnection', - edges: [ - { - __typename: 'AccountEdge', - node: { - __typename: 'AccountBalance', - balance: '18003328918633596575000', - type: 'ACCOUNT_TYPE_BOND', - }, - }, - ], - }, - id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59', - }, - status: 'STATUS_ACTIVE', - updatedAt: '2023-01-04T22:13:27.761985Z', - }, - ]; - expect(data).toStrictEqual(result); - }); - it('should return empty array when no liquidity fee share param is provided', () => { - const data = getLiquidityProvision( - input.liquidityProvisions, - input.marketLiquidity, - [] - ); + const data = getLiquidityProvision(input.liquidityProvisions, []); const result = [ { __typename: 'LiquidityProvision', diff --git a/libs/liquidity/src/lib/liquidity-data-provider.ts b/libs/liquidity/src/lib/liquidity-data-provider.ts index 8bf15c0f3..7384635f7 100644 --- a/libs/liquidity/src/lib/liquidity-data-provider.ts +++ b/libs/liquidity/src/lib/liquidity-data-provider.ts @@ -11,12 +11,9 @@ import { LiquidityProviderFeeShareDocument, LiquidityProvisionsDocument, LiquidityProvisionsUpdateDocument, - MarketLpDocument, } from './__generated__/MarketLiquidity'; import type { - MarketLpQuery, - MarketLpQueryVariables, LiquidityProviderFeeShareFieldsFragment, LiquidityProviderFeeShareQuery, LiquidityProviderFeeShareQueryVariables, @@ -78,19 +75,6 @@ export const liquidityProvisionsDataProvider = makeDataProvider< }, }); -export const marketLiquidityDataProvider = makeDataProvider< - MarketLpQuery, - MarketLpQuery, - never, - never, - MarketLpQueryVariables ->({ - query: MarketLpDocument, - getData: (responseData: MarketLpQuery | null) => { - return responseData; - }, -}); - export const liquidityFeeShareDataProvider = makeDataProvider< LiquidityProviderFeeShareQuery, LiquidityProviderFeeShareFieldsFragment[], @@ -109,29 +93,24 @@ export type Filter = { partyId?: string; active?: boolean }; export const lpAggregatedDataProvider = makeDerivedDataProvider< LiquidityProvisionData[], never, - MarketLpQueryVariables & { filter?: Filter } + LiquidityProvisionsQueryVariables & { filter?: Filter } >( [ (callback, client, variables) => liquidityProvisionsDataProvider(callback, client, { marketId: variables.marketId, }), - (callback, client, variables) => - marketLiquidityDataProvider(callback, client, { - marketId: variables.marketId, - }), (callback, client, variables) => liquidityFeeShareDataProvider(callback, client, { marketId: variables.marketId, }), ], ( - [liquidityProvisions, marketLiquidity, liquidityFeeShare], + [liquidityProvisions, liquidityFeeShare], { filter } ): LiquidityProvisionData[] => { return getLiquidityProvision( liquidityProvisions, - marketLiquidity, liquidityFeeShare, filter ); @@ -162,7 +141,6 @@ export const matchFilter = ( export const getLiquidityProvision = ( liquidityProvisions: LiquidityProvisionFieldsFragment[], - marketLiquidity: MarketLpQuery, liquidityFeeShare: LiquidityProviderFeeShareFieldsFragment[], filter?: Filter ): LiquidityProvisionData[] => { @@ -183,7 +161,6 @@ export const getLiquidityProvision = ( return true; }) .map((lp) => { - const market = marketLiquidity?.market; const feeShare = liquidityFeeShare.find( (f) => f.party.id === lp.party.id ); @@ -205,9 +182,6 @@ export const getLiquidityProvision = ( ...lp, averageEntryValuation: feeShare?.averageEntryValuation, equityLikeShare: feeShare?.equityLikeShare, - assetDecimalPlaces: - market?.tradableInstrument.instrument.product.settlementAsset - .decimals, balance, }; }); diff --git a/libs/liquidity/src/lib/liquidity.mock.ts b/libs/liquidity/src/lib/liquidity.mock.ts new file mode 100644 index 000000000..a91902557 --- /dev/null +++ b/libs/liquidity/src/lib/liquidity.mock.ts @@ -0,0 +1,119 @@ +import merge from 'lodash/merge'; +import * as Schema from '@vegaprotocol/types'; +import type { PartialDeep } from 'type-fest'; +import type { + LiquidityProviderFeeShareQuery, + LiquidityProvisionsQuery, +} from './__generated__/MarketLiquidity'; +import type { LiquidityProvisionFieldsFragment } from './__generated__/MarketLiquidity'; + +export const liquidityProvisionsQuery = ( + override?: PartialDeep +): LiquidityProvisionsQuery => { + const defaultResult: LiquidityProvisionsQuery = { + market: { + liquidityProvisionsConnection: { + __typename: 'LiquidityProvisionsConnection', + edges: liquidityFields.map((node) => { + return { + __typename: 'LiquidityProvisionsEdge', + node, + }; + }), + }, + }, + }; + return merge(defaultResult, override); +}; + +export const liquidityProviderFeeShareQuery = ( + override?: PartialDeep +): LiquidityProviderFeeShareQuery => { + const defaultResult: LiquidityProviderFeeShareQuery = { + market: { + id: 'market-0', + data: { + market: { + id: 'market-0', + __typename: 'Market', + }, + liquidityProviderFeeShare: [ + { + party: { + id: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f', + __typename: 'Party', + }, + equityLikeShare: '1', + averageEntryValuation: '68585293691.5598054356207737', + __typename: 'LiquidityProviderFeeShare', + }, + { + party: { + id: 'cc464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f', + __typename: 'Party', + }, + equityLikeShare: '1', + averageEntryValuation: '68585293691.5598054356207737', + __typename: 'LiquidityProviderFeeShare', + }, + ], + __typename: 'MarketData', + }, + __typename: 'Market', + }, + }; + return merge(defaultResult, override); +}; + +export const liquidityFields: LiquidityProvisionFieldsFragment[] = [ + { + party: { + id: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f', + accountsConnection: { + edges: [ + { + node: { + type: Schema.AccountType.ACCOUNT_TYPE_BOND, + balance: '400000000', + __typename: 'AccountBalance', + }, + __typename: 'AccountEdge', + }, + ], + __typename: 'AccountsConnection', + }, + __typename: 'Party', + }, + createdAt: '2023-05-15T11:47:15.132571Z', + updatedAt: '2023-05-15T11:47:15.132571Z', + commitmentAmount: '400000000', + fee: '0.0009', + status: Schema.LiquidityProvisionStatus.STATUS_ACTIVE, + __typename: 'LiquidityProvision', + }, + { + party: { + id: 'cc464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f', + accountsConnection: { + edges: [ + { + node: { + type: Schema.AccountType.ACCOUNT_TYPE_BOND, + balance: '200000000', + __typename: 'AccountBalance', + }, + __typename: 'AccountEdge', + }, + ], + __typename: 'AccountsConnection', + }, + __typename: 'Party', + }, + createdAt: '2023-05-15T11:47:15.132571Z', + updatedAt: '2023-05-15T11:47:15.132571Z', + commitmentAmount: '400000000', + fee: '0.004', + status: Schema.LiquidityProvisionStatus.STATUS_PENDING, + __typename: 'LiquidityProvision', + }, +]; From bdf166370970a61479869a742bd7160c72249077 Mon Sep 17 00:00:00 2001 From: daro-maj <119658839+daro-maj@users.noreply.github.com> Date: Wed, 14 Jun 2023 16:00:04 +0200 Subject: [PATCH 49/49] test(trading): update e2e tests for acs - closed all and proposed markets (#4094) --- .../src/integration/closed-markets.cy.ts | 14 ++++++ .../src/integration/market-all.cy.ts | 38 +++++++++++++- .../src/integration/market-selector.cy.ts | 5 +- .../src/integration/markets-proposed.cy.ts | 49 ++++++++++++++----- 4 files changed, 90 insertions(+), 16 deletions(-) diff --git a/apps/trading-e2e/src/integration/closed-markets.cy.ts b/apps/trading-e2e/src/integration/closed-markets.cy.ts index 08c1819ba..2778d1845 100644 --- a/apps/trading-e2e/src/integration/closed-markets.cy.ts +++ b/apps/trading-e2e/src/integration/closed-markets.cy.ts @@ -450,3 +450,17 @@ describe('Closed markets', { tags: '@smoke' }, () => { .should('have.text', 'View on Explorer'); }); }); + +describe('no closed markets', { tags: '@smoke', testIsolation: true }, () => { + before(() => { + cy.mockTradingPage(); + cy.mockSubscription(); + 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'); + }); +}); diff --git a/apps/trading-e2e/src/integration/market-all.cy.ts b/apps/trading-e2e/src/integration/market-all.cy.ts index f576a6ee5..9aee10bce 100644 --- a/apps/trading-e2e/src/integration/market-all.cy.ts +++ b/apps/trading-e2e/src/integration/market-all.cy.ts @@ -1,7 +1,10 @@ +import { aliasGQLQuery } from '@vegaprotocol/cypress'; +import type { MarketsQuery } from '@vegaprotocol/markets'; import * as Schema from '@vegaprotocol/types'; const rowSelector = '[data-testid="tab-all-markets"] .ag-center-cols-container .ag-row'; +const colInstrumentCode = '[col-id="tradableInstrument.instrument.code"]'; describe('markets all table', { tags: '@smoke' }, () => { beforeEach(() => { @@ -60,7 +63,7 @@ describe('markets all table', { tags: '@smoke' }, () => { // 6001-MARK-035 cy.get(rowSelector) .first() - .find('[col-id="tradableInstrument.instrument.code"]') + .find(colInstrumentCode) .should('have.text', 'SOLUSD'); // 6001-MARK-036 @@ -155,6 +158,7 @@ describe('markets all table', { tags: '@smoke' }, () => { }); it('able to open and sort full market list - market page', () => { + // 6001-MARK-064 const ExpectedSortedMarkets = [ 'AAPL.MF21', 'BTCUSD.MF21', @@ -167,8 +171,38 @@ describe('markets all table', { tags: '@smoke' }, () => { cy.get('.ag-header-cell-label').contains('Market').click(); // sort by market name for (let i = 0; i < ExpectedSortedMarkets.length; i++) { cy.get(`[row-index=${i}]`) - .find('[col-id="tradableInstrument.instrument.code"]') + .find(colInstrumentCode) .should('have.text', ExpectedSortedMarkets[i]); } }); + + it('can drag and drop columns', () => { + // 6001-MARK-065 + cy.get('.ag-overlay-loading-wrapper').should('not.be.visible'); + cy.get(colInstrumentCode) + .realMouseDown() + .realMouseMove(700, 15) + .realMouseUp(); + cy.get(colInstrumentCode).should(($element) => { + const attributeValue = $element.attr('aria-colindex'); + expect(attributeValue).not.to.equal('1'); + }); + }); +}); + +describe('no all markets', { tags: '@smoke', testIsolation: true }, () => { + before(() => { + cy.mockTradingPage(); + const markets: MarketsQuery = {}; + cy.mockGQL((req) => { + aliasGQLQuery(req, 'Markets', markets); + }); + cy.mockSubscription(); + cy.visit('/#/markets/all'); + }); + + it('can see no markets message', () => { + // 6001-MARK-048 + cy.getByTestId('tab-all-markets').should('contain.text', 'No markets'); + }); }); diff --git a/apps/trading-e2e/src/integration/market-selector.cy.ts b/apps/trading-e2e/src/integration/market-selector.cy.ts index 8a5fa15f3..42792e2b3 100644 --- a/apps/trading-e2e/src/integration/market-selector.cy.ts +++ b/apps/trading-e2e/src/integration/market-selector.cy.ts @@ -25,6 +25,7 @@ describe('markets selector', { tags: '@smoke' }, () => { cy.wait('@MarketsCandles'); }); + // 6001-MARK-066 it('can toggle the sidebar', () => { cy.getByTestId('market-selector').should('be.visible'); cy.getByTestId('sidebar-toggle').click(); @@ -84,7 +85,7 @@ describe('markets selector', { tags: '@smoke' }, () => { }); }); - // 6001-MARK-27 + // 6001-MARK-027 it('can use the filter options', () => { // product type cy.getByTestId('product-Spot').click(); @@ -94,7 +95,7 @@ describe('markets selector', { tags: '@smoke' }, () => { cy.getByTestId('product-Future').click(); cy.getByTestId(list).find('a').should('have.length', 4); - // 6001-MARK-29 + // 6001-MARK-029 cy.getByTestId(searchInput).clear().type('btc'); cy.getByTestId(list).find('a').should('have.length', 2); cy.getByTestId(list).find('a').eq(1).contains('BTCUSD.MF21'); diff --git a/apps/trading-e2e/src/integration/markets-proposed.cy.ts b/apps/trading-e2e/src/integration/markets-proposed.cy.ts index de9c5a13d..1cb30314c 100644 --- a/apps/trading-e2e/src/integration/markets-proposed.cy.ts +++ b/apps/trading-e2e/src/integration/markets-proposed.cy.ts @@ -1,16 +1,16 @@ -import { checkSorting } from '@vegaprotocol/cypress'; +import { aliasGQLQuery, checkSorting } from '@vegaprotocol/cypress'; +import type { ProposalsListQuery } from '@vegaprotocol/proposals'; const rowSelector = '[data-testid="tab-proposed-markets"] .ag-center-cols-container .ag-row'; +const colMarketId = '[col-id="market"]'; describe('markets proposed table', { tags: '@smoke' }, () => { - beforeEach(() => { - cy.clearLocalStorage().then(() => { - cy.mockTradingPage(); - cy.mockSubscription(); - cy.visit('/#/markets/all'); - cy.get('[data-testid="Proposed markets"]').click(); - }); + before(() => { + cy.mockTradingPage(); + cy.mockSubscription(); + cy.visit('/#/markets/all'); + cy.get('[data-testid="Proposed markets"]').click(); }); it('can see table headers', () => { @@ -35,10 +35,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => { it('renders markets correctly', () => { // 6001-MARK-049 - cy.get(rowSelector) - .first() - .find('[col-id="market"]') - .should('have.text', 'ETHUSD'); + cy.get(rowSelector).first().find(colMarketId).should('have.text', 'ETHUSD'); // 6001-MARK-050 cy.get(rowSelector) @@ -119,6 +116,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => { ); }); it('proposed markets tab should be sorted properly', () => { + // 6001-MARK-062 cy.get('[data-testid="Proposed markets"]').click({ force: true }); const marketColDefault = [ 'ETHUSD', @@ -196,4 +194,31 @@ describe('markets proposed table', { tags: '@smoke' }, () => { ]; checkSorting('state', stateColDefault, stateColAsc, stateColDesc); }); + + it('can drag and drop columns', () => { + // 6001-MARK-063 + cy.get(colMarketId).realMouseDown().realMouseMove(700, 15).realMouseUp(); + cy.get(colMarketId).should(($element) => { + const attributeValue = $element.attr('aria-colindex'); + expect(attributeValue).not.to.equal('1'); + }); + }); +}); + +describe('no markets proposed', { tags: '@smoke', testIsolation: true }, () => { + before(() => { + cy.mockTradingPage(); + const proposal: ProposalsListQuery = {}; + cy.mockGQL((req) => { + aliasGQLQuery(req, 'ProposalsList', proposal); + }); + cy.mockSubscription(); + cy.visit('/#/markets/all'); + cy.get('[data-testid="Proposed markets"]').click(); + }); + + it('can see no markets message', () => { + // 6001-MARK-061 + cy.getByTestId('tab-proposed-markets').should('contain.text', 'No markets'); + }); });