diff --git a/.husky/commit-msg b/.husky/commit-msg index b0dca9dee..047fa8eb6 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -3,6 +3,3 @@ # Lint commit messages to ensure they follow conventional commit standards yarn commitlint --edit "${1}" - -# Lint all staged files -yarn lint-staged diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..c18cf4092 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +# Lint all staged files +yarn lint-staged diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 000000000..30b469329 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,8 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +# Lint all staged files +yarn nx format:check + +# Test all projects with changes +yarn nx affected -t test --exclude trading diff --git a/apps/explorer-e2e/src/integration/oracles.cy.js b/apps/explorer-e2e/src/integration/oracles.cy.js new file mode 100644 index 000000000..06c4466af --- /dev/null +++ b/apps/explorer-e2e/src/integration/oracles.cy.js @@ -0,0 +1,68 @@ +context('Oracle page', { tags: '@smoke' }, () => { + describe('Verify elements on page', () => { + before('create market and navigate to oracle page', () => { + cy.createMarket(); + cy.visit('/oracles'); + }); + it('should see oracle data', () => { + cy.getByTestId('oracle-details').should('have.length.at.least', 2); + cy.getByTestId('oracle-details') + .should('exist') + .eq(0) + .within(() => { + cy.get('tr') + .eq(0) + .within(() => { + cy.get('th').should('have.text', 'ID'); + cy.get('a').invoke('text').should('have.length', 64); + cy.get('a') + .should('have.attr', 'href') + .and('contain', '/oracles/'); + }); + cy.get('tr') + .eq(1) + .within(() => { + cy.get('th').should('have.text', 'Type'); + cy.get('td').should('have.text', 'External data'); + }); + cy.get('tr') + .eq(2) + .within(() => { + cy.get('th').should('have.text', 'Signer'); + cy.getByTestId('keytype').should('have.text', 'Vega'); + cy.get('a').invoke('text').should('have.length', 64); + cy.get('a') + .should('have.attr', 'href') + .and('contain', '/parties/'); + }); + cy.get('tr') + .eq(3) + .within(() => { + cy.get('th').should('have.text', 'Settlement for'); + cy.get('a').invoke('text').should('have.length', 64); + cy.get('a') + .should('have.attr', 'href') + .and('contain', '/markets/'); + }); + cy.get('tr') + .eq(4) + .within(() => { + cy.get('th').should('have.text', 'Matched data'); + cy.get('td').should('have.text', '❌'); + }); + cy.get('details') + .eq(0) + .within(() => { + cy.contains('Filter').click(); + cy.get('.language-json').should('exist'); + }); + cy.get('details') + .eq(1) + .within(() => { + cy.contains('JSON').click(); + cy.get('.language-json').should('exist'); + }); + }); + }); + }); +}); diff --git a/apps/explorer/src/app/routes/oracles/home/index.tsx b/apps/explorer/src/app/routes/oracles/home/index.tsx index 0ee9abc0b..d088a0a47 100644 --- a/apps/explorer/src/app/routes/oracles/home/index.tsx +++ b/apps/explorer/src/app/routes/oracles/home/index.tsx @@ -38,7 +38,12 @@ const Oracles = () => { const dataConnection = o?.node.dataConnection; return ( -
+
{ validateMarketDataRow(4, 'Decimals', '5'); validateMarketDataRow(5, 'Quantum', '1'); validateMarketDataRow(6, 'Status', 'Enabled'); - validateMarketDataRow( - 7, - 'Contract address', - '0x0158031158Bb4dF2AD02eAA31e8963E84EA978a4' - ); + validateMarketDataRow(7, 'Contract address', '0x0158…78a4'); validateMarketDataRow(8, 'Withdrawal threshold', '0.0005'); validateMarketDataRow(9, 'Lifetime limit', '1,230'); validateMarketDataRow(10, 'Infrastructure fee account balance', '0.00001'); diff --git a/apps/trading-e2e/src/integration/order-book.cy.ts b/apps/trading-e2e/src/integration/order-book.cy.ts index ae12be599..34cb68fce 100644 --- a/apps/trading-e2e/src/integration/order-book.cy.ts +++ b/apps/trading-e2e/src/integration/order-book.cy.ts @@ -6,7 +6,7 @@ const askVolume = 'ask-vol-9894185'; const bidVolume = 'bid-vol-9889001'; const askCumulative = 'cumulative-vol-9894185'; const bidCumulative = 'cumulative-vol-9889001'; -const midPrice = 'middle-mark-price-4612690000'; +const midPrice = 'last-traded-4612690000'; const priceResolution = 'resolution'; const dealTicketPrice = 'order-price'; const dealTicketSize = 'order-size'; diff --git a/apps/trading-e2e/src/integration/wallet-eth.cy.ts b/apps/trading-e2e/src/integration/wallet-eth.cy.ts index 9859ff4c9..a5a5f89cf 100644 --- a/apps/trading-e2e/src/integration/wallet-eth.cy.ts +++ b/apps/trading-e2e/src/integration/wallet-eth.cy.ts @@ -43,11 +43,10 @@ describe('ethereum wallet', { tags: '@smoke', testIsolation: true }, () => { // 0004-EWAL-005 // 0004-EWAL-006 - const ethWalletAddress = Cypress.env('ETHEREUM_WALLET_ADDRESS'); cy.getByTestId('Deposits').click(); cy.getByTestId('deposit-button').click(); connectEthereumWallet('MetaMask'); - cy.getByTestId('ethereum-address').should('have.text', ethWalletAddress); + cy.getByTestId('ethereum-address').should('have.text', '0xEe7D…d94F'); cy.getByTestId('disconnect-ethereum-wallet') .should('have.text', 'Disconnect') .click(); diff --git a/apps/trading/.env b/apps/trading/.env index fbd2a537f..78e6de583 100644 --- a/apps/trading/.env +++ b/apps/trading/.env @@ -13,7 +13,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72 NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground # Cosmic elevator flags diff --git a/apps/trading/.env.capsule b/apps/trading/.env.capsule index 4638a051a..6d0e681ed 100644 --- a/apps/trading/.env.capsule +++ b/apps/trading/.env.capsule @@ -13,7 +13,7 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground NX_ETH_LOCAL_PROVIDER_URL=http://localhost:8545/ NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supply green clown session" diff --git a/apps/trading/.env.devnet b/apps/trading/.env.devnet index 9462cb56c..99d465c04 100644 --- a/apps/trading/.env.devnet +++ b/apps/trading/.env.devnet @@ -14,7 +14,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground # Cosmic elevator flags NX_SUCCESSOR_MARKETS=true diff --git a/apps/trading/.env.mainnet b/apps/trading/.env.mainnet index 44e6c8447..df13750fa 100644 --- a/apps/trading/.env.mainnet +++ b/apps/trading/.env.mainnet @@ -14,7 +14,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_VEGA_CONSOLE_URL=https://console.vega.xyz NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-mainnet/codfcglpplgmmlokgilfkpcjnmkbfiel -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet-mainnet +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet # TAG name of the current app version - TODO: bump to the latest upon release NX_APP_VERSION=v0.20.21-core-0.71.6 diff --git a/apps/trading/.env.mainnet-mirror b/apps/trading/.env.mainnet-mirror index aebcf5e35..9a575f33c 100644 --- a/apps/trading/.env.mainnet-mirror +++ b/apps/trading/.env.mainnet-mirror @@ -14,7 +14,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_VEGA_CONSOLE_URL=https://console.mainnet-mirror.vega.rocks NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-mainnet/codfcglpplgmmlokgilfkpcjnmkbfiel -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet-mainnet +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet # TAG name of the current app version - TODO: bump to the latest upon release NX_APP_VERSION=v0.20.19-core-0.71.6 diff --git a/apps/trading/.env.stagnet1 b/apps/trading/.env.stagnet1 index 0294d0d0b..4a9bc1e46 100644 --- a/apps/trading/.env.stagnet1 +++ b/apps/trading/.env.stagnet1 @@ -14,7 +14,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground # Cosmic elevator flags NX_SUCCESSOR_MARKETS=true diff --git a/apps/trading/.env.testnet b/apps/trading/.env.testnet index c673562b1..1e3dba3ca 100644 --- a/apps/trading/.env.testnet +++ b/apps/trading/.env.testnet @@ -15,7 +15,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_VEGA_CONSOLE_URL=https://console.fairground.wtf NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground # Cosmic elevator flags NX_SUCCESSOR_MARKETS=true diff --git a/apps/trading/.env.validators-testnet b/apps/trading/.env.validators-testnet index c31b364b1..c8e48e938 100644 --- a/apps/trading/.env.validators-testnet +++ b/apps/trading/.env.validators-testnet @@ -16,7 +16,7 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn -NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet +NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground # Cosmic elevator flags NX_SUCCESSOR_MARKETS=false diff --git a/apps/trading/components/accounts-menu/accounts-menu.tsx b/apps/trading/components/accounts-menu/accounts-menu.tsx index 8fe5ae993..86c80e91c 100644 --- a/apps/trading/components/accounts-menu/accounts-menu.tsx +++ b/apps/trading/components/accounts-menu/accounts-menu.tsx @@ -1,5 +1,5 @@ import { t } from '@vegaprotocol/i18n'; -import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit'; +import { TradingButton } from '@vegaprotocol/ui-toolkit'; import { ViewType, useSidebar } from '../sidebar'; export const AccountsMenu = () => { @@ -8,7 +8,6 @@ export const AccountsMenu = () => { return ( <> setView({ type: ViewType.Transfer })} @@ -16,7 +15,6 @@ export const AccountsMenu = () => { {t('Transfer')} setView({ type: ViewType.Deposit })} > diff --git a/apps/trading/components/deposits-menu/deposits-menu.tsx b/apps/trading/components/deposits-menu/deposits-menu.tsx index 28c751e84..81b6edf60 100644 --- a/apps/trading/components/deposits-menu/deposits-menu.tsx +++ b/apps/trading/components/deposits-menu/deposits-menu.tsx @@ -1,5 +1,5 @@ import { t } from '@vegaprotocol/i18n'; -import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit'; +import { TradingButton } from '@vegaprotocol/ui-toolkit'; import { ViewType, useSidebar } from '../sidebar'; export const DepositsMenu = () => { @@ -7,7 +7,6 @@ export const DepositsMenu = () => { return ( setView({ type: ViewType.Deposit })} data-testid="deposit-button" diff --git a/apps/trading/components/market-selector/market-selector-item.spec.tsx b/apps/trading/components/market-selector/market-selector-item.spec.tsx index 16d298cca..ee97aac1c 100644 --- a/apps/trading/components/market-selector/market-selector-item.spec.tsx +++ b/apps/trading/components/market-selector/market-selector-item.spec.tsx @@ -70,6 +70,7 @@ describe('MarketSelectorItem', () => { targetStake: '1000000', trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED, priceMonitoringBounds: null, + lastTradedPrice: '100', }; const candles = [ diff --git a/apps/trading/components/market-selector/market-selector.spec.tsx b/apps/trading/components/market-selector/market-selector.spec.tsx index 33f12fd18..fed3e90a7 100644 --- a/apps/trading/components/market-selector/market-selector.spec.tsx +++ b/apps/trading/components/market-selector/market-selector.spec.tsx @@ -228,9 +228,7 @@ describe('MarketSelector', () => { await userEvent.click(screen.getByTestId('sort-trigger')); const options = screen.getAllByTestId(/sort-item/); expect(options.map((o) => o.textContent?.trim())).toEqual( - Object.entries(Sort) - .filter(([key]) => key !== Sort.None) - .map(([key]) => SortTypeMapping[key as SortType]) + Object.entries(Sort).map(([key]) => SortTypeMapping[key as SortType]) ); await userEvent.click(screen.getByTestId('sort-item-Gained')); expect( diff --git a/apps/trading/components/market-selector/market-selector.tsx b/apps/trading/components/market-selector/market-selector.tsx index 90c6d1312..8606235b6 100644 --- a/apps/trading/components/market-selector/market-selector.tsx +++ b/apps/trading/components/market-selector/market-selector.tsx @@ -43,7 +43,7 @@ export const MarketSelector = ({ const [filter, setFilter] = useState({ searchTerm: '', product: Product.All, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); const allProducts = filter.product === Product.All; @@ -56,7 +56,7 @@ export const MarketSelector = ({ return (
-
+
{ @@ -107,9 +107,6 @@ export const MarketSelector = ({ currentSort={filter.sort} onSelect={(sort) => { setFilter((curr) => { - if (curr.sort === sort) { - return { ...curr, sort: Sort.None }; - } return { ...curr, sort, @@ -295,9 +292,9 @@ const List = ({ const Skeleton = () => { return ( -
-
-
+
+
+
diff --git a/apps/trading/components/market-selector/sort-dropdown.tsx b/apps/trading/components/market-selector/sort-dropdown.tsx index 82e6382ea..285d6d566 100644 --- a/apps/trading/components/market-selector/sort-dropdown.tsx +++ b/apps/trading/components/market-selector/sort-dropdown.tsx @@ -1,4 +1,3 @@ -import { t } from '@vegaprotocol/i18n'; import { DropdownMenu, DropdownMenuContent, @@ -11,7 +10,6 @@ import { } from '@vegaprotocol/ui-toolkit'; export const Sort = { - None: 'None', Gained: 'Gained', Lost: 'Lost', New: 'New', @@ -23,17 +21,15 @@ export type SortType = keyof typeof Sort; export const SortTypeMapping: { [key in SortType]: string; } = { - [Sort.None]: 'None', + [Sort.TopTraded]: 'Top traded', [Sort.Gained]: 'Top gaining', [Sort.Lost]: 'Top losing', [Sort.New]: 'New markets', - [Sort.TopTraded]: 'Top traded', }; const SortIconMapping: { [key in SortType]: VegaIconNames; } = { - [Sort.None]: null as unknown as VegaIconNames, // not shown in list [Sort.Gained]: VegaIconNames.TREND_UP, [Sort.Lost]: VegaIconNames.TREND_DOWN, [Sort.New]: VegaIconNames.STAR, @@ -51,10 +47,8 @@ export const SortDropdown = ({ - - {currentSort === SortTypeMapping.None - ? t('Sort') - : SortTypeMapping[currentSort]}{' '} + + {SortTypeMapping[currentSort]} @@ -65,24 +59,22 @@ export const SortDropdown = ({ value={currentSort} onValueChange={(value) => onSelect(value as SortType)} > - {Object.keys(Sort) - .filter((s) => s !== Sort.None) - .map((key) => { - return ( - - - {' '} - {SortTypeMapping[key as SortType]} - - - - ); - })} + {Object.keys(Sort).map((key) => { + return ( + + + {' '} + {SortTypeMapping[key as SortType]} + + + + ); + })} diff --git a/apps/trading/components/market-selector/use-market-selector-list.spec.tsx b/apps/trading/components/market-selector/use-market-selector-list.spec.tsx index ba8420cb6..ec5149073 100644 --- a/apps/trading/components/market-selector/use-market-selector-list.spec.tsx +++ b/apps/trading/components/market-selector/use-market-selector-list.spec.tsx @@ -12,7 +12,10 @@ import { useMarketList } from '@vegaprotocol/markets'; import type { Filter } from '../../components/market-selector'; import { subDays } from 'date-fns'; -jest.mock('@vegaprotocol/markets'); +jest.mock('@vegaprotocol/markets', () => ({ + ...jest.requireActual('@vegaprotocol/markets'), + useMarketList: jest.fn(), +})); const mockUseMarketList = useMarketList as jest.Mock; describe('useMarketSelectorList', () => { @@ -20,7 +23,7 @@ describe('useMarketSelectorList', () => { const defaultArgs: Filter = { searchTerm: '', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }; return renderHook((args) => useMarketSelectorList(args), { @@ -109,21 +112,21 @@ describe('useMarketSelectorList', () => { rerender({ searchTerm: '', product: Product.Spot as 'Future', - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([markets[1]]); rerender({ searchTerm: '', product: Product.Perpetual as 'Future', - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([markets[2]]); rerender({ searchTerm: '', product: Product.All, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual(markets); @@ -194,7 +197,7 @@ describe('useMarketSelectorList', () => { const { result, rerender } = setup({ searchTerm: '', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: ['asset-0'], }); @@ -203,7 +206,7 @@ describe('useMarketSelectorList', () => { rerender({ searchTerm: '', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: ['asset-0', 'asset-1'], }); @@ -216,7 +219,7 @@ describe('useMarketSelectorList', () => { rerender({ searchTerm: '', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: ['asset-0', 'asset-1', 'asset-2'], }); @@ -226,7 +229,7 @@ describe('useMarketSelectorList', () => { rerender({ searchTerm: '', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: ['asset-invalid'], }); @@ -281,28 +284,28 @@ describe('useMarketSelectorList', () => { const { result, rerender } = setup({ searchTerm: 'abc', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([markets[0]]); rerender({ searchTerm: 'def', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([markets[1], markets[2]]); rerender({ searchTerm: 'defg', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([markets[2]]); rerender({ searchTerm: 'zzz', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([]); @@ -311,14 +314,14 @@ describe('useMarketSelectorList', () => { rerender({ searchTerm: 'aaa', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([markets[0]]); rerender({ searchTerm: 'ggg', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); expect(result.current.markets).toEqual([ @@ -328,11 +331,15 @@ describe('useMarketSelectorList', () => { ]); }); - it('sorts by state and volume by default', () => { + it('sorts by top traded by default', () => { const markets = [ createMarketFragment({ id: 'market-0', - state: MarketState.STATE_PENDING, + state: MarketState.STATE_ACTIVE, + // @ts-ignore data not on fragment + data: { + markPrice: '1', + }, // @ts-ignore candles not on fragment candles: [ { @@ -343,30 +350,42 @@ describe('useMarketSelectorList', () => { createMarketFragment({ id: 'market-1', state: MarketState.STATE_ACTIVE, + // @ts-ignore data not on fragment + data: { + markPrice: '1', + }, // @ts-ignore candles not on fragment candles: [ { - volume: '200', + volume: '100', }, ], }), createMarketFragment({ id: 'market-2', state: MarketState.STATE_ACTIVE, + // @ts-ignore data not on fragment + data: { + markPrice: '1', + }, // @ts-ignore candles not on fragment candles: [ { - volume: '100', + volume: '300', }, ], }), createMarketFragment({ - state: MarketState.STATE_PENDING, id: 'market-3', + state: MarketState.STATE_ACTIVE, + // @ts-ignore data not on fragment + data: { + markPrice: '1', + }, // @ts-ignore candles not on fragment candles: [ { - volume: '100', + volume: '400', }, ], }), @@ -381,14 +400,15 @@ describe('useMarketSelectorList', () => { const { result } = setup({ searchTerm: '', product: Product.Future, - sort: Sort.None, + sort: Sort.TopTraded, assets: [], }); + expect(result.current.markets).toEqual([ - markets[1], + markets[3], markets[2], markets[0], - markets[3], + markets[1], ]); }); diff --git a/apps/trading/components/market-selector/use-market-selector-list.ts b/apps/trading/components/market-selector/use-market-selector-list.ts index 8dd29bc08..77ebaa86c 100644 --- a/apps/trading/components/market-selector/use-market-selector-list.ts +++ b/apps/trading/components/market-selector/use-market-selector-list.ts @@ -1,12 +1,7 @@ import { useMemo } from 'react'; import orderBy from 'lodash/orderBy'; import { MarketState } from '@vegaprotocol/types'; -import { - calcCandleVolume, - getAsset, - calcTradedFactor, - useMarketList, -} from '@vegaprotocol/markets'; +import { calcTradedFactor, useMarketList } from '@vegaprotocol/markets'; import { priceChangePercentage } from '@vegaprotocol/utils'; import type { Filter } from '../../components/market-selector/market-selector'; import { Sort } from './sort-dropdown'; @@ -60,22 +55,6 @@ export const useMarketSelectorList = ({ return false; }); - if (sort === Sort.None) { - // Sort by market state primarily and AtoZ secondarily - return orderBy( - markets, - [ - (m) => MARKET_TEMPLATE.indexOf(m.state), - (m) => { - if (!m.candles?.length) return 0; - const vol = calcCandleVolume(m.candles); - return Number(vol || 0); - }, - ], - ['asc', 'desc'] - ); - } - if (sort === Sort.Gained || sort === Sort.Lost) { const dir = sort === Sort.Gained ? 'desc' : 'asc'; return orderBy( diff --git a/apps/trading/components/positions-menu/positions-menu.tsx b/apps/trading/components/positions-menu/positions-menu.tsx index 58dddee7b..428bb82d7 100644 --- a/apps/trading/components/positions-menu/positions-menu.tsx +++ b/apps/trading/components/positions-menu/positions-menu.tsx @@ -1,5 +1,5 @@ import { t } from '@vegaprotocol/i18n'; -import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit'; +import { TradingButton } from '@vegaprotocol/ui-toolkit'; import { usePositionsStore } from '../positions-container'; export const PositionsMenu = () => { @@ -7,7 +7,6 @@ export const PositionsMenu = () => { const toggle = usePositionsStore((store) => store.toggleClosedMarkets); return ( { > setIsApproved(isOn)} - checked={isApproved} + onCheckedChange={(isOn) => setIsApproved(isOn ? 'true' : 'false')} + checked={isApproved === 'true'} /> diff --git a/apps/trading/components/welcome-dialog/get-started.tsx b/apps/trading/components/welcome-dialog/get-started.tsx index 59c0099ee..9b6480ba5 100644 --- a/apps/trading/components/welcome-dialog/get-started.tsx +++ b/apps/trading/components/welcome-dialog/get-started.tsx @@ -23,6 +23,7 @@ import { Links, Routes } from '../../pages/client-router'; import { useGlobalStore } from '../../stores'; import { useSidebar, ViewType } from '../sidebar'; import * as constants from '../constants'; +import { useOnboardingStore } from './welcome-dialog'; interface Props { lead?: string; @@ -35,7 +36,7 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => { constants.ONBOARDING_VIEWED_KEY ); - const update = useGlobalStore((store) => store.update); + const dismiss = useOnboardingStore((store) => store.dismiss); const marketId = useGlobalStore((store) => store.marketId); const link = marketId ? Links[Routes.MARKET](marketId) : Links[Routes.HOME](); const openVegaWalletDialog = useVegaWalletDialogStore( @@ -61,7 +62,7 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => { onClickHandle = () => { navigate(link); setView({ type: ViewType.Deposit }); - update({ onBoardingDismissed: true }); + dismiss(); }; } else if (step === OnboardingStep.ONBOARDING_ORDER_STEP) { buttonText = t('Dismiss'); diff --git a/apps/trading/components/welcome-dialog/telemetry-approval.spec.tsx b/apps/trading/components/welcome-dialog/telemetry-approval.spec.tsx index ac66dcaff..37fd35258 100644 --- a/apps/trading/components/welcome-dialog/telemetry-approval.spec.tsx +++ b/apps/trading/components/welcome-dialog/telemetry-approval.spec.tsx @@ -2,29 +2,35 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { TelemetryApproval } from './telemetry-approval'; -jest.mock('@vegaprotocol/logger', () => ({ - SentryInit: () => undefined, - SentryClose: () => undefined, -})); - -jest.mock('@vegaprotocol/environment', () => ({ - useEnvironment: () => ({ VEGA_ENV: 'test', SENTRY_DSN: 'sentry-dsn' }), -})); - describe('TelemetryApproval', () => { - it('click on checkbox should be properly handled', async () => { - const helpText = 'My help text'; - render(); - expect(screen.getByRole('checkbox')).toHaveAttribute( - 'data-state', - 'unchecked' + it('click on buttons should be properly handled', async () => { + const mockSetTelemetryValue = jest.fn(); + render( + ); - await userEvent.click(screen.getByRole('checkbox')); - expect(screen.getByRole('checkbox')).toHaveAttribute( - 'data-state', - 'checked' + expect( + screen.getByRole('button', { name: 'No thanks' }) + ).toBeInTheDocument(); + await userEvent.click(screen.getByRole('button', { name: 'No thanks' })); + expect(mockSetTelemetryValue).toHaveBeenCalledWith('false'); + expect(screen.getByText('Share data')).toBeInTheDocument(); + await userEvent.click(screen.getByText('Share data')); + expect(mockSetTelemetryValue).toHaveBeenCalledWith('true'); + }); + + it('confirm button should have proper text', async () => { + const mockSetTelemetryValue = jest.fn(); + render( + ); - expect(screen.getByText('Share usage data')).toBeInTheDocument(); - expect(screen.getByText(helpText)).toBeInTheDocument(); + expect(screen.getByText('Continue sharing data')).toBeInTheDocument(); + await userEvent.click(screen.getByText('Continue sharing data')); + expect(mockSetTelemetryValue).toHaveBeenCalledWith('true'); }); }); diff --git a/apps/trading/components/welcome-dialog/telemetry-approval.tsx b/apps/trading/components/welcome-dialog/telemetry-approval.tsx index 0fd49c983..5b38c2963 100644 --- a/apps/trading/components/welcome-dialog/telemetry-approval.tsx +++ b/apps/trading/components/welcome-dialog/telemetry-approval.tsx @@ -1,21 +1,66 @@ -import { TradingCheckbox } from '@vegaprotocol/ui-toolkit'; +import { + Intent, + TradingButton, + VegaIcon, + VegaIconNames, +} from '@vegaprotocol/ui-toolkit'; import { t } from '@vegaprotocol/i18n'; -import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval'; -export const TelemetryApproval = ({ helpText }: { helpText: string }) => { - const [isApproved, setIsApproved] = useTelemetryApproval(); +interface Props { + telemetryValue: string; + setTelemetryValue: (value: string) => void; +} + +export const TelemetryApproval = ({ + telemetryValue, + setTelemetryValue, +}: Props) => { return (
- {t('Share usage data')}} - checked={isApproved} - name="telemetry-approval" - onCheckedChange={() => setIsApproved(!isApproved)} - /> -
-
- {helpText} +
+ {t( + 'Help us identify bugs and improve Vega Governance by sharing anonymous usage data.' + )} +
+
+ +
+
{t('Anonymous')}
+
{t('Your identity is always anonymous on Vega')}
+
+
+ +
+ +
+
{t('Optional')}
+
{t('You can opt out any time via settings')}
+
+
+
+ setTelemetryValue('false')} + size="small" + intent={Intent.None} + data-testid="do-not-share-data-button" + fill + > + {t('No thanks')} + + + setTelemetryValue('true')} + intent={Intent.Info} + data-testid="share-data-button" + size="small" + fill + > + {telemetryValue === 'true' + ? t('Continue sharing data') + : t('Share data')} + +
); diff --git a/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx b/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx index 218db85bb..7e9564de4 100644 --- a/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx +++ b/apps/trading/components/welcome-dialog/welcome-dialog-content.tsx @@ -5,17 +5,17 @@ import { useNavigate } from 'react-router-dom'; import { Links, Routes } from '../../pages/client-router'; import { Networks, useEnvironment } from '@vegaprotocol/environment'; import type { ReactNode } from 'react'; -import { useGlobalStore } from '../../stores'; +import { useOnboardingStore } from './welcome-dialog'; export const WelcomeDialogContent = () => { const { VEGA_ENV } = useEnvironment(); - const update = useGlobalStore((store) => store.update); + const dismiss = useOnboardingStore((store) => store.dismiss); const navigate = useNavigate(); const browseMarkets = () => { const link = Links[Routes.MARKETS](); navigate(link); - update({ onBoardingDismissed: true }); + dismiss(); }; const lead = VEGA_ENV === Networks.MAINNET diff --git a/apps/trading/components/welcome-dialog/welcome-dialog.tsx b/apps/trading/components/welcome-dialog/welcome-dialog.tsx index 7739156ee..a3c42cdf1 100644 --- a/apps/trading/components/welcome-dialog/welcome-dialog.tsx +++ b/apps/trading/components/welcome-dialog/welcome-dialog.tsx @@ -1,7 +1,9 @@ -import React from 'react'; import { useNavigate } from 'react-router-dom'; -import { Dialog, Intent } from '@vegaprotocol/ui-toolkit'; +import type { Toast } from '@vegaprotocol/ui-toolkit'; +import { Dialog, Intent, useToasts } from '@vegaprotocol/ui-toolkit'; import { t } from '@vegaprotocol/i18n'; +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; import { useEnvironment } from '@vegaprotocol/environment'; import { useLocalStorage } from '@vegaprotocol/react-helpers'; import { WelcomeDialogContent } from './welcome-dialog-content'; @@ -12,15 +14,41 @@ import { OnboardingStep, } from './use-get-onboarding-step'; import * as constants from '../constants'; +import { TelemetryApproval } from './telemetry-approval'; +import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval'; +import { useCallback } from 'react'; +const ONBOARDING_STORAGE_KEY = 'vega_onboarding_dismiss_store'; +export const useOnboardingStore = create<{ + dismissed: boolean; + dismiss: () => void; +}>()( + persist( + (set) => ({ + dismissed: false, + dismiss: () => set(() => ({ dismissed: true })), + }), + { + name: ONBOARDING_STORAGE_KEY, + } + ) +); + +const TELEMETRY_APPROVAL_TOAST_ID = 'telemetry_tost_id'; export const WelcomeDialog = () => { const { VEGA_ENV } = useEnvironment(); - const [onBoardingViewed] = useLocalStorage(constants.ONBOARDING_VIEWED_KEY); - const update = useGlobalStore((store) => store.update); - const dismissed = useGlobalStore((store) => store.onBoardingDismissed); - const currentStep = useGetOnboardingStep(); - const navigate = useNavigate(); + const [telemetryValue, setTelemetryValue, isTelemetryNeeded, closeTelemetry] = + useTelemetryApproval(); + const [onBoardingViewed] = useLocalStorage(constants.ONBOARDING_VIEWED_KEY); + const dismiss = useOnboardingStore((store) => store.dismiss); + const dismissed = useOnboardingStore((store) => store.dismissed); + const currentStep = useGetOnboardingStep(); + const isTelemetryPopupNeeded = + isTelemetryNeeded && + (onBoardingViewed === 'true' || + currentStep > OnboardingStep.ONBOARDING_ORDER_STEP); + const isOnboardingDialogNeeded = onBoardingViewed !== 'true' && currentStep && @@ -29,12 +57,58 @@ export const WelcomeDialog = () => { const marketId = useGlobalStore((store) => store.marketId); const onClose = () => { - const link = marketId - ? Links[Routes.MARKET](marketId) - : Links[Routes.HOME](); - navigate(link); - update({ onBoardingDismissed: true }); + if (isTelemetryPopupNeeded) { + closeTelemetry(); + } else { + const link = marketId + ? Links[Routes.MARKET](marketId) + : Links[Routes.HOME](); + navigate(link); + dismiss(); + } }; + + const [setToast, hasToast, removeToast] = useToasts((store) => [ + store.setToast, + store.hasToast, + store.remove, + ]); + const onApprovalClose = useCallback(() => { + closeTelemetry(); + removeToast(TELEMETRY_APPROVAL_TOAST_ID); + }, [removeToast, closeTelemetry]); + + const setTelemetryApprovalAndClose = useCallback( + (value: string) => { + setTelemetryValue(value); + onApprovalClose(); + }, + [setTelemetryValue, onApprovalClose] + ); + + if (isTelemetryPopupNeeded) { + const toast: Toast = { + id: TELEMETRY_APPROVAL_TOAST_ID, + intent: Intent.Primary, + content: ( + <> +

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

+ + + ), + onClose: onApprovalClose, + }; + if (!hasToast(TELEMETRY_APPROVAL_TOAST_ID)) { + setToast(toast); + } + return; + } + const title = ( {t('Console')}{' '} diff --git a/apps/trading/components/withdrawals-menu/withdrawals-menu.tsx b/apps/trading/components/withdrawals-menu/withdrawals-menu.tsx index 22f64f40f..cc0724cf4 100644 --- a/apps/trading/components/withdrawals-menu/withdrawals-menu.tsx +++ b/apps/trading/components/withdrawals-menu/withdrawals-menu.tsx @@ -1,5 +1,5 @@ import { t } from '@vegaprotocol/i18n'; -import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit'; +import { TradingButton } from '@vegaprotocol/ui-toolkit'; import { ViewType, useSidebar } from '../sidebar'; export const WithdrawalsMenu = () => { @@ -7,7 +7,6 @@ export const WithdrawalsMenu = () => { return ( setView({ type: ViewType.Withdraw })} data-testid="withdraw-dialog-button" diff --git a/apps/trading/lib/hooks/use-telemetry-approval.spec.ts b/apps/trading/lib/hooks/use-telemetry-approval.spec.ts index bd0be96b9..eccddbcff 100644 --- a/apps/trading/lib/hooks/use-telemetry-approval.spec.ts +++ b/apps/trading/lib/hooks/use-telemetry-approval.spec.ts @@ -1,19 +1,40 @@ import { renderHook, act, waitFor } from '@testing-library/react'; import { useLocalStorage } from '@vegaprotocol/react-helpers'; import { SentryInit, SentryClose } from '@vegaprotocol/logger'; -import { STORAGE_KEY, useTelemetryApproval } from './use-telemetry-approval'; +import { + STORAGE_KEY, + STORAGE_SECOND_KEY, + useTelemetryApproval, +} from './use-telemetry-approval'; +import { Networks } from '@vegaprotocol/environment'; const mockSetValue = jest.fn(); -const mockRemoveValue = jest.fn(); +let mockStorageHookApprovalResult: [string | null, jest.Mock] = [ + null, + mockSetValue, +]; +const mockSetSecondValue = jest.fn(); +let mockStorageHookViewedResult: [string | null, jest.Mock] = [ + null, + mockSetSecondValue, +]; jest.mock('@vegaprotocol/logger'); jest.mock('@vegaprotocol/react-helpers', () => ({ ...jest.requireActual('@vegaprotocol/react-helpers'), - useLocalStorage: jest - .fn() - .mockImplementation(() => [false, mockSetValue, mockRemoveValue]), + useLocalStorage: jest.fn((key: string) => { + if (key === 'vega_telemetry_approval') { + return mockStorageHookApprovalResult; + } + return mockStorageHookViewedResult; + }), })); +let mockVegaEnv = 'test'; jest.mock('@vegaprotocol/environment', () => ({ - useEnvironment: () => ({ VEGA_ENV: 'test', SENTRY_DSN: 'sentry-dsn' }), + ...jest.requireActual('@vegaprotocol/environment'), + useEnvironment: jest.fn(() => ({ + VEGA_ENV: mockVegaEnv, + SENTRY_DSN: 'sentry-dsn', + })), })); describe('useTelemetryApproval', () => { @@ -21,32 +42,71 @@ describe('useTelemetryApproval', () => { jest.clearAllMocks(); }); - it('hook should return proper array', () => { + it('when empty hook should return proper array', () => { const { result } = renderHook(() => useTelemetryApproval()); - expect(result.current[0]).toEqual(false); + expect(result.current[0]).toEqual(''); expect(result.current[1]).toEqual(expect.any(Function)); + expect(result.current[2]).toEqual(true); + expect(result.current[3]).toEqual(expect.any(Function)); expect(useLocalStorage).toHaveBeenCalledWith(STORAGE_KEY); + expect(useLocalStorage).toHaveBeenCalledWith(STORAGE_SECOND_KEY); + expect(mockSetValue).toHaveBeenCalledWith('true'); + expect(mockSetSecondValue).not.toHaveBeenCalledWith('true'); + }); + + it('when approval not empty but viewed is empty should return proper array', () => { + mockStorageHookApprovalResult = ['false', mockSetValue]; + const { result } = renderHook(() => useTelemetryApproval()); + expect(result.current[0]).toEqual('false'); + expect(result.current[1]).toEqual(expect.any(Function)); + expect(result.current[2]).toEqual(true); + expect(result.current[3]).toEqual(expect.any(Function)); + expect(useLocalStorage).toHaveBeenCalledWith(STORAGE_KEY); + expect(useLocalStorage).toHaveBeenCalledWith(STORAGE_SECOND_KEY); + expect(mockSetValue).not.toHaveBeenCalled(); + expect(mockSetSecondValue).not.toHaveBeenCalled(); + }); + + it('when NOT empty hook should return proper array', () => { + mockStorageHookApprovalResult = ['false', mockSetValue]; + mockStorageHookViewedResult = ['true', mockSetSecondValue]; + const { result } = renderHook(() => useTelemetryApproval()); + expect(result.current[0]).toEqual('false'); + expect(result.current[1]).toEqual(expect.any(Function)); + expect(result.current[2]).toEqual(false); + expect(result.current[3]).toEqual(expect.any(Function)); + expect(useLocalStorage).toHaveBeenCalledWith(STORAGE_KEY); + expect(mockSetValue).not.toHaveBeenCalled(); + }); + + it('on mainnet hook should init properly', () => { + mockStorageHookApprovalResult = [null, mockSetValue]; + mockVegaEnv = Networks.MAINNET; + renderHook(() => useTelemetryApproval()); + expect(mockSetValue).toHaveBeenCalledWith('false'); }); it('hook should init stuff properly', async () => { const { result } = renderHook(() => useTelemetryApproval()); await act(() => { - result.current[1](true); + result.current[1]('true'); }); await waitFor(() => { expect(SentryInit).toHaveBeenCalled(); - expect(mockSetValue).toHaveBeenCalledWith('1'); + expect(mockSetValue).toHaveBeenCalledWith('true'); + expect(mockSetSecondValue).toHaveBeenCalledWith('true'); }); }); it('hook should close stuff properly', async () => { const { result } = renderHook(() => useTelemetryApproval()); await act(() => { - result.current[1](false); + result.current[1]('false'); }); await waitFor(() => { expect(SentryClose).toHaveBeenCalled(); - expect(mockRemoveValue).toHaveBeenCalledWith(); + expect(mockSetValue).toHaveBeenCalledWith('false'); + expect(mockSetSecondValue).toHaveBeenCalledWith('true'); }); }); }); diff --git a/apps/trading/lib/hooks/use-telemetry-approval.ts b/apps/trading/lib/hooks/use-telemetry-approval.ts index 25e0203b5..d235c9b35 100644 --- a/apps/trading/lib/hooks/use-telemetry-approval.ts +++ b/apps/trading/lib/hooks/use-telemetry-approval.ts @@ -1,25 +1,51 @@ import { useLocalStorage } from '@vegaprotocol/react-helpers'; -import { useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { SentryInit, SentryClose } from '@vegaprotocol/logger'; -import { useEnvironment } from '@vegaprotocol/environment'; +import { Networks, useEnvironment } from '@vegaprotocol/environment'; + export const STORAGE_KEY = 'vega_telemetry_approval'; +export const STORAGE_SECOND_KEY = 'vega_telemetry_viewed'; export const useTelemetryApproval = (): [ - value: boolean, - setValue: (value: boolean) => void + value: string, + setValue: (value: string) => void, + shouldOpen: boolean, + close: () => void ] => { const { VEGA_ENV, SENTRY_DSN } = useEnvironment(); - const [value, setValue, removeValue] = useLocalStorage(STORAGE_KEY); - const setApprove = useCallback( - (value: boolean) => { - if (value && SENTRY_DSN) { + const defaultTelemetryValue = + VEGA_ENV === Networks.MAINNET ? 'false' : 'true'; + const [value, setValue] = useLocalStorage(STORAGE_KEY); + const [viewedValue, setViewedValue] = useLocalStorage(STORAGE_SECOND_KEY); + const [shouldOpen, setShouldOpen] = useState(!value || !viewedValue); + const close = useCallback(() => { + setShouldOpen(false); + setViewedValue('true'); + }, [setViewedValue]); + const manageValue = useCallback( + (value: string) => { + if (value === 'true' && SENTRY_DSN) { SentryInit(SENTRY_DSN, VEGA_ENV); - return setValue('1'); + return setValue('true'); } SentryClose(); - removeValue(); + setValue('false'); }, - [setValue, removeValue, SENTRY_DSN, VEGA_ENV] + [setValue, SENTRY_DSN, VEGA_ENV] ); - return [Boolean(value), setApprove]; + const setTelemetryValue = useCallback( + (value: string) => { + setShouldOpen(false); + setViewedValue('true'); + manageValue(value); + }, + [manageValue, setViewedValue] + ); + useEffect(() => { + if (!value) { + manageValue(defaultTelemetryValue); + } + }, [value, manageValue, defaultTelemetryValue]); + + return [value || '', setTelemetryValue, shouldOpen, close]; }; diff --git a/apps/trading/stores/global.ts b/apps/trading/stores/global.ts index cbd56fd09..e83687eae 100644 --- a/apps/trading/stores/global.ts +++ b/apps/trading/stores/global.ts @@ -4,7 +4,6 @@ import produce from 'immer'; interface GlobalStore { marketId: string | null; - onBoardingDismissed: boolean; eagerConnecting: boolean; update: (store: Partial>) => void; } @@ -16,7 +15,6 @@ interface PageTitleStore { export const useGlobalStore = create()((set) => ({ marketId: LocalStorage.getItem('marketId') || null, - onBoardingDismissed: false, eagerConnecting: false, update: (newState) => { set( diff --git a/libs/assets/src/lib/asset-details-dialog.tsx b/libs/assets/src/lib/asset-details-dialog.tsx index f1bf66e39..1fb26fe48 100644 --- a/libs/assets/src/lib/asset-details-dialog.tsx +++ b/libs/assets/src/lib/asset-details-dialog.tsx @@ -97,7 +97,7 @@ export const AssetDetailsDialog = ({ }} > {content} -

+

{t( 'There is 1 unit of the settlement asset (%s) to every 1 quote unit.', [assetSymbol] diff --git a/libs/assets/src/lib/asset-details-table.tsx b/libs/assets/src/lib/asset-details-table.tsx index dcf179748..eca9a30a3 100644 --- a/libs/assets/src/lib/asset-details-table.tsx +++ b/libs/assets/src/lib/asset-details-table.tsx @@ -3,7 +3,11 @@ import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; import type * as Schema from '@vegaprotocol/types'; import type { KeyValueTableRowProps } from '@vegaprotocol/ui-toolkit'; -import { CopyWithTooltip, Icon } from '@vegaprotocol/ui-toolkit'; +import { + CopyWithTooltip, + Icon, + truncateMiddle, +} from '@vegaprotocol/ui-toolkit'; import { KeyValueTable, KeyValueTableRow, @@ -56,7 +60,7 @@ export const rows: Rows = [ key: AssetDetail.ID, label: t('ID'), tooltip: '', - value: (asset) => asset.id, + value: (asset) => truncateMiddle(asset.id), }, { key: AssetDetail.TYPE, @@ -109,7 +113,9 @@ export const rows: Rows = [ return ( <> - {' '} + + {truncateMiddle(asset.source.contractAddress)} + {' '}

diff --git a/libs/candles-chart/src/lib/candles-menu.spec.tsx b/libs/candles-chart/src/lib/candles-menu.spec.tsx index 7a9ed6f52..85f8b5b24 100644 --- a/libs/candles-chart/src/lib/candles-menu.spec.tsx +++ b/libs/candles-chart/src/lib/candles-menu.spec.tsx @@ -7,8 +7,8 @@ describe('CandlesMenu', () => { render(); await userEvent.click( - screen.getByText('Studies', { - selector: '[type="button"]', + screen.getByRole('button', { + name: 'Studies', }) ); expect(await screen.findByRole('menu')).toBeInTheDocument(); diff --git a/libs/candles-chart/src/lib/candles-menu.tsx b/libs/candles-chart/src/lib/candles-menu.tsx index 2b3209e0a..cc5b62e40 100644 --- a/libs/candles-chart/src/lib/candles-menu.tsx +++ b/libs/candles-chart/src/lib/candles-menu.tsx @@ -10,13 +10,14 @@ import { studyLabels, } from 'pennant'; import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuItemIndicator, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuTrigger, + TradingButton, + TradingDropdown, + TradingDropdownCheckboxItem, + TradingDropdownContent, + TradingDropdownItemIndicator, + TradingDropdownRadioGroup, + TradingDropdownRadioItem, + TradingDropdownTrigger, Icon, } from '@vegaprotocol/ui-toolkit'; import type { IconName } from '@blueprintjs/icons'; @@ -44,69 +45,76 @@ export const CandlesMenu = () => { } = useCandlesChartSettings(); const triggerClasses = 'text-xs'; const contentAlign = 'end'; + const triggerButtonProps = { size: 'extra-small' } as const; return ( <> - - {t(`Interval: ${intervalLabels[interval]}`)} - + + + {t(`Interval: ${intervalLabels[interval]}`)} + + } > - - + { setInterval(value as Interval); }} > {Object.values(Interval).map((timeInterval) => ( - {intervalLabels[timeInterval]} - - + + ))} - - - - + + + - - + + + + + } > - - + { setType(value as ChartType); }} > {Object.values(ChartType).map((type) => ( - + {chartTypeLabels[type]} - - + + ))} - - - - + + + - {t('Overlays')} - + + + {t('Overlays')} + + } > - + {Object.values(Overlay).map((overlay) => ( - { @@ -121,21 +129,23 @@ export const CandlesMenu = () => { }} > {overlayLabels[overlay]} - - + + ))} - - - + + - {t('Studies')} - + + + {t('Studies')} + + } > - + {Object.values(Study).map((study) => ( - { @@ -150,11 +160,11 @@ export const CandlesMenu = () => { }} > {studyLabels[study]} - - + + ))} - - + + ); }; diff --git a/libs/cypress/src/lib/commands/vega-wallet-connect.ts b/libs/cypress/src/lib/commands/vega-wallet-connect.ts index dba138df2..5ca985bdd 100644 --- a/libs/cypress/src/lib/commands/vega-wallet-connect.ts +++ b/libs/cypress/src/lib/commands/vega-wallet-connect.ts @@ -65,6 +65,8 @@ export function addSetVegaWallet() { Cypress.Commands.add('setVegaWallet', () => { cy.window().then((win) => { win.localStorage.setItem('vega_onboarding_viewed', 'true'); + win.localStorage.setItem('vega_telemetry_approval', 'false'); + win.localStorage.setItem('vega_telemetry_viewed', 'true'); win.localStorage.setItem( 'vega_wallet_config', JSON.stringify({ @@ -81,6 +83,8 @@ export function addSetOnBoardingViewed() { Cypress.Commands.add('setOnBoardingViewed', () => { cy.window().then((win) => { win.localStorage.setItem('vega_onboarding_viewed', 'true'); + win.localStorage.setItem('vega_telemetry_approval', 'false'); + win.localStorage.setItem('vega_telemetry_viewed', 'true'); }); }); } diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx index ce1762733..c15023da1 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-stop-order.spec.tsx @@ -432,9 +432,9 @@ describe('StopOrder', () => { }); it('sets expiry time/date to now if expiry is changed to checked', async () => { - const now = Math.round(Date.now() / 1000) * 1000; + const now = 24 * 60 * 60 * 1000; render(generateJsx()); - jest.spyOn(global.Date, 'now').mockImplementationOnce(() => now); + jest.spyOn(global.Date, 'now').mockImplementation(() => now); await userEvent.click(screen.getByTestId(expire)); // expiry time/date was empty it should be set to now 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 8de33126a..c672e93bc 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx @@ -21,6 +21,8 @@ import { import * as positionsTools from '@vegaprotocol/positions'; import { OrdersDocument } from '@vegaprotocol/orders'; import { formatForInput } from '@vegaprotocol/utils'; +import type { PartialDeep } from 'type-fest'; +import type { Market } from '@vegaprotocol/markets'; jest.mock('zustand'); jest.mock('./deal-ticket-fee-details', () => ({ @@ -36,12 +38,19 @@ const market = generateMarket(); const marketData = generateMarketData(); const submit = jest.fn(); -function generateJsx(mocks: MockedResponse[] = []) { +function generateJsx( + mocks: MockedResponse[] = [], + marketOverrides: PartialDeep = {} +) { + const joinedMarket: Market = { + ...market, + ...marketOverrides, + } as Market; return ( { expect(screen.getByTestId('iceberg')).toBeDisabled(); }); - // eslint-disable-next-line jest/no-disabled-tests it('handles TIF select box dependent on order type', async () => { render(generateJsx()); @@ -533,6 +541,25 @@ describe('DealTicket', () => { expect(screen.queryByTestId(priceErrorMessage)).toBeNull(); }); + it('validates size when positionDecimalPlaces is negative', async () => { + render(generateJsx([], { positionDecimalPlaces: -4 })); + const sizeErrorMessage = 'deal-ticket-error-message-size'; + const sizeInput = 'order-size'; + await userEvent.click(screen.getByTestId('place-order')); + // default value should be invalid + expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument(); + expect(screen.getByTestId(sizeErrorMessage)).toHaveTextContent( + 'Size cannot be lower than 10000' + ); + await userEvent.type(screen.getByTestId(sizeInput), '10001'); + expect(screen.getByTestId(sizeErrorMessage)).toHaveTextContent( + 'Size must be a multiple of 10000 for this market' + ); + await userEvent.clear(screen.getByTestId(sizeInput)); + await userEvent.type(screen.getByTestId(sizeInput), '10000'); + expect(screen.queryByTestId(sizeErrorMessage)).toBeNull(); + }); + it('validates iceberg field', async () => { const peakSizeErrorMessage = 'deal-ticket-peak-error-message'; const minimumSizeErrorMessage = 'deal-ticket-minimum-error-message'; @@ -590,9 +617,9 @@ describe('DealTicket', () => { it('sets expiry time/date to now if expiry is changed to checked', async () => { const datePicker = 'date-picker-field'; - const now = Math.round(Date.now() / 1000) * 1000; + const now = 24 * 60 * 60 * 1000; render(generateJsx()); - jest.spyOn(global.Date, 'now').mockImplementationOnce(() => now); + jest.spyOn(global.Date, 'now').mockImplementation(() => now); await userEvent.selectOptions( screen.getByTestId('order-tif'), Schema.OrderTimeInForce.TIME_IN_FORCE_GTT 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 8d6e1ee56..a2ffbb807 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -495,11 +495,12 @@ export const DealTicket = ({ onSelect={(value) => { // If GTT is selected and no expiresAt time is set, or its // behind current time then reset the value to current time + const now = Date.now(); if ( value === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && - (!expiresAt || new Date(expiresAt).getTime() < Date.now()) + (!expiresAt || new Date(expiresAt).getTime() < now) ) { - setValue('expiresAt', formatForInput(new Date()), { + setValue('expiresAt', formatForInput(new Date(now)), { shouldValidate: true, }); } diff --git a/libs/deposits/src/lib/approve-notification.tsx b/libs/deposits/src/lib/approve-notification.tsx index 1105e54e0..c1bac5c84 100644 --- a/libs/deposits/src/lib/approve-notification.tsx +++ b/libs/deposits/src/lib/approve-notification.tsx @@ -2,7 +2,11 @@ import type { Asset } from '@vegaprotocol/assets'; import { EtherscanLink } from '@vegaprotocol/environment'; import { t } from '@vegaprotocol/i18n'; import { Intent, Notification } from '@vegaprotocol/ui-toolkit'; -import { formatNumber } from '@vegaprotocol/utils'; +import { + formatNumber, + getUnlimitedThreshold, + quantumDecimalPlaces, +} from '@vegaprotocol/utils'; import type { EthStoredTxState } from '@vegaprotocol/web3'; import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3'; import BigNumber from 'bignumber.js'; @@ -188,6 +192,15 @@ const ApprovalTxFeedback = ({ } if (tx.status === EthTxStatus.Confirmed) { + const approvedAllowanceValue = ( + allowance || new BigNumber(0) + ).isGreaterThan(getUnlimitedThreshold(selectedAsset.decimals)) + ? '∞' + : formatNumber( + allowance?.toString() || 0, + quantumDecimalPlaces(selectedAsset.quantum, selectedAsset.decimals) + ); + return (
{t('You approved deposits of up to %s %s.', [ selectedAsset?.symbol, - formatNumber(allowance?.toString() || 0), + approvedAllowanceValue, ])}

{txLink &&

{txLink}

} diff --git a/libs/deposits/src/lib/deposit-form.spec.tsx b/libs/deposits/src/lib/deposit-form.spec.tsx index 726d61211..b6f226cc3 100644 --- a/libs/deposits/src/lib/deposit-form.spec.tsx +++ b/libs/deposits/src/lib/deposit-form.spec.tsx @@ -15,6 +15,7 @@ import { useWeb3ConnectStore } from '@vegaprotocol/web3'; import { useWeb3React } from '@web3-react/core'; import type { AssetFieldsFragment } from '@vegaprotocol/assets'; import type { DepositBalances } from './use-deposit-balances'; +import { truncateMiddle } from '@vegaprotocol/ui-toolkit'; jest.mock('@vegaprotocol/wallet'); jest.mock('@vegaprotocol/web3'); @@ -90,7 +91,7 @@ describe('Deposit form', () => { // Assert default values (including) from/to provided by useVegaWallet and useWeb3React expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument(); expect(screen.getByTestId('ethereum-address')).toHaveTextContent( - MOCK_ETH_ADDRESS + truncateMiddle(MOCK_ETH_ADDRESS) ); expect(screen.getByLabelText('Asset')).toHaveValue(''); expect(screen.getByLabelText('To (Vega key)')).toHaveValue(''); @@ -353,7 +354,7 @@ describe('Deposit form', () => { ).not.toBeInTheDocument(); expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument(); expect(screen.getByTestId('ethereum-address')).toHaveTextContent( - MOCK_ETH_ADDRESS + truncateMiddle(MOCK_ETH_ADDRESS) ); }); diff --git a/libs/deposits/src/lib/deposit-form.tsx b/libs/deposits/src/lib/deposit-form.tsx index 4dc0a8771..0044592c2 100644 --- a/libs/deposits/src/lib/deposit-form.tsx +++ b/libs/deposits/src/lib/deposit-form.tsx @@ -22,6 +22,7 @@ import { Intent, ButtonLink, TradingSelect, + truncateMiddle, } from '@vegaprotocol/ui-toolkit'; import { useVegaWallet } from '@vegaprotocol/wallet'; import { useWeb3React } from '@web3-react/core'; @@ -173,7 +174,7 @@ export const DepositForm = ({ return (

- {account} + {truncateMiddle(account)}

{ diff --git a/libs/deposits/src/lib/deposit-limits.tsx b/libs/deposits/src/lib/deposit-limits.tsx index a88beb027..e4054f23d 100644 --- a/libs/deposits/src/lib/deposit-limits.tsx +++ b/libs/deposits/src/lib/deposit-limits.tsx @@ -7,7 +7,7 @@ import { Tooltip, } from '@vegaprotocol/ui-toolkit'; import type BigNumber from 'bignumber.js'; -import { formatNumber } from '@vegaprotocol/utils'; +import { formatNumber, quantumDecimalPlaces } from '@vegaprotocol/utils'; // Note: all of the values here are with correct asset's decimals // See `libs/deposits/src/lib/use-deposit-balances.ts` @@ -35,7 +35,10 @@ export const DepositLimits = ({ label: t('Balance available'), rawValue: balance, value: balance ? ( - + ) : ( '-' ), @@ -73,7 +76,7 @@ export const DepositLimits = ({ value: !exempt ? ( ) : (
{t('Exempt')}
@@ -97,7 +100,10 @@ export const DepositLimits = ({ ), rawValue: allowance, value: allowance ? ( - + ) : ( '-' ), diff --git a/libs/fills/src/lib/fills-table.spec.tsx b/libs/fills/src/lib/fills-table.spec.tsx index f10924c97..570a1a5f8 100644 --- a/libs/fills/src/lib/fills-table.spec.tsx +++ b/libs/fills/src/lib/fills-table.spec.tsx @@ -4,7 +4,6 @@ import { getDateTimeFormat } from '@vegaprotocol/utils'; import * as Schema from '@vegaprotocol/types'; import type { PartialDeep } from 'type-fest'; import type { Trade } from './fills-data-provider'; - import { FillsTable, getFeesBreakdown } from './fills-table'; import { generateFill } from './test-helpers'; @@ -216,6 +215,27 @@ describe('FillsTable', () => { ).toBeInTheDocument(); }); + it('negative positionDecimalPoints should be properly rendered in size column', async () => { + const partyId = 'party-id'; + const negativeDecimalPositionFill = generateFill({ + ...defaultFill, + market: { + ...defaultFill.market, + positionDecimalPlaces: -4, + }, + }); + await act(async () => { + render( + + ); + }); + + const sizeCell = screen + .getAllByRole('gridcell') + .find((c) => c.getAttribute('col-id') === 'size'); + expect(sizeCell).toHaveTextContent('3,000,000,000'); + }); + describe('getFeesBreakdown', () => { it('should return correct fees breakdown for a taker', () => { const fees = { diff --git a/libs/market-depth/src/lib/orderbook-controls.tsx b/libs/market-depth/src/lib/orderbook-controls.tsx new file mode 100644 index 000000000..62c3f6b0a --- /dev/null +++ b/libs/market-depth/src/lib/orderbook-controls.tsx @@ -0,0 +1,109 @@ +import { useState } from 'react'; +import { + VegaIcon, + VegaIconNames, + TradingDropdown, + TradingDropdownTrigger, + TradingDropdownContent, + TradingDropdownItem, +} from '@vegaprotocol/ui-toolkit'; +import { formatNumberFixed } from '@vegaprotocol/utils'; + +export const OrderbookControls = ({ + lastTradedPrice, + resolution, + decimalPlaces, + setResolution, +}: { + lastTradedPrice: string | undefined; + resolution: number; + decimalPlaces: number; + setResolution: (resolution: number) => void; +}) => { + const [isOpen, setOpen] = useState(false); + + const resolutions = new Array( + Math.max(lastTradedPrice?.toString().length ?? 0, decimalPlaces + 1) + ) + .fill(null) + .map((v, i) => Math.pow(10, i)); + + const formatResolution = (r: number) => { + return formatNumberFixed( + Math.log10(r) - decimalPlaces > 0 + ? Math.pow(10, Math.log10(r) - decimalPlaces) + : 0, + decimalPlaces - Math.log10(r) + ); + }; + + const increaseResolution = () => { + const index = resolutions.indexOf(resolution); + if (index < resolutions.length - 1) { + setResolution(resolutions[index + 1]); + } + }; + + const decreaseResolution = () => { + const index = resolutions.indexOf(resolution); + if (index > 0) { + setResolution(resolutions[index - 1]); + } + }; + + return ( +
+ + setOpen(open)} + trigger={ + + + + } + > + + {resolutions.map((r) => ( + setResolution(r)}> + {formatResolution(r)} + + ))} + + + +
+ ); +}; diff --git a/libs/market-depth/src/lib/orderbook-data.spec.ts b/libs/market-depth/src/lib/orderbook-data.spec.ts index 871ca4145..389cc2919 100644 --- a/libs/market-depth/src/lib/orderbook-data.spec.ts +++ b/libs/market-depth/src/lib/orderbook-data.spec.ts @@ -31,21 +31,12 @@ describe('compactRows', () => { it('counts cumulative vol', () => { const asks = compactRows(sell, VolumeType.ask, 10); const bids = compactRows(buy, VolumeType.bid, 10); - expect(asks[0].cumulativeVol.value).toEqual(4950); - expect(bids[0].cumulativeVol.value).toEqual(579); - expect(asks[10].cumulativeVol.value).toEqual(390); - expect(bids[10].cumulativeVol.value).toEqual(4950); - expect(bids[bids.length - 1].cumulativeVol.value).toEqual(4950); - expect(asks[asks.length - 1].cumulativeVol.value).toEqual(390); - }); - - it('updates relative data', () => { - const asks = compactRows(sell, VolumeType.ask, 10); - const bids = compactRows(buy, VolumeType.bid, 10); - expect(asks[0].cumulativeVol.relativeValue).toEqual(100); - expect(bids[0].cumulativeVol.relativeValue).toEqual(12); - expect(asks[10].cumulativeVol.relativeValue).toEqual(8); - expect(bids[10].cumulativeVol.relativeValue).toEqual(100); + expect(asks[0].cumulativeVol).toEqual(4950); + expect(bids[0].cumulativeVol).toEqual(579); + expect(asks[10].cumulativeVol).toEqual(390); + expect(bids[10].cumulativeVol).toEqual(4950); + expect(bids[bids.length - 1].cumulativeVol).toEqual(4950); + expect(asks[asks.length - 1].cumulativeVol).toEqual(390); }); }); diff --git a/libs/market-depth/src/lib/orderbook-data.ts b/libs/market-depth/src/lib/orderbook-data.ts index 8d54ae2e2..31be1491a 100644 --- a/libs/market-depth/src/lib/orderbook-data.ts +++ b/libs/market-depth/src/lib/orderbook-data.ts @@ -5,15 +5,11 @@ export enum VolumeType { bid, ask, } -export interface CumulativeVol { - value: number; - relativeValue?: number; -} export interface OrderbookRowData { price: string; - value: number; - cumulativeVol: CumulativeVol; + volume: number; + cumulativeVol: number; } export const getPriceLevel = (price: string | bigint, resolution: number) => { @@ -26,25 +22,6 @@ export const getPriceLevel = (price: string | bigint, resolution: number) => { return priceLevel.toString(); }; -const getMaxVolumes = (orderbookData: OrderbookRowData[]) => ({ - cumulativeVol: Math.max( - orderbookData[0]?.cumulativeVol.value, - orderbookData[orderbookData.length - 1]?.cumulativeVol.value - ), -}); - -// round instead of ceil so we will not show 0 if value if different than 0 -const toPercentValue = (value?: number) => Math.ceil((value ?? 0) * 100); - -const updateRelativeData = (data: OrderbookRowData[]) => { - const { cumulativeVol } = getMaxVolumes(data); - data.forEach((data, i) => { - data.cumulativeVol.relativeValue = toPercentValue( - data.cumulativeVol.value / cumulativeVol - ); - }); -}; - const updateCumulativeVolumeByType = ( data: OrderbookRowData[], dataType: VolumeType @@ -53,14 +30,13 @@ const updateCumulativeVolumeByType = ( const maxIndex = data.length - 1; if (dataType === VolumeType.bid) { for (let i = 0; i <= maxIndex; i++) { - data[i].cumulativeVol.value = - data[i].value + (i !== 0 ? data[i - 1].cumulativeVol.value : 0); + data[i].cumulativeVol = + data[i].volume + (i !== 0 ? data[i - 1].cumulativeVol : 0); } } else { for (let i = maxIndex; i >= 0; i--) { - data[i].cumulativeVol.value = - data[i].value + - (i !== maxIndex ? data[i + 1].cumulativeVol.value : 0); + data[i].cumulativeVol = + data[i].volume + (i !== maxIndex ? data[i + 1].cumulativeVol : 0); } } } @@ -75,6 +51,7 @@ export const compactRows = ( getPriceLevel(row.price, resolution) ); const orderbookData: OrderbookRowData[] = []; + Object.keys(groupedByLevel).forEach((price) => { const { volume } = groupedByLevel[price].pop() as PriceLevelFieldsFragment; let value = Number(volume); @@ -83,7 +60,11 @@ export const compactRows = ( value += Number(subRow.volume); subRow = groupedByLevel[price].pop(); } - orderbookData.push({ price, value, cumulativeVol: { value: 0 } }); + orderbookData.push({ + price, + volume: value, + cumulativeVol: 0, + }); }); orderbookData.sort((a, b) => { @@ -95,8 +76,9 @@ export const compactRows = ( } return 1; }); + updateCumulativeVolumeByType(orderbookData, dataType); - updateRelativeData(orderbookData); + return orderbookData; }; @@ -140,7 +122,7 @@ export interface MockDataGeneratorParams { numberOfSellRows: number; numberOfBuyRows: number; overlap: number; - midPrice?: string; + lastTradedPrice: string; bestStaticBidPrice: number; bestStaticOfferPrice: number; } @@ -148,14 +130,14 @@ export interface MockDataGeneratorParams { export const generateMockData = ({ numberOfSellRows, numberOfBuyRows, - midPrice, + lastTradedPrice, overlap, bestStaticBidPrice, bestStaticOfferPrice, }: MockDataGeneratorParams) => { let matrix = new Array(numberOfSellRows).fill(undefined); let price = - Number(midPrice) + (numberOfSellRows - Math.ceil(overlap / 2) + 1); + Number(lastTradedPrice) + (numberOfSellRows - Math.ceil(overlap / 2) + 1); const sell: PriceLevelFieldsFragment[] = matrix.map((row, i) => ({ price: (price -= 1).toString(), volume: (numberOfSellRows - i + 1).toString(), @@ -171,7 +153,7 @@ export const generateMockData = ({ return { asks: sell, bids: buy, - midPrice, + lastTradedPrice, bestStaticBidPrice: bestStaticBidPrice.toString(), bestStaticOfferPrice: bestStaticOfferPrice.toString(), }; diff --git a/libs/market-depth/src/lib/orderbook-manager.tsx b/libs/market-depth/src/lib/orderbook-manager.tsx index 12f667871..95457e645 100644 --- a/libs/market-depth/src/lib/orderbook-manager.tsx +++ b/libs/market-depth/src/lib/orderbook-manager.tsx @@ -21,7 +21,7 @@ export type OrderbookData = { interface OrderbookManagerProps { marketId: string; - onClick?: (args: { price?: string; size?: string }) => void; + onClick: (args: { price?: string; size?: string }) => void; } export const OrderbookManager = ({ @@ -65,15 +65,17 @@ export const OrderbookManager = ({ data={data} reload={reload} > - + {market && marketData && ( + + )} ); }; diff --git a/libs/market-depth/src/lib/orderbook-row.tsx b/libs/market-depth/src/lib/orderbook-row.tsx index 293b46065..82cc238c1 100644 --- a/libs/market-depth/src/lib/orderbook-row.tsx +++ b/libs/market-depth/src/lib/orderbook-row.tsx @@ -1,158 +1,149 @@ -import React, { memo } from 'react'; +import type { ReactNode } from 'react'; +import { memo } from 'react'; import { addDecimal, addDecimalsFixedFormatNumber } from '@vegaprotocol/utils'; -import { NumericCell, PriceCell } from '@vegaprotocol/datagrid'; +import { NumericCell } from '@vegaprotocol/datagrid'; import { VolumeType } from './orderbook-data'; import classNames from 'classnames'; +const HIDE_VOL_WIDTH = 190; +const HIDE_CUMULATIVE_VOL_WIDTH = 260; + interface OrderbookRowProps { - value: number; - cumulativeValue?: number; - cumulativeRelativeValue?: number; + volume: number; + cumulativeVolume: number; decimalPlaces: number; positionDecimalPlaces: number; price: string; - onClick?: (args: { price?: string; size?: string }) => void; + onClick: (args: { price?: string; size?: string }) => void; type: VolumeType; width: number; + maxVol: number; } -const HIDE_VOL_WIDTH = 150; -const HIDE_CUMULATIVE_VOL_WIDTH = 220; - -const CumulationBar = ({ - cumulativeValue = 0, - type, -}: { - cumulativeValue?: number; - type: VolumeType; -}) => { - return ( -
- ); -}; - -const CumulativeVol = memo( +export const OrderbookRow = memo( ({ - testId, - positionDecimalPlaces, - cumulativeValue, - onClick, - }: { - ask?: number; - bid?: number; - cumulativeValue?: number; - testId?: string; - className?: string; - positionDecimalPlaces: number; - onClick?: (size?: string | number) => void; - }) => { - const volume = cumulativeValue ? ( - - ) : null; - - return onClick && volume ? ( - - ) : ( -
- {volume} -
- ); - } -); -CumulativeVol.displayName = 'OrderBookCumulativeVol'; - -export const OrderbookRow = React.memo( - ({ - value, - cumulativeValue, - cumulativeRelativeValue, + volume, + cumulativeVolume, decimalPlaces, positionDecimalPlaces, price, onClick, type, width, + maxVol, }: OrderbookRowProps) => { const txtId = type === VolumeType.bid ? 'bid' : 'ask'; const cols = width >= HIDE_CUMULATIVE_VOL_WIDTH ? 3 : width >= HIDE_VOL_WIDTH ? 2 : 1; return ( -
- +
+
- - onClick && onClick({ price: addDecimal(price, decimalPlaces) }) - } - valueFormatted={addDecimalsFixedFormatNumber(price, decimalPlaces)} - className={ - type === VolumeType.ask - ? 'text-market-red dark:text-market-red' - : 'text-market-green-600 dark:text-market-green' - } - /> - {width >= HIDE_VOL_WIDTH && ( - - onClick && - value && - onClick({ - size: addDecimal(value, positionDecimalPlaces), - }) - } - value={value} + onClick({ price: addDecimal(price, decimalPlaces) })} + > + + + {width >= HIDE_VOL_WIDTH && ( + + onClick({ size: addDecimal(volume, positionDecimalPlaces) }) + } + > + + )} {width >= HIDE_CUMULATIVE_VOL_WIDTH && ( - - onClick && - cumulativeValue && onClick({ - size: addDecimal(cumulativeValue, positionDecimalPlaces), + size: addDecimal(cumulativeVolume, positionDecimalPlaces), }) } - positionDecimalPlaces={positionDecimalPlaces} - cumulativeValue={cumulativeValue} - /> + > + + )}
); } ); + OrderbookRow.displayName = 'OrderbookRow'; + +const OrderBookRowCell = ({ + children, + onClick, +}: { + children: ReactNode; + onClick: () => void; +}) => { + return ( + + ); +}; + +const CumulationBar = ({ + cumulativeVolume = 0, + type, + maxVol, +}: { + cumulativeVolume: number; + type: VolumeType; + maxVol: number; +}) => { + const width = (cumulativeVolume / maxVol) * 100; + return ( +
+ ); +}; diff --git a/libs/market-depth/src/lib/orderbook.spec.tsx b/libs/market-depth/src/lib/orderbook.spec.tsx index 00d9b48e5..68972cd62 100644 --- a/libs/market-depth/src/lib/orderbook.spec.tsx +++ b/libs/market-depth/src/lib/orderbook.spec.tsx @@ -1,7 +1,7 @@ import { render, waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { generateMockData, VolumeType } from './orderbook-data'; -import { Orderbook } from './orderbook'; +import { Orderbook, OrderbookMid } from './orderbook'; import * as orderbookData from './orderbook-data'; function mockOffsetSize(width: number, height: number) { @@ -24,7 +24,7 @@ describe('Orderbook', () => { numberOfSellRows: 100, numberOfBuyRows: 100, step: 1, - midPrice: '122900', + lastTradedPrice: '122900', bestStaticBidPrice: 122905, bestStaticOfferPrice: 122895, decimalPlaces: 3, @@ -44,13 +44,14 @@ describe('Orderbook', () => { positionDecimalPlaces={0} {...generateMockData(params)} assetSymbol="USD" + onClick={jest.fn()} /> ); await waitFor(() => - screen.getByTestId(`middle-mark-price-${params.midPrice}`) + screen.getByTestId(`last-traded-${params.lastTradedPrice}`) ); expect( - screen.getByTestId(`middle-mark-price-${params.midPrice}`) + screen.getByTestId(`last-traded-${params.lastTradedPrice}`) ).toHaveTextContent('122.90'); }); @@ -68,10 +69,10 @@ describe('Orderbook', () => { /> ); expect( - await screen.findByTestId(`middle-mark-price-${params.midPrice}`) + await screen.findByTestId(`last-traded-${params.lastTradedPrice}`) ).toBeInTheDocument(); // Before resolution change the price is 122.934 - await userEvent.click(await screen.getByTestId('price-122901')); + await userEvent.click(screen.getByTestId('price-122901')); expect(onClickSpy).toBeCalledWith({ price: '122.901' }); await userEvent.click(screen.getByTestId('resolution')); @@ -92,7 +93,7 @@ describe('Orderbook', () => { VolumeType.ask, 10 ); - await userEvent.click(await screen.getByTestId('price-12294')); + await userEvent.click(screen.getByTestId('price-12294')); expect(onClickSpy).toBeCalledWith({ price: '122.94' }); }); @@ -177,3 +178,48 @@ describe('Orderbook', () => { }); }); }); + +describe('OrderbookMid', () => { + const props = { + lastTradedPrice: '100', + decimalPlaces: 0, + assetSymbol: 'BTC', + bestAskPrice: '101', + bestBidPrice: '99', + }; + + it('renders no change until lastTradedPrice changes', () => { + const { rerender } = render(); + expect(screen.getByTestId(/last-traded/)).toHaveTextContent( + props.lastTradedPrice + ); + expect(screen.getByText(props.assetSymbol)).toBeInTheDocument(); + expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument(); + expect(screen.getByTestId('spread')).toHaveTextContent('(2)'); + + // rerender with no change should not show the icon + rerender(); + expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument(); + + rerender( + + ); + expect(screen.getByTestId('icon-arrow-up')).toBeInTheDocument(); + expect(screen.getByTestId('spread')).toHaveTextContent('(3)'); + + // rerender again with the same price, should still be set to 'up' + rerender( + + ); + expect(screen.getByTestId('icon-arrow-up')).toBeInTheDocument(); + expect(screen.getByTestId('spread')).toHaveTextContent('(4)'); + + rerender(); + expect(screen.getByTestId('icon-arrow-down')).toBeInTheDocument(); + }); +}); diff --git a/libs/market-depth/src/lib/orderbook.stories.tsx b/libs/market-depth/src/lib/orderbook.stories.tsx index 84400cddf..7346ef6bd 100644 --- a/libs/market-depth/src/lib/orderbook.stories.tsx +++ b/libs/market-depth/src/lib/orderbook.stories.tsx @@ -9,9 +9,9 @@ type Props = Omit & { const OrderbookMockDataProvider = ({ decimalPlaces, ...props }: Props) => { return ( -
+
{ decimalPlaces={decimalPlaces} {...generateMockData({ ...props })} assetSymbol="USD" + onClick={() => undefined} />
diff --git a/libs/market-depth/src/lib/orderbook.tsx b/libs/market-depth/src/lib/orderbook.tsx index 9ccc0a7c0..145ddbb30 100644 --- a/libs/market-depth/src/lib/orderbook.tsx +++ b/libs/market-depth/src/lib/orderbook.tsx @@ -1,25 +1,15 @@ import { useMemo, useRef, useState } from 'react'; import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer'; -import { - addDecimalsFormatNumber, - formatNumberFixed, -} from '@vegaprotocol/utils'; +import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; import { usePrevious } from '@vegaprotocol/react-helpers'; import { OrderbookRow } from './orderbook-row'; import type { OrderbookRowData } from './orderbook-data'; import { compactRows, VolumeType } from './orderbook-data'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - Splash, - VegaIcon, - VegaIconNames, -} from '@vegaprotocol/ui-toolkit'; +import { Splash, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; import classNames from 'classnames'; import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth'; +import { OrderbookControls } from './orderbook-controls'; // Sets row height, will be used to calculate number of rows that can be // displayed each side of the book without overflow @@ -27,20 +17,7 @@ export const rowHeight = 17; const rowGap = 1; const midHeight = 30; -type PriceChange = 'up' | 'down' | 'none'; - -const PRICE_CHANGE_ICON_MAP: Readonly> = { - up: VegaIconNames.ARROW_UP, - down: VegaIconNames.ARROW_DOWN, - none: VegaIconNames.BULLET, -}; -const PRICE_CHANGE_CLASS_MAP: Readonly> = { - up: 'text-market-green-600 dark:text-market-green', - down: 'text-market-red dark:text-market-red', - none: 'text-vega-blue-500', -}; - -const OrderbookTable = ({ +const OrderbookSide = ({ rows, resolution, type, @@ -48,14 +25,16 @@ const OrderbookTable = ({ positionDecimalPlaces, onClick, width, + maxVol, }: { rows: OrderbookRowData[]; resolution: number; decimalPlaces: number; positionDecimalPlaces: number; type: VolumeType; - onClick?: (args: { price?: string; size?: string }) => void; + onClick: (args: { price?: string; size?: string }) => void; width: number; + maxVol: number; }) => { return (
))}
@@ -90,31 +69,88 @@ const OrderbookTable = ({ ); }; +export const OrderbookMid = ({ + lastTradedPrice, + decimalPlaces, + assetSymbol, + bestAskPrice, + bestBidPrice, +}: { + lastTradedPrice: string; + decimalPlaces: number; + assetSymbol: string; + bestAskPrice: string; + bestBidPrice: string; +}) => { + const previousLastTradedPrice = usePrevious(lastTradedPrice); + const priceChangeRef = useRef<'up' | 'down' | 'none'>('none'); + const spread = (BigInt(bestAskPrice) - BigInt(bestBidPrice)).toString(); + + if (previousLastTradedPrice !== lastTradedPrice) { + priceChangeRef.current = + Number(previousLastTradedPrice) > Number(lastTradedPrice) ? 'down' : 'up'; + } + + return ( +
+ {priceChangeRef.current !== 'none' && ( + + + + )} + + {addDecimalsFormatNumber(lastTradedPrice, decimalPlaces)} + + {assetSymbol} + + ({addDecimalsFormatNumber(spread, decimalPlaces)}) + +
+ ); +}; + interface OrderbookProps { decimalPlaces: number; positionDecimalPlaces: number; - onClick?: (args: { price?: string; size?: string }) => void; - midPrice?: string; + onClick: (args: { price?: string; size?: string }) => void; + lastTradedPrice: string; bids: PriceLevelFieldsFragment[]; asks: PriceLevelFieldsFragment[]; - assetSymbol: string | undefined; + assetSymbol: string; } export const Orderbook = ({ decimalPlaces, positionDecimalPlaces, onClick, - midPrice, + lastTradedPrice, asks, bids, assetSymbol, }: OrderbookProps) => { const [resolution, setResolution] = useState(1); - const resolutions = new Array( - Math.max(midPrice?.toString().length ?? 0, decimalPlaces + 1) - ) - .fill(null) - .map((v, i) => Math.pow(10, i)); const groupedAsks = useMemo(() => { return compactRows(asks, VolumeType.ask, resolution); @@ -123,44 +159,11 @@ export const Orderbook = ({ const groupedBids = useMemo(() => { return compactRows(bids, VolumeType.bid, resolution); }, [bids, resolution]); - const [isOpen, setOpen] = useState(false); - const previousMidPrice = usePrevious(midPrice); - const priceChangeRef = useRef<'up' | 'down' | 'none'>('none'); - if (midPrice && previousMidPrice !== midPrice) { - priceChangeRef.current = - (previousMidPrice || '') > midPrice ? 'down' : 'up'; - } - const priceChangeIcon = ( - - - - ); - - const formatResolution = (r: number) => { - return formatNumberFixed( - Math.log10(r) - decimalPlaces > 0 - ? Math.pow(10, Math.log10(r) - decimalPlaces) - : 0, - decimalPlaces - Math.log10(r) - ); - }; - - const increaseResolution = () => { - const index = resolutions.indexOf(resolution); - if (index < resolutions.length - 1) { - setResolution(resolutions[index + 1]); - } - }; - - const decreaseResolution = () => { - const index = resolutions.indexOf(resolution); - if (index > 0) { - setResolution(resolutions[index - 1]); - } - }; + // get the best bid/ask, note that we are using the pre aggregated + // values so we can render the most accurate spread in the mid section + const bestAskPrice = asks[0] ? asks[0].price : '0'; + const bestBidPrice = bids[0] ? bids[0].price : '0'; return (
@@ -171,21 +174,30 @@ export const Orderbook = ({ 1, Math.floor((height - midHeight) / 2 / (rowHeight + rowGap)) ); - const askRows = groupedAsks?.slice(limit * -1) ?? []; - const bidRows = groupedBids?.slice(0, limit) ?? []; + const askRows = groupedAsks.slice(limit * -1); + const bidRows = groupedBids.slice(0, limit); + + // this is used for providing a scale to render the volume + // bars based on the visible book + const deepestVisibleAsk = askRows[0]; + const deepestVisibleBid = bidRows[bidRows.length - 1]; + const maxVol = Math.max( + deepestVisibleAsk?.cumulativeVol || 0, + deepestVisibleBid?.cumulativeVol || 0 + ); return (
{askRows.length || bidRows.length ? ( <> - -
- {midPrice && ( - <> - - {addDecimalsFormatNumber(midPrice, decimalPlaces)} - - {assetSymbol} - {priceChangeIcon} - - )} -
- + ) : ( -
+
{t('No data')}
)} @@ -228,59 +235,13 @@ export const Orderbook = ({ }}
-
- - setOpen(open)} - trigger={ - formatResolution(item).length) - ) + 3 - }ch`, - }} - > - -
- {formatResolution(resolution)} -
-
- } - > - - {resolutions.map((r) => ( - setResolution(r)}> - {formatResolution(r)} - - ))} - -
- +
+
); diff --git a/libs/markets/src/lib/__generated__/market-data.ts b/libs/markets/src/lib/__generated__/market-data.ts index 2ce7201b7..385775ebc 100644 --- a/libs/markets/src/lib/__generated__/market-data.ts +++ b/libs/markets/src/lib/__generated__/market-data.ts @@ -3,23 +3,23 @@ import * as Types from '@vegaprotocol/types'; import { gql } from '@apollo/client'; import * as Apollo from '@apollo/client'; const defaultOptions = {} as const; -export type MarketDataUpdateFieldsFragment = { __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }; +export type MarketDataUpdateFieldsFragment = { __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }; export type MarketDataUpdateSubscriptionVariables = Types.Exact<{ marketId: Types.Scalars['ID']; }>; -export type MarketDataUpdateSubscription = { __typename?: 'Subscription', marketsData: Array<{ __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }> }; +export type MarketDataUpdateSubscription = { __typename?: 'Subscription', marketsData: Array<{ __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }> }; -export type MarketDataFieldsFragment = { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }; +export type MarketDataFieldsFragment = { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }; export type MarketDataQueryVariables = Types.Exact<{ marketId: Types.Scalars['ID']; }>; -export type MarketDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null } | null } }> } | null }; +export type MarketDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null } | null } }> } | null }; export const MarketDataUpdateFieldsFragmentDoc = gql` fragment MarketDataUpdateFields on ObservableMarketData { @@ -56,6 +56,7 @@ export const MarketDataUpdateFieldsFragmentDoc = gql` suppliedStake targetStake trigger + lastTradedPrice } `; export const MarketDataFieldsFragmentDoc = gql` @@ -95,6 +96,7 @@ export const MarketDataFieldsFragmentDoc = gql` suppliedStake targetStake trigger + lastTradedPrice } `; export const MarketDataUpdateDocument = gql` diff --git a/libs/markets/src/lib/market-data.graphql b/libs/markets/src/lib/market-data.graphql index 35272316b..fb760b16a 100644 --- a/libs/markets/src/lib/market-data.graphql +++ b/libs/markets/src/lib/market-data.graphql @@ -32,6 +32,7 @@ fragment MarketDataUpdateFields on ObservableMarketData { suppliedStake targetStake trigger + lastTradedPrice } subscription MarketDataUpdate($marketId: ID!) { @@ -76,6 +77,7 @@ fragment MarketDataFields on MarketData { suppliedStake targetStake trigger + lastTradedPrice } query MarketData($marketId: ID!) { diff --git a/libs/markets/src/lib/market-data.mock.ts b/libs/markets/src/lib/market-data.mock.ts index 496000768..d248acb92 100644 --- a/libs/markets/src/lib/market-data.mock.ts +++ b/libs/markets/src/lib/market-data.mock.ts @@ -62,6 +62,7 @@ const marketDataFields: MarketDataFieldsFragment = { markPrice: '4612690058', midPrice: '4612690000', openInterest: '0', + lastTradedPrice: '4612690000', priceMonitoringBounds: [ { minValidPrice: '654701', @@ -99,6 +100,7 @@ const marketDataUpdateFields: MarketDataUpdateFieldsFragment = { marketValueProxy: '', markPrice: '4612690058', midPrice: '0', + lastTradedPrice: '0', openInterest: '0', staticMidPrice: '0', trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED, diff --git a/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx b/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx index 4ada22fb6..1f3879aa1 100644 --- a/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx +++ b/libs/orders/src/lib/components/open-orders-menu/open-orders-menu.tsx @@ -1,5 +1,5 @@ import { t } from '@vegaprotocol/i18n'; -import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit'; +import { TradingButton } from '@vegaprotocol/ui-toolkit'; import { useVegaTransactionStore, useVegaWallet } from '@vegaprotocol/wallet'; import { useHasAmendableOrder } from '../../order-hooks'; @@ -28,12 +28,7 @@ export const OpenOrdersMenu = ({ marketId }: { marketId: string }) => { }; const CancelAllOrdersButton = ({ onClick }: { onClick: () => void }) => ( - + {t('Cancel all')} ); diff --git a/libs/orders/src/lib/components/order-list/order-list.spec.tsx b/libs/orders/src/lib/components/order-list/order-list.spec.tsx index 8dde7f8a9..5cfa49863 100644 --- a/libs/orders/src/lib/components/order-list/order-list.spec.tsx +++ b/libs/orders/src/lib/components/order-list/order-list.spec.tsx @@ -6,7 +6,7 @@ import type { PartialDeep } from 'type-fest'; import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet'; import { MockedProvider } from '@apollo/client/testing'; -import type { OrderFieldsFragment, OrderListTableProps } from '../'; +import type { Order, OrderFieldsFragment, OrderListTableProps } from '../'; import { OrderListTable } from '../'; import { generateOrder, @@ -164,6 +164,24 @@ describe('OrderListTable', () => { ); }); + it('negative positionDecimalPoints should be properly rendered in size column', async () => { + const localMarketOrder = { + ...marketOrder, + size: '3000', + market: { + ...marketOrder.market, + positionDecimalPlaces: -4, + }, + } as Order; + + await act(async () => { + render(generateJsx({ rowData: [localMarketOrder] })); + }); + + const cells = screen.getAllByRole('gridcell'); + expect(cells[2]).toHaveTextContent('+30,000,000'); + }); + describe('amend cell', () => { it('allows cancelling and editing for permitted orders', async () => { const mockEdit = jest.fn(); diff --git a/libs/positions/src/lib/positions-table.spec.tsx b/libs/positions/src/lib/positions-table.spec.tsx index 79fc39b6d..7dc85c5d1 100644 --- a/libs/positions/src/lib/positions-table.spec.tsx +++ b/libs/positions/src/lib/positions-table.spec.tsx @@ -201,6 +201,19 @@ describe('Positions', () => { ).not.toBeInTheDocument(); }); + it('handle negative positionDecimalPlaces', async () => { + await renderComponent({ + ...singleRow, + openVolume: '-2000', + positionDecimalPlaces: -4, + }); + const cells = screen.getAllByRole('gridcell'); + const cell = cells[1]; + expect(within(cell).getByTestId('stack-cell-primary')).toHaveTextContent( + '-20,000,000' + ); + }); + describe('PNLCell', () => { const props = { data: undefined, diff --git a/libs/proposals/src/components/protocol-upgrade-in-progress-notification.tsx b/libs/proposals/src/components/protocol-upgrade-in-progress-notification.tsx index a9227289f..3330abb2c 100644 --- a/libs/proposals/src/components/protocol-upgrade-in-progress-notification.tsx +++ b/libs/proposals/src/components/protocol-upgrade-in-progress-notification.tsx @@ -26,22 +26,36 @@ export const ProtocolUpgradeInProgressNotification = () => { const [nextUpgrade] = useLocalStorageSnapshot( NEXT_PROTOCOL_UPGRADE_PROPOSAL_SNAPSHOT ); - const { blocksRising, block } = useBlockRising(); const detailsLink = useProtocolUpgradeProposalLink(); let vegaReleaseTag: string | undefined; let upgradeBlockHeight: string | undefined; - if (error && !data && nextUpgrade && ALLOW_STORED_PROPOSAL_DATA) { + const hasData = data && !error; + const hasStoredData = nextUpgrade && ALLOW_STORED_PROPOSAL_DATA; + + if (hasData) { + // gets tag and height from the data api + vegaReleaseTag = data.vegaReleaseTag; + upgradeBlockHeight = data.upgradeBlockHeight; + } else if (hasStoredData) { + // gets tag and height from stored value if data api is unavailable try { const stored = JSON.parse(nextUpgrade) as StoredNextProtocolUpgradeData; vegaReleaseTag = stored.vegaReleaseTag; upgradeBlockHeight = stored.upgradeBlockHeight; } catch { - // no op + // NOOP - could not parse stored data } } + const hasUpgradeInfo = vegaReleaseTag && upgradeBlockHeight; + + const { blocksRising, block } = useBlockRising( + // skips querying blocks if there's no upgrade information available + !hasUpgradeInfo + ); + /** * If upgrade is in progress then none of the nodes should produce blocks, * same should be with the tendermint block info otherwise it's a network @@ -50,10 +64,7 @@ export const ProtocolUpgradeInProgressNotification = () => { * Once the networks is back then the notification disappears. */ const upgradeInProgress = - vegaReleaseTag && - upgradeBlockHeight && - !blocksRising && - block <= Number(upgradeBlockHeight); + hasUpgradeInfo && !blocksRising && block <= Number(upgradeBlockHeight); if (!upgradeInProgress) return null; diff --git a/libs/proposals/src/lib/protocol-upgrade-proposals/use-block-rising.ts b/libs/proposals/src/lib/protocol-upgrade-proposals/use-block-rising.ts index f02be60aa..60d7bdc9c 100644 --- a/libs/proposals/src/lib/protocol-upgrade-proposals/use-block-rising.ts +++ b/libs/proposals/src/lib/protocol-upgrade-proposals/use-block-rising.ts @@ -15,31 +15,33 @@ const CHECK_INTERVAL = 5000; // ms */ const ALLOW_STALE = 2; // times -> MAX(this, 1) * CHECK_INTERVAL ~> min check time -export const useBlockRising = () => { +export const useBlockRising = (skip = false) => { const [blocksRising, setBlocksRising] = useState(true); const [block, setBlock] = useState(0); const nodes = useEnvironment((state) => state.nodes); const clients = useMemo(() => { - return nodes.map( - (n) => - n && - n.length > 0 && - createClient({ + return nodes.map((n) => { + if (n && n.length > 0) { + const client = createClient({ url: n, cacheConfig: undefined, retry: false, connectToDevTools: false, connectToHeaderStore: true, - }) - ); + }); + return client; + } + return undefined; + }); }, [nodes]); const { refetch: fetchBlockInfo } = useBlockInfo(); useEffect(() => { + if (skip) return; let stale = 0; let prev = 0; const check = async () => { - const queries = clients.map((client, index) => + const queries = clients.map((client) => client ? client .query({ @@ -47,18 +49,16 @@ export const useBlockRising = () => { fetchPolicy: 'network-only', errorPolicy: 'ignore', }) - .catch((err) => - Promise.reject( - `could not retrieve statistics from ${nodes[index]}` - ) - ) + .catch(() => { + // NOOP - could not retrieve statistics for that node (network error) + }) : undefined ); const blockInfo = await fetchBlockInfo(); const results = (await Promise.allSettled(compact(queries))).map( (res) => { - if (res && res.status === 'fulfilled') { + if (res && res.status === 'fulfilled' && res.value) { return res.value.data.statistics; } else { return undefined; @@ -86,7 +86,7 @@ export const useBlockRising = () => { return () => { clearInterval(interval); }; - }, [clients, fetchBlockInfo, blocksRising, nodes]); + }, [clients, fetchBlockInfo, blocksRising, nodes, skip]); return { blocksRising, block }; }; diff --git a/libs/react-helpers/src/lib/format/number.tsx b/libs/react-helpers/src/lib/format/number.tsx index a16c94551..299d03c52 100644 --- a/libs/react-helpers/src/lib/format/number.tsx +++ b/libs/react-helpers/src/lib/format/number.tsx @@ -1,9 +1,14 @@ import { BigNumber } from 'bignumber.js'; -import { getUserLocale, formatNumber } from '@vegaprotocol/utils'; +import { + getUserLocale, + formatNumber, + getUnlimitedThreshold, +} from '@vegaprotocol/utils'; const INFINITY = '∞'; const DEFAULT_COMPACT_ABOVE = 1_000_000; const DEFAULT_COMPACT_CAP = new BigNumber(1e24); + /** * Compacts given number to human readable format. * @param number @@ -35,6 +40,10 @@ export const CompactNumber = ({ const decimalPlaces = (decimals === 'infer' ? number.decimalPlaces() : decimals) || 0; + if (number.isGreaterThan(getUnlimitedThreshold(decimalPlaces))) { + return {INFINITY}; + } + if (number.isLessThan(DEFAULT_COMPACT_ABOVE)) { return ( {formatNumber(number, decimalPlaces)} diff --git a/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-eye-off.tsx b/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-eye-off.tsx new file mode 100644 index 000000000..31aae4fc8 --- /dev/null +++ b/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-eye-off.tsx @@ -0,0 +1,7 @@ +export const IconEyeOff = ({ size = 16 }: { size: number }) => { + return ( + + + + ); +}; diff --git a/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon-record.ts b/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon-record.ts index e8069b90f..753984075 100644 --- a/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon-record.ts +++ b/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon-record.ts @@ -15,6 +15,7 @@ import { IconDeposit } from './svg-icons/icon-deposit'; import { IconEdit } from './svg-icons/icon-edit'; import { IconExclaimationMark } from './svg-icons/icon-exclaimation-mark'; import { IconEye } from './svg-icons/icon-eye'; +import { IconEyeOff } from './svg-icons/icon-eye-off'; import { IconForum } from './svg-icons/icon-forum'; import { IconGlobe } from './svg-icons/icon-globe'; import { IconInfo } from './svg-icons/icon-info'; @@ -55,6 +56,7 @@ export enum VegaIconNames { EDIT = 'edit', EXCLAIMATION_MARK = 'exclaimation-mark', EYE = 'eye', + EYE_OFF = 'eye-off', FORUM = 'forum', GLOBE = 'globe', INFO = 'info', @@ -90,6 +92,7 @@ export const VegaIconNameMap: Record< 'chevron-down': IconChevronDown, 'chevron-left': IconChevronLeft, 'chevron-up': IconChevronUp, + 'eye-off': IconEyeOff, 'exclaimation-mark': IconExclaimationMark, 'open-external': IconOpenExternal, 'question-mark': IconQuestionMark, diff --git a/libs/utils/src/lib/format/number.spec.ts b/libs/utils/src/lib/format/number.spec.ts index 8ff305247..119087fca 100644 --- a/libs/utils/src/lib/format/number.spec.ts +++ b/libs/utils/src/lib/format/number.spec.ts @@ -5,7 +5,11 @@ import { addDecimalsFormatNumberQuantum, formatNumber, formatNumberPercentage, + getUnlimitedThreshold, isNumeric, + removeDecimal, + toBigNum, + quantumDecimalPlaces, toDecimal, toNumberParts, } from './number'; @@ -151,8 +155,94 @@ describe('number utils', () => { { v: 7, o: '0.0000001' }, { v: 8, o: '0.00000001' }, { v: 9, o: '0.000000001' }, + { v: -1, o: '10' }, + { v: -2, o: '100' }, + { v: -3, o: '1000' }, ])('formats with toNumber given number correctly', ({ v, o }) => { expect(toDecimal(v)).toStrictEqual(o); }); }); + + describe('positive and negative decimals should be handled correctly', () => { + const baseNum = '2000'; + const methods = [removeDecimal, toBigNum]; + it.each([ + { decimals: 0, result: ['2000', '2000'] }, + { decimals: 1, result: ['20000', '200'] }, + { decimals: -1, result: ['200', '20000'] }, + { decimals: 2, result: ['200000', '20'] }, + { decimals: -2, result: ['20', '200000'] }, + { decimals: 3, result: ['2000000', '2'] }, + { decimals: -3, result: ['2', '2000000'] }, + { decimals: 4, result: ['20000000', '0.2'] }, + { decimals: -4, result: ['0', '20000000'] }, // removeDecimal has toFixed(0) at the end + ])( + 'number methods should handle negative decimals', + ({ decimals, result }) => { + methods.forEach((method, i) => { + expect(method(baseNum, decimals).toString()).toEqual(result[i]); + }); + } + ); + }); +}); + +describe('quantumDecimalPlaces', () => { + it.each([ + ['1', 1, 3], + ['10', 1, 2], + ['100', 1, 1], + ['1000', 1, 0], + ['1', 2, 4], + ['10', 2, 3], + ['100', 2, 2], + ['1000', 2, 1], + ['1', 3, 5], + ['10', 3, 4], + ['100', 3, 3], + ['1000', 3, 2], + ['1', 18, 20], + ['1000000000', 18, 11], + ['5000000000', 18, 11], + ['1000000000000000000', 18, 2], + ])( + 'converts quantum %s of %d decimal places to %d quant. decimal places', + (quantum, decimals, output) => { + expect(quantumDecimalPlaces(quantum, decimals)).toEqual(output); + } + ); +}); + +describe('getUnlimitedThreshold', () => { + it.each([ + [ + 0, + '9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+76', + ], + [ + 1, + '9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+75', + ], + [ + 2, + '9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+74', + ], + [ + 3, + '9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+73', + ], + [ + 10, + '9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+66', + ], + [ + 18, + '9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+58', + ], + ])( + 'given %d decimal places it returns unlimited threshold %s', + (decimals, output) => { + expect(getUnlimitedThreshold(decimals).toString()).toEqual(output); + } + ); }); diff --git a/libs/utils/src/lib/format/number.ts b/libs/utils/src/lib/format/number.ts index d3a8a2382..bf5a93ff4 100644 --- a/libs/utils/src/lib/format/number.ts +++ b/libs/utils/src/lib/format/number.ts @@ -4,12 +4,24 @@ import memoize from 'lodash/memoize'; import { getUserLocale } from '../get-user-locale'; +/** + * A raw unformatted value greater than this is considered and displayed + * as UNLIMITED. + */ +export const UNLIMITED_THRESHOLD = new BigNumber(2).pow(256).times(0.8); +/** + * Gets the unlimited threshold value for given decimal places. + * @param decimalPlaces the asset's decimal places + */ +export const getUnlimitedThreshold = (decimalPlaces: number) => + UNLIMITED_THRESHOLD.dividedBy(Math.pow(10, decimalPlaces)); + const MIN_FRACTION_DIGITS = 2; const MAX_FRACTION_DIGITS = 20; export function toDecimal(numberOfDecimals: number) { return new BigNumber(1) - .dividedBy(Math.pow(10, numberOfDecimals)) + .dividedBy(new BigNumber(10).exponentiatedBy(numberOfDecimals)) .toString(10); } @@ -17,7 +29,8 @@ export function toBigNum( rawValue: string | number, decimals: number ): BigNumber { - return new BigNumber(rawValue || 0).dividedBy(Math.pow(10, decimals)); + const divides = new BigNumber(10).exponentiatedBy(decimals); + return new BigNumber(rawValue || 0).dividedBy(divides); } export function addDecimal( @@ -36,7 +49,8 @@ export function removeDecimal( value: string | BigNumber, decimals: number ): string { - return new BigNumber(value || 0).times(Math.pow(10, decimals)).toFixed(0); + const times = new BigNumber(10).exponentiatedBy(decimals); + return new BigNumber(value || 0).times(times).toFixed(0); } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat @@ -90,6 +104,25 @@ export const formatNumberFixed = ( return getFixedNumberFormat(formatDecimals).format(Number(rawValue)); }; +export const quantumDecimalPlaces = ( + /** Raw asset's quantum value */ + rawQuantum: number | string, + /** Asset's decimal places */ + decimalPlaces: number +) => { + // if raw quantum value is an empty string then it'll evaluate to 0 + // this check ignores NaNs and zeroes + const formatDecimals = + isNaN(Number(rawQuantum)) || Number(rawQuantum) === 0 + ? decimalPlaces + : Math.max( + 0, + Math.log10(100 / Number(addDecimal(rawQuantum, decimalPlaces))) + ); + + return Math.ceil(formatDecimals); +}; + export const addDecimalsFormatNumberQuantum = ( rawValue: string | number, decimalPlaces: number, diff --git a/libs/utils/src/lib/validate/validate-amount.ts b/libs/utils/src/lib/validate/validate-amount.ts index ef45d3bba..95549ab1f 100644 --- a/libs/utils/src/lib/validate/validate-amount.ts +++ b/libs/utils/src/lib/validate/validate-amount.ts @@ -4,6 +4,12 @@ export const validateAmount = (step: number | string, field: string) => { const [, stepDecimals = ''] = String(step).split('.'); return (value?: string) => { + if (Number(step) > 1) { + if (Number(value) % Number(step) > 0) { + return t(`${field} must be a multiple of ${step} for this market`); + } + return true; + } const [, valueDecimals = ''] = (value || '').split('.'); if (stepDecimals.length < valueDecimals.length) { if (stepDecimals === '') { diff --git a/libs/wallet/src/connect-dialog/connect-dialog.tsx b/libs/wallet/src/connect-dialog/connect-dialog.tsx index 7aa5c32e4..e3c0ed4dc 100644 --- a/libs/wallet/src/connect-dialog/connect-dialog.tsx +++ b/libs/wallet/src/connect-dialog/connect-dialog.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames'; import { Dialog, + ExternalLink, Intent, Pill, TradingButton, @@ -39,7 +40,7 @@ import { useVegaWallet } from '../use-vega-wallet'; import { InjectedConnectorForm } from './injected-connector-form'; import { isBrowserWalletInstalled } from '../utils'; import { useIsWalletServiceRunning } from '../use-is-wallet-service-running'; -import { useIsSnapRunning } from '../use-is-snap-running'; +import { SnapStatus, useSnapStatus } from '../use-snap-status'; import { useVegaWalletDialogStore } from './vega-wallet-dialog-store'; export const CLOSE_DELAY = 1700; @@ -158,7 +159,7 @@ const ConnectDialogContainer = ({ appChainId ); - const isSnapRunning = useIsSnapRunning( + const snapStatus = useSnapStatus( DEFAULT_SNAP_ID, Boolean(connectors['snap']) ); @@ -183,7 +184,7 @@ const ConnectDialogContainer = ({ setWalletUrl={setWalletUrl} onSelect={handleSelect} isDesktopWalletRunning={isDesktopWalletRunning} - isSnapRunning={isSnapRunning} + snapStatus={snapStatus} /> )} @@ -198,14 +199,14 @@ const ConnectorList = ({ walletUrl, setWalletUrl, isDesktopWalletRunning, - isSnapRunning, + snapStatus, }: { connectors: Connectors; onSelect: (type: WalletType) => void; walletUrl: string; setWalletUrl: (value: string) => void; isDesktopWalletRunning: boolean | null; - isSnapRunning: boolean | null; + snapStatus: SnapStatus; }) => { const { pubKey, links } = useVegaWallet(); const title = isBrowserWalletInstalled() @@ -249,7 +250,7 @@ const ConnectorList = ({
{connectors['snap'] !== undefined ? (
- {isSnapRunning ? ( + {snapStatus === SnapStatus.INSTALLED ? ( ) : ( - -
- {t('Install Vega MetaMask Snap')} -
-
- -
- - } - onClick={() => { - requestSnap(DEFAULT_SNAP_ID); - }} - /> + <> + +
+ {t('Install Vega MetaMask Snap')} +
+
+ +
+ + } + onClick={() => { + requestSnap(DEFAULT_SNAP_ID); + }} + /> + {snapStatus === SnapStatus.NOT_SUPPORTED ? ( +

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

+ ) : null} + )}
) : null} diff --git a/libs/wallet/src/connectors/snap-connector.ts b/libs/wallet/src/connectors/snap-connector.ts index ede6fa70e..861967a09 100644 --- a/libs/wallet/src/connectors/snap-connector.ts +++ b/libs/wallet/src/connectors/snap-connector.ts @@ -118,14 +118,10 @@ export const getSnap = async ( snapId: string, version?: string ): Promise => { - try { - const snaps = await getSnaps(); - return Object.values(snaps).find( - (snap) => snap.id === snapId && (!version || snap.version === version) - ); - } catch (e) { - return undefined; - } + const snaps = await getSnaps(); + return Object.values(snaps).find( + (snap) => snap.id === snapId && (!version || snap.version === version) + ); }; export const invokeSnap = async ( diff --git a/libs/wallet/src/use-is-snap-running.ts b/libs/wallet/src/use-is-snap-running.ts deleted file mode 100644 index ec44e2a36..000000000 --- a/libs/wallet/src/use-is-snap-running.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useState } from 'react'; -import { getSnap } from './connectors'; - -const INTERVAL = 2_000; - -export const useIsSnapRunning = (snapId: string, shouldCheck: boolean) => { - const [running, setRunning] = useState(false); - useEffect(() => { - if (!shouldCheck) return; - - const checkState = async () => { - const snap = await getSnap(snapId); - setRunning(!!snap); - }; - - const i = setInterval(() => { - checkState(); - }, INTERVAL); - - checkState(); - - return () => { - clearInterval(i); - }; - }, [snapId, shouldCheck]); - return running; -}; diff --git a/libs/wallet/src/use-snap-status.ts b/libs/wallet/src/use-snap-status.ts new file mode 100644 index 000000000..87c267aa9 --- /dev/null +++ b/libs/wallet/src/use-snap-status.ts @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; +import { getSnap } from './connectors'; + +const INTERVAL = 2_000; + +export enum SnapStatus { + NOT_SUPPORTED, + INSTALLED, + NOT_INSTALLED, +} + +export const useSnapStatus = (snapId: string, shouldCheck: boolean) => { + const [status, setStatus] = useState(SnapStatus.NOT_INSTALLED); + useEffect(() => { + if (!shouldCheck) return; + + const checkState = async () => { + try { + const snap = await getSnap(snapId); + setStatus(snap ? SnapStatus.INSTALLED : SnapStatus.NOT_INSTALLED); + } catch (err) { + setStatus(SnapStatus.NOT_SUPPORTED); + } + }; + + const i = setInterval(() => { + checkState(); + }, INTERVAL); + + checkState(); + + return () => { + clearInterval(i); + }; + }, [snapId, shouldCheck]); + return status; +}; diff --git a/specs/0002-WCON-connect_vega_wallet.md b/specs/0002-WCON-connect_vega_wallet.md index d430e34bd..68e110541 100644 --- a/specs/0002-WCON-connect_vega_wallet.md +++ b/specs/0002-WCON-connect_vega_wallet.md @@ -13,6 +13,8 @@ When looking to use Vega via a user interface e.g. Dapp (Decentralized web App), - If there is not running desktop wallet or CLI detected, I can see that I need to open my wallet. (0002-WCON-0013) - I can find out more about supported browsers i.e. there is a link (Issue "List compatible browsers" vegawallet-browser#360 has to be implemented). (0002-WCON-0013) - I can find out more about the Vega Wallet and see what "other" versions there are i.e. there is a link to the page on the website (currently - https://vega.xyz/wallet#overview). (0002-WCON-0014) +- I can install and connect vega metamask snap wallet (0002-WCON-0015) + - Browser wallet: - The browser extension you need is automatically detected if you are using Chrome or Firefox, presenting the specific call to action to install that browser extension in a visible way e.g. with a Chrome or Firefox icon. (0002-WCON-041) diff --git a/specs/0007-FUGS-first-use-get-started.md b/specs/0007-FUGS-first-use-get-started.md index 10c87a581..374782e50 100644 --- a/specs/0007-FUGS-first-use-get-started.md +++ b/specs/0007-FUGS-first-use-get-started.md @@ -23,13 +23,16 @@ - **Must** There is a link to try out trading on Fairground when I'm on Mainnet (0007-FUGS-008) - **Must** There is a link to trade with real funds on Mainnet when I am on Fairground (0007-FUGS-010) - **Must** When I am on the Fairground version, I can see a warning / call out that this is Fairground meaning I can try out with virtual assets at no risk (0007-FUGS-011) -- If I dismiss the popup, I **must** not see it unless I NOT accomplish full "onboarding" +- If I dismiss the popup, I **must** not see it anymore (0007-FUGS-018) - If I dismiss the popup, I land on the default market (0007-FUGS-012) ## When the popup has been dismissed: - **Must** I can see the steps to get started with a visible call to action (according to my progress) in the context of the deal ticket, deposit, withdraw, transfer components in the sidebar (0007-FUGS-013) -- **Must** Remove buttons from pane containers that prompt to connect wallet (0007-FUGS-014) -- **Must** We've replaced "connect wallet" in the top right with "get started" (0007-FUGS-015) -- **Must** When I press the get started CTA, I see the wallet connect popup (0007-FUGS-016) -- **Must** If I have a wallet installed already I don't see this quick start onboarding, and instead call(s) to action in Console revert to connect wallet, not "get started" (button in nav header) (0007-FUGS-017) +- **Must** We've replaced "connect wallet" in the top right with "get started" (0007-FUGS-014) +- **Must** When I press the get started CTA, I see the wallet connect popup (0007-FUGS-015) +- **Must** If I have a wallet installed already I don't see this quick start onboarding, and instead call(s) to action in Console revert to connect wallet, not "get started" (button in nav header) (0007-FUGS-016) + +## When onboarding process has been accomplished: + +- I can see telemetry approval toast: on environment other than mainnet telemetry is enabled by default (0007-FUGS-018) diff --git a/specs/6003-ORDB-order_book.md b/specs/6003-ORDB-order_book.md index 56acdc4a2..de2a87823 100644 --- a/specs/6003-ORDB-order_book.md +++ b/specs/6003-ORDB-order_book.md @@ -11,4 +11,7 @@ As a market user I want to see information about orders existing in the market. - I **Must** see prices sorted descending (6003-ORDB-007) - I **Must** be able to set a resolution of data (6003-ORDB-008) - When I click specific price, it **Must** be copied to deal ticket form (6003-ORDB-009) -- Order is removed from orderbook if traded away(6003-ORDB-010) +- Order is removed from orderbook if traded away (6003-ORDB-010) +- Spread (bestAsk - bestOffer) is show in the mid secion (6003-ORDB-011) +- Cumulative volume is displayed visually (volume bars) as a proportion of the entire book volume (6003-ORDB-012) +- Mid section shows the last traded price movement using an arrow (6003-ORDB-013) diff --git a/specs/7002-SORD-submit_orders.md b/specs/7002-SORD-submit_orders.md index c5e41c202..733388f67 100644 --- a/specs/7002-SORD-submit_orders.md +++ b/specs/7002-SORD-submit_orders.md @@ -174,11 +174,34 @@ When populating a deal ticket I... - **must** see that selecting "Post only" or "reduce only" de-select the other (if selected). As is it not possibly to apply them both to an order (7003-SORD-059) - **must** see an option to select expire for stop order limit and market (7002-SORD-071) + - **must** select strategy for stop order limit and market (7002-SORD-072) - **must** see strategy submit (7002-SORD-073) + if it is oco then submit strategy is not available (7002-SORD-081) - **must** see strategy cancel (7002-SORD-074) - **must** see expiry time/date (7002-SORD-075) +- **must** see an option to select oco for stop order limit and market (7002-SORD-082) + - **must** see a type to select market or limit (7002-SORD-083) + - **must** see for market type + - **must** see rises above (7002-SORD-084) + - **must** see falls below (7002-SORD-085) + - **must** enter a trigger value (7002-SORD-086) + - **must** select a trigger price (7002-SORD-087) + - **must** select a trigger trailing percent offset (7002-SORD-088) + - **must** see for limit type + - **must** enter a price (7002-SORD-089) + - **must** select a trigger direction (7002-SORD-090) + - **must** see rises above (7002-SORD-099) + - **must** see falls below (7002-SORD-091) + - if the user has not set a preference: trigger direction **must** default to `opposite to the one selected in the stop order` (7002-SORD-092) + - **must** enter a trigger value (7002-SORD-093) + - **must** select a trigger type (7002-SORD-094) + - **must** see a trigger price (7002-SORD-095) + - **must** see a trigger trailing percent offset (7002-SORD-096) + - if the user has not set a preference: trigger type **must** default to `price` (7002-SORD-097) + - if the trigger price is already met on submission, **must** warn the user that the stop order will be triggered immediately (7002-SORD-098) + ... so I can control in more detail how my order is executed ## Auto Populating a deal ticket non-manual methods diff --git a/vegacapsule/config.hcl b/vegacapsule/config.hcl index 7aca9f010..9559d2cc7 100644 --- a/vegacapsule/config.hcl +++ b/vegacapsule/config.hcl @@ -37,14 +37,11 @@ EOT auth_soft_fail = true } docker_service "postgres-1" { - image = "vegaprotocol/timescaledb:2.8.0-pg14" + # Read me for more details: https://github.com/vegaprotocol/docker/tree/main/timescaledb#timescaledb + image = "vegaprotocol/vegacapsule-timescaledb:2.8.0-pg14-v0.0.1" cmd = "postgres" args = [] - env = { - POSTGRES_USER="vega" - POSTGRES_PASSWORD="vega" - POSTGRES_DBS="vega0,vega1,vega2,vega3,vega4,vega5,vega6,vega7,vega8,vega9,vega10,vega11,vega12,vega13,vega14,vega15,vega16,vega17,vega18,vega19,vega20,vega21,vega22,vega23,vega24,vega25" - } + env = {} static_port { value = 5232