Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a20b0d03f2 | ||
|
|
b5ba4f99d9 | ||
|
|
66f25603d3 | ||
|
|
4afd469404 | ||
|
|
9e5bc9c8d1 | ||
|
|
e41aff88b1 | ||
|
|
9209074332 | ||
|
|
39e5836edb | ||
|
|
85a4981700 | ||
|
|
247927e939 | ||
|
|
6523490d96 | ||
|
|
105a758e8d | ||
|
|
559ef48d6d | ||
|
|
255c3752f2 | ||
|
|
2bbf1e2b81 |
@@ -10,7 +10,7 @@ on:
|
||||
jobs:
|
||||
run-tests:
|
||||
name: run-tests
|
||||
runs-on: 8-cores
|
||||
runs-on: console-test
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
#----------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
# Lint all staged files
|
||||
yarn lint-staged
|
||||
Executable
+8
@@ -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
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,7 +38,12 @@ const Oracles = () => {
|
||||
const dataConnection = o?.node.dataConnection;
|
||||
|
||||
return (
|
||||
<div id={id} key={id} className="mb-10">
|
||||
<div
|
||||
id={id}
|
||||
key={id}
|
||||
className="mb-10"
|
||||
data-testid="oracle-details"
|
||||
>
|
||||
<OracleDetails
|
||||
id={id}
|
||||
dataSource={o?.node}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const WalletCardRow = ({
|
||||
}) => {
|
||||
const ref = React.useRef<HTMLDivElement | null>(null);
|
||||
useAnimateValue(ref, value);
|
||||
const [integers, decimalsPlaces] = useNumberParts(value, decimals);
|
||||
const [integers, decimalsPlaces, separator] = useNumberParts(value, decimals);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -75,7 +75,10 @@ export const WalletCardRow = ({
|
||||
className="font-mono flex-1 text-right"
|
||||
data-testid="associated-amount"
|
||||
>
|
||||
<span>{integers}.</span>
|
||||
<span>
|
||||
{integers}
|
||||
{separator}
|
||||
</span>
|
||||
<span>{decimalsPlaces}</span>
|
||||
</span>
|
||||
)}
|
||||
@@ -110,7 +113,10 @@ export const WalletCardAsset = ({
|
||||
border,
|
||||
subheading,
|
||||
}: WalletCardAssetProps) => {
|
||||
const [integers, decimalsPlaces] = useNumberParts(balance, decimals);
|
||||
const [integers, decimalsPlaces, separator] = useNumberParts(
|
||||
balance,
|
||||
decimals
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-nowrap mt-2 mb-4">
|
||||
@@ -132,7 +138,10 @@ export const WalletCardAsset = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 basis-full font-mono" data-testid="currency-value">
|
||||
<span>{integers}.</span>
|
||||
<span>
|
||||
{integers}
|
||||
{separator}
|
||||
</span>
|
||||
<span className="text-neutral-400">{decimalsPlaces}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
size="extra-small"
|
||||
data-testid="open-transfer"
|
||||
onClick={() => setView({ type: ViewType.Transfer })}
|
||||
@@ -16,7 +15,6 @@ export const AccountsMenu = () => {
|
||||
{t('Transfer')}
|
||||
</TradingButton>
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
size="extra-small"
|
||||
onClick={() => setView({ type: ViewType.Deposit })}
|
||||
>
|
||||
|
||||
@@ -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 (
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
size="extra-small"
|
||||
onClick={() => setView({ type: ViewType.Deposit })}
|
||||
data-testid="deposit-button"
|
||||
|
||||
@@ -69,6 +69,7 @@ describe('MarketSelectorItem', () => {
|
||||
targetStake: '1000000',
|
||||
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
|
||||
priceMonitoringBounds: null,
|
||||
lastTradedPrice: '100',
|
||||
};
|
||||
|
||||
const candles = [
|
||||
|
||||
@@ -262,9 +262,7 @@ describe('MarketSelector', () => {
|
||||
await userEvent.click(screen.getByTestId('sort-trigger'));
|
||||
const options = screen.getAllByTestId(/sort-item/);
|
||||
expect(options.map((o) => o.textContent?.trim())).toEqual(
|
||||
Object.entries(Sort)
|
||||
.filter(([key]) => key !== Sort.None)
|
||||
.map(([key]) => SortTypeMapping[key as SortType])
|
||||
Object.entries(Sort).map(([key]) => SortTypeMapping[key as SortType])
|
||||
);
|
||||
await userEvent.click(screen.getByTestId('sort-item-Gained'));
|
||||
expect(
|
||||
|
||||
@@ -40,7 +40,7 @@ export const MarketSelector = ({
|
||||
const [filter, setFilter] = useState<Filter>({
|
||||
searchTerm: '',
|
||||
product: Product.All,
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: [],
|
||||
});
|
||||
const allProducts = filter.product === Product.All;
|
||||
@@ -53,7 +53,7 @@ export const MarketSelector = ({
|
||||
|
||||
return (
|
||||
<div data-testid="market-selector">
|
||||
<div className="pt-2 px-2 mb-2">
|
||||
<div className="px-2 pt-2 mb-2">
|
||||
<ProductSelector
|
||||
product={filter.product}
|
||||
onSelect={(product) => {
|
||||
@@ -106,9 +106,6 @@ export const MarketSelector = ({
|
||||
currentSort={filter.sort}
|
||||
onSelect={(sort) => {
|
||||
setFilter((curr) => {
|
||||
if (curr.sort === sort) {
|
||||
return { ...curr, sort: Sort.None };
|
||||
}
|
||||
return {
|
||||
...curr,
|
||||
sort,
|
||||
@@ -294,9 +291,9 @@ const List = ({
|
||||
|
||||
const Skeleton = () => {
|
||||
return (
|
||||
<div className="mb-2 px-2">
|
||||
<div className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg p-4">
|
||||
<div className="w-full h-3 bg-vega-light-200 dark:bg-vega-dark-200 mb-2" />
|
||||
<div className="px-2 mb-2">
|
||||
<div className="p-4 rounded-lg bg-vega-light-100 dark:bg-vega-dark-100">
|
||||
<div className="w-full h-3 mb-2 bg-vega-light-200 dark:bg-vega-dark-200" />
|
||||
<div className="w-2/3 h-3 bg-vega-light-200 dark:bg-vega-dark-200" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 = ({
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger data-testid="sort-trigger">
|
||||
<span className="flex justify-between items-center">
|
||||
{currentSort === SortTypeMapping.None
|
||||
? t('Sort')
|
||||
: SortTypeMapping[currentSort]}{' '}
|
||||
<span className="flex items-center justify-between gap-1">
|
||||
{SortTypeMapping[currentSort]}
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} />
|
||||
</span>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -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 (
|
||||
<DropdownMenuRadioItem
|
||||
inset
|
||||
key={key}
|
||||
value={key}
|
||||
data-testid={`sort-item-${key}`}
|
||||
>
|
||||
<span className="flex gap-2">
|
||||
<VegaIcon name={SortIconMapping[key as SortType]} />{' '}
|
||||
{SortTypeMapping[key as SortType]}
|
||||
</span>
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuRadioItem>
|
||||
);
|
||||
})}
|
||||
{Object.keys(Sort).map((key) => {
|
||||
return (
|
||||
<DropdownMenuRadioItem
|
||||
inset
|
||||
key={key}
|
||||
value={key}
|
||||
data-testid={`sort-item-${key}`}
|
||||
>
|
||||
<span className="flex gap-2">
|
||||
<VegaIcon name={SortIconMapping[key as SortType]} />{' '}
|
||||
{SortTypeMapping[key as SortType]}
|
||||
</span>
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuRadioItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -12,7 +12,10 @@ import { useMarketList } from '@vegaprotocol/markets';
|
||||
import type { Filter } from '../../components/market-selector';
|
||||
import { subDays } from 'date-fns';
|
||||
|
||||
jest.mock('@vegaprotocol/markets');
|
||||
jest.mock('@vegaprotocol/markets', () => ({
|
||||
...jest.requireActual('@vegaprotocol/markets'),
|
||||
useMarketList: jest.fn(),
|
||||
}));
|
||||
const mockUseMarketList = useMarketList as jest.Mock;
|
||||
|
||||
describe('useMarketSelectorList', () => {
|
||||
@@ -20,7 +23,7 @@ describe('useMarketSelectorList', () => {
|
||||
const defaultArgs: Filter = {
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: [],
|
||||
};
|
||||
return renderHook((args) => useMarketSelectorList(args), {
|
||||
@@ -109,21 +112,21 @@ describe('useMarketSelectorList', () => {
|
||||
rerender({
|
||||
searchTerm: '',
|
||||
product: Product.Spot as 'Future',
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([markets[1]]);
|
||||
rerender({
|
||||
searchTerm: '',
|
||||
product: Product.Perpetual as 'Future',
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([markets[2]]);
|
||||
rerender({
|
||||
searchTerm: '',
|
||||
product: Product.All,
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual(markets);
|
||||
@@ -189,7 +192,7 @@ describe('useMarketSelectorList', () => {
|
||||
const { result, rerender } = setup({
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: ['asset-0'],
|
||||
});
|
||||
expect(result.current.markets).toEqual([markets[0], markets[1]]);
|
||||
@@ -197,7 +200,7 @@ describe('useMarketSelectorList', () => {
|
||||
rerender({
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: ['asset-0', 'asset-1'],
|
||||
});
|
||||
|
||||
@@ -210,7 +213,7 @@ describe('useMarketSelectorList', () => {
|
||||
rerender({
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: ['asset-0', 'asset-1', 'asset-2'],
|
||||
});
|
||||
|
||||
@@ -220,7 +223,7 @@ describe('useMarketSelectorList', () => {
|
||||
rerender({
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: ['asset-invalid'],
|
||||
});
|
||||
|
||||
@@ -275,28 +278,28 @@ describe('useMarketSelectorList', () => {
|
||||
const { result, rerender } = setup({
|
||||
searchTerm: 'abc',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([markets[0]]);
|
||||
rerender({
|
||||
searchTerm: 'def',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([markets[1], markets[2]]);
|
||||
rerender({
|
||||
searchTerm: 'defg',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([markets[2]]);
|
||||
rerender({
|
||||
searchTerm: 'zzz',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([]);
|
||||
@@ -305,14 +308,14 @@ describe('useMarketSelectorList', () => {
|
||||
rerender({
|
||||
searchTerm: 'aaa',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([markets[0]]);
|
||||
rerender({
|
||||
searchTerm: 'ggg',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([
|
||||
@@ -322,11 +325,15 @@ describe('useMarketSelectorList', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts by state and volume by default', () => {
|
||||
it('sorts by top traded by default', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
state: MarketState.STATE_PENDING,
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
markPrice: '1',
|
||||
},
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
@@ -337,30 +344,42 @@ describe('useMarketSelectorList', () => {
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
markPrice: '1',
|
||||
},
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
volume: '200',
|
||||
volume: '100',
|
||||
},
|
||||
],
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
markPrice: '1',
|
||||
},
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
volume: '100',
|
||||
volume: '300',
|
||||
},
|
||||
],
|
||||
}),
|
||||
createMarketFragment({
|
||||
state: MarketState.STATE_PENDING,
|
||||
id: 'market-3',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
markPrice: '1',
|
||||
},
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
volume: '100',
|
||||
volume: '400',
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -375,14 +394,15 @@ describe('useMarketSelectorList', () => {
|
||||
const { result } = setup({
|
||||
searchTerm: '',
|
||||
product: Product.Future,
|
||||
sort: Sort.None,
|
||||
sort: Sort.TopTraded,
|
||||
assets: [],
|
||||
});
|
||||
|
||||
expect(result.current.markets).toEqual([
|
||||
markets[1],
|
||||
markets[3],
|
||||
markets[2],
|
||||
markets[0],
|
||||
markets[3],
|
||||
markets[1],
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import {
|
||||
calcCandleVolume,
|
||||
calcTradedFactor,
|
||||
useMarketList,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { calcTradedFactor, useMarketList } from '@vegaprotocol/markets';
|
||||
import { priceChangePercentage } from '@vegaprotocol/utils';
|
||||
import type { Filter } from '../../components/market-selector/market-selector';
|
||||
import { Sort } from './sort-dropdown';
|
||||
@@ -60,22 +56,6 @@ export const useMarketSelectorList = ({
|
||||
return false;
|
||||
});
|
||||
|
||||
if (sort === Sort.None) {
|
||||
// Sort by market state primarily and AtoZ secondarily
|
||||
return orderBy(
|
||||
markets,
|
||||
[
|
||||
(m) => MARKET_TEMPLATE.indexOf(m.state),
|
||||
(m) => {
|
||||
if (!m.candles?.length) return 0;
|
||||
const vol = calcCandleVolume(m.candles);
|
||||
return Number(vol || 0);
|
||||
},
|
||||
],
|
||||
['asc', 'desc']
|
||||
);
|
||||
}
|
||||
|
||||
if (sort === Sort.Gained || sort === Sort.Lost) {
|
||||
const dir = sort === Sort.Gained ? 'desc' : 'asc';
|
||||
return orderBy(
|
||||
|
||||
@@ -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 (
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
size="extra-small"
|
||||
data-testid="open-transfer"
|
||||
onClick={toggle}
|
||||
|
||||
@@ -24,8 +24,8 @@ export const Settings = () => {
|
||||
>
|
||||
<Switch
|
||||
name="settings-telemetry-switch"
|
||||
onCheckedChange={(isOn) => setIsApproved(isOn)}
|
||||
checked={isApproved}
|
||||
onCheckedChange={(isOn) => setIsApproved(isOn ? 'true' : 'false')}
|
||||
checked={isApproved === 'true'}
|
||||
/>
|
||||
</SettingsGroup>
|
||||
<SettingsGroup label={t('Toast location')}>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(<TelemetryApproval helpText={helpText} />);
|
||||
expect(screen.getByRole('checkbox')).toHaveAttribute(
|
||||
'data-state',
|
||||
'unchecked'
|
||||
it('click on buttons should be properly handled', async () => {
|
||||
const mockSetTelemetryValue = jest.fn();
|
||||
render(
|
||||
<TelemetryApproval
|
||||
telemetryValue="false"
|
||||
setTelemetryValue={mockSetTelemetryValue}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<TelemetryApproval
|
||||
telemetryValue="true"
|
||||
setTelemetryValue={mockSetTelemetryValue}
|
||||
/>
|
||||
);
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col py-3">
|
||||
<div className="mr-4" role="form">
|
||||
<TradingCheckbox
|
||||
label={<span className="text-lg pl-1">{t('Share usage data')}</span>}
|
||||
checked={isApproved}
|
||||
name="telemetry-approval"
|
||||
onCheckedChange={() => setIsApproved(!isApproved)}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm text-vega-light-300 dark:text-vega-dark-300 ml-6">
|
||||
<span>{helpText}</span>
|
||||
<div className="mt-2">
|
||||
{t(
|
||||
'Help us identify bugs and improve Vega Governance by sharing anonymous usage data.'
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<VegaIcon name={VegaIconNames.EYE_OFF} size={24} />
|
||||
<div className="flex flex-col gap-1 ml-6">
|
||||
<div className="font-semibold">{t('Anonymous')}</div>
|
||||
<div>{t('Your identity is always anonymous on Vega')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center mt-2">
|
||||
<VegaIcon name={VegaIconNames.COG} size={24} />
|
||||
<div className="flex flex-col gap-1 ml-6">
|
||||
<div className="font-semibold">{t('Optional')}</div>
|
||||
<div>{t('You can opt out any time via settings')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-around items-center mt-10 gap-4 w-full px-4">
|
||||
<TradingButton
|
||||
onClick={() => setTelemetryValue('false')}
|
||||
size="small"
|
||||
intent={Intent.None}
|
||||
data-testid="do-not-share-data-button"
|
||||
fill
|
||||
>
|
||||
{t('No thanks')}
|
||||
</TradingButton>
|
||||
|
||||
<TradingButton
|
||||
onClick={() => setTelemetryValue('true')}
|
||||
intent={Intent.Info}
|
||||
data-testid="share-data-button"
|
||||
size="small"
|
||||
fill
|
||||
>
|
||||
{telemetryValue === 'true'
|
||||
? t('Continue sharing data')
|
||||
: t('Share data')}
|
||||
</TradingButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,17 +5,17 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import type { ReactNode } from 'react';
|
||||
import { 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
|
||||
|
||||
@@ -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: (
|
||||
<>
|
||||
<h3 className="text-sm uppercase mb-1">
|
||||
{t('Improve vega console')}
|
||||
</h3>
|
||||
<TelemetryApproval
|
||||
telemetryValue={telemetryValue}
|
||||
setTelemetryValue={setTelemetryApprovalAndClose}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
onClose: onApprovalClose,
|
||||
};
|
||||
if (!hasToast(TELEMETRY_APPROVAL_TOAST_ID)) {
|
||||
setToast(toast);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const title = (
|
||||
<span className="font-alpha calt" data-testid="welcome-title">
|
||||
{t('Console')}{' '}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { 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 (
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
size="extra-small"
|
||||
onClick={() => setView({ type: ViewType.Withdraw })}
|
||||
data-testid="withdraw-dialog-button"
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ import produce from 'immer';
|
||||
|
||||
interface GlobalStore {
|
||||
marketId: string | null;
|
||||
onBoardingDismissed: boolean;
|
||||
eagerConnecting: boolean;
|
||||
update: (store: Partial<Omit<GlobalStore, 'update'>>) => void;
|
||||
}
|
||||
@@ -16,7 +15,6 @@ interface PageTitleStore {
|
||||
|
||||
export const useGlobalStore = create<GlobalStore>()((set) => ({
|
||||
marketId: LocalStorage.getItem('marketId') || null,
|
||||
onBoardingDismissed: false,
|
||||
eagerConnecting: false,
|
||||
update: (newState) => {
|
||||
set(
|
||||
|
||||
@@ -7,8 +7,8 @@ describe('CandlesMenu', () => {
|
||||
render(<CandlesMenu />);
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByText('Studies', {
|
||||
selector: '[type="button"]',
|
||||
screen.getByRole('button', {
|
||||
name: 'Studies',
|
||||
})
|
||||
);
|
||||
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<DropdownMenu
|
||||
<TradingDropdown
|
||||
trigger={
|
||||
<DropdownMenuTrigger className={triggerClasses}>
|
||||
{t(`Interval: ${intervalLabels[interval]}`)}
|
||||
</DropdownMenuTrigger>
|
||||
<TradingDropdownTrigger className={triggerClasses}>
|
||||
<TradingButton {...triggerButtonProps}>
|
||||
{t(`Interval: ${intervalLabels[interval]}`)}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent align={contentAlign}>
|
||||
<DropdownMenuRadioGroup
|
||||
<TradingDropdownContent align={contentAlign}>
|
||||
<TradingDropdownRadioGroup
|
||||
value={interval}
|
||||
onValueChange={(value) => {
|
||||
setInterval(value as Interval);
|
||||
}}
|
||||
>
|
||||
{Object.values(Interval).map((timeInterval) => (
|
||||
<DropdownMenuRadioItem
|
||||
<TradingDropdownRadioItem
|
||||
key={timeInterval}
|
||||
inset
|
||||
value={timeInterval}
|
||||
>
|
||||
{intervalLabels[timeInterval]}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuRadioItem>
|
||||
<TradingDropdownItemIndicator />
|
||||
</TradingDropdownRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu
|
||||
</TradingDropdownRadioGroup>
|
||||
</TradingDropdownContent>
|
||||
</TradingDropdown>
|
||||
<TradingDropdown
|
||||
trigger={
|
||||
<DropdownMenuTrigger className={triggerClasses}>
|
||||
<Icon name={chartTypeIcon.get(chartType) as IconName} />
|
||||
</DropdownMenuTrigger>
|
||||
<TradingDropdownTrigger className={triggerClasses}>
|
||||
<TradingButton {...triggerButtonProps}>
|
||||
<Icon name={chartTypeIcon.get(chartType) as IconName} />
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent align={contentAlign}>
|
||||
<DropdownMenuRadioGroup
|
||||
<TradingDropdownContent align={contentAlign}>
|
||||
<TradingDropdownRadioGroup
|
||||
value={chartType}
|
||||
onValueChange={(value) => {
|
||||
setType(value as ChartType);
|
||||
}}
|
||||
>
|
||||
{Object.values(ChartType).map((type) => (
|
||||
<DropdownMenuRadioItem key={type} inset value={type}>
|
||||
<TradingDropdownRadioItem key={type} inset value={type}>
|
||||
{chartTypeLabels[type]}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuRadioItem>
|
||||
<TradingDropdownItemIndicator />
|
||||
</TradingDropdownRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu
|
||||
</TradingDropdownRadioGroup>
|
||||
</TradingDropdownContent>
|
||||
</TradingDropdown>
|
||||
<TradingDropdown
|
||||
trigger={
|
||||
<DropdownMenuTrigger className={triggerClasses}>
|
||||
{t('Overlays')}
|
||||
</DropdownMenuTrigger>
|
||||
<TradingDropdownTrigger className={triggerClasses}>
|
||||
<TradingButton {...triggerButtonProps}>
|
||||
{t('Overlays')}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent align={contentAlign}>
|
||||
<TradingDropdownContent align={contentAlign}>
|
||||
{Object.values(Overlay).map((overlay) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
<TradingDropdownCheckboxItem
|
||||
key={overlay}
|
||||
checked={overlays.includes(overlay)}
|
||||
onCheckedChange={() => {
|
||||
@@ -121,21 +129,23 @@ export const CandlesMenu = () => {
|
||||
}}
|
||||
>
|
||||
{overlayLabels[overlay]}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuCheckboxItem>
|
||||
<TradingDropdownItemIndicator />
|
||||
</TradingDropdownCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu
|
||||
</TradingDropdownContent>
|
||||
</TradingDropdown>
|
||||
<TradingDropdown
|
||||
trigger={
|
||||
<DropdownMenuTrigger className={triggerClasses}>
|
||||
{t('Studies')}
|
||||
</DropdownMenuTrigger>
|
||||
<TradingDropdownTrigger className={triggerClasses}>
|
||||
<TradingButton {...triggerButtonProps}>
|
||||
{t('Studies')}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent align={contentAlign}>
|
||||
<TradingDropdownContent align={contentAlign}>
|
||||
{Object.values(Study).map((study) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
<TradingDropdownCheckboxItem
|
||||
key={study}
|
||||
checked={studies.includes(study)}
|
||||
onCheckedChange={() => {
|
||||
@@ -150,11 +160,11 @@ export const CandlesMenu = () => {
|
||||
}}
|
||||
>
|
||||
{studyLabels[study]}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuCheckboxItem>
|
||||
<TradingDropdownItemIndicator />
|
||||
</TradingDropdownCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TradingDropdownContent>
|
||||
</TradingDropdown>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Market> = {}
|
||||
) {
|
||||
const joinedMarket: Market = {
|
||||
...market,
|
||||
...marketOverrides,
|
||||
} as Market;
|
||||
return (
|
||||
<MockedProvider mocks={[...mocks]}>
|
||||
<VegaWalletContext.Provider value={{ pubKey, isReadOnly: false } as any}>
|
||||
<DealTicket
|
||||
market={market}
|
||||
market={joinedMarket}
|
||||
marketData={marketData}
|
||||
marketPrice={marketPrice}
|
||||
submit={submit}
|
||||
@@ -367,7 +376,6 @@ describe('DealTicket', () => {
|
||||
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
|
||||
|
||||
@@ -496,11 +496,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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type { Trade } from './fills-data-provider';
|
||||
|
||||
import { FillsTable, getFeesBreakdown } from './fills-table';
|
||||
import { generateFill } from './test-helpers';
|
||||
|
||||
@@ -215,6 +214,27 @@ describe('FillsTable', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('negative positionDecimalPoints should be properly rendered in size column', async () => {
|
||||
const partyId = 'party-id';
|
||||
const negativeDecimalPositionFill = generateFill({
|
||||
...defaultFill,
|
||||
market: {
|
||||
...defaultFill.market,
|
||||
positionDecimalPlaces: -4,
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
render(
|
||||
<FillsTable partyId={partyId} rowData={[negativeDecimalPositionFill]} />
|
||||
);
|
||||
});
|
||||
|
||||
const sizeCell = screen
|
||||
.getAllByRole('gridcell')
|
||||
.find((c) => c.getAttribute('col-id') === 'size');
|
||||
expect(sizeCell).toHaveTextContent('3,000,000,000');
|
||||
});
|
||||
|
||||
describe('getFeesBreakdown', () => {
|
||||
it('should return correct fees breakdown for a taker', () => {
|
||||
const fees = {
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex h-6">
|
||||
<button
|
||||
onClick={increaseResolution}
|
||||
disabled={resolutions.indexOf(resolution) >= resolutions.length - 1}
|
||||
className="flex items-center px-2 border-r cursor-pointer border-default"
|
||||
data-testid="plus-button"
|
||||
>
|
||||
<VegaIcon size={12} name={VegaIconNames.PLUS} />
|
||||
</button>
|
||||
<TradingDropdown
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => setOpen(open)}
|
||||
trigger={
|
||||
<TradingDropdownTrigger data-testid="resolution">
|
||||
<button
|
||||
className="flex items-center px-2 text-left gap-1"
|
||||
style={{
|
||||
minWidth: `${
|
||||
Math.max.apply(
|
||||
null,
|
||||
resolutions.map((item) => formatResolution(item).length)
|
||||
) + 5
|
||||
}ch`,
|
||||
}}
|
||||
>
|
||||
<VegaIcon
|
||||
size={12}
|
||||
name={
|
||||
isOpen ? VegaIconNames.CHEVRON_UP : VegaIconNames.CHEVRON_DOWN
|
||||
}
|
||||
/>
|
||||
{formatResolution(resolution)}
|
||||
</button>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
>
|
||||
<TradingDropdownContent align="start">
|
||||
{resolutions.map((r) => (
|
||||
<TradingDropdownItem key={r} onClick={() => setResolution(r)}>
|
||||
{formatResolution(r)}
|
||||
</TradingDropdownItem>
|
||||
))}
|
||||
</TradingDropdownContent>
|
||||
</TradingDropdown>
|
||||
<button
|
||||
onClick={decreaseResolution}
|
||||
disabled={resolutions.indexOf(resolution) <= 0}
|
||||
className="flex items-center px-2 cursor-pointer border-x border-default"
|
||||
data-testid="minus-button"
|
||||
>
|
||||
<VegaIcon size={12} name={VegaIconNames.MINUS} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ export type OrderbookData = {
|
||||
|
||||
interface OrderbookManagerProps {
|
||||
marketId: string;
|
||||
onClick?: (args: { price?: string; size?: string }) => void;
|
||||
onClick: (args: { price?: string; size?: string }) => void;
|
||||
}
|
||||
|
||||
export const OrderbookManager = ({
|
||||
@@ -61,15 +61,17 @@ export const OrderbookManager = ({
|
||||
data={data}
|
||||
reload={reload}
|
||||
>
|
||||
<Orderbook
|
||||
bids={data?.depth.buy ?? []}
|
||||
asks={data?.depth.sell ?? []}
|
||||
decimalPlaces={market?.decimalPlaces ?? 0}
|
||||
positionDecimalPlaces={market?.positionDecimalPlaces ?? 0}
|
||||
assetSymbol={market?.tradableInstrument.instrument.product.quoteName}
|
||||
onClick={onClick}
|
||||
midPrice={marketData?.midPrice}
|
||||
/>
|
||||
{market && marketData && (
|
||||
<Orderbook
|
||||
bids={data?.depth.buy ?? []}
|
||||
asks={data?.depth.sell ?? []}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
positionDecimalPlaces={market.positionDecimalPlaces}
|
||||
assetSymbol={market.tradableInstrument.instrument.product.quoteName}
|
||||
onClick={onClick}
|
||||
lastTradedPrice={marketData.lastTradedPrice}
|
||||
/>
|
||||
)}
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
data-testid={`${VolumeType.bid === type ? 'bid' : 'ask'}-bar`}
|
||||
className={classNames(
|
||||
'absolute top-0 left-0 h-full',
|
||||
type === VolumeType.bid
|
||||
? 'bg-market-green-300 dark:bg-market-green/50'
|
||||
: 'bg-market-red-300 dark:bg-market-red/30'
|
||||
)}
|
||||
style={{
|
||||
width: `${cumulativeValue}%`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 ? (
|
||||
<NumericCell
|
||||
testId={testId}
|
||||
value={cumulativeValue}
|
||||
valueFormatted={addDecimalsFixedFormatNumber(
|
||||
cumulativeValue,
|
||||
positionDecimalPlaces ?? 0
|
||||
)}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return onClick && volume ? (
|
||||
<button
|
||||
onClick={() => onClick(cumulativeValue)}
|
||||
className="hover:dark:bg-neutral-800 hover:bg-neutral-200 text-right pr-1"
|
||||
>
|
||||
{volume}
|
||||
</button>
|
||||
) : (
|
||||
<div className="pr-1" data-testid={testId}>
|
||||
{volume}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
CumulativeVol.displayName = 'OrderBookCumulativeVol';
|
||||
|
||||
export const OrderbookRow = React.memo(
|
||||
({
|
||||
value,
|
||||
cumulativeValue,
|
||||
cumulativeRelativeValue,
|
||||
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 (
|
||||
<div className="relative pr-1">
|
||||
<CumulationBar cumulativeValue={cumulativeRelativeValue} type={type} />
|
||||
<div className="relative px-1">
|
||||
<CumulationBar
|
||||
cumulativeVolume={cumulativeVolume}
|
||||
type={type}
|
||||
maxVol={maxVol}
|
||||
/>
|
||||
<div
|
||||
data-testid={`${txtId}-rows-container`}
|
||||
className={classNames('grid gap-1 text-right', `grid-cols-${cols}`)}
|
||||
>
|
||||
<PriceCell
|
||||
testId={`price-${price}`}
|
||||
value={BigInt(price)}
|
||||
onClick={() =>
|
||||
onClick && onClick({ price: addDecimal(price, decimalPlaces) })
|
||||
}
|
||||
valueFormatted={addDecimalsFixedFormatNumber(price, decimalPlaces)}
|
||||
className={
|
||||
type === VolumeType.ask
|
||||
? 'text-market-red dark:text-market-red'
|
||||
: 'text-market-green-600 dark:text-market-green'
|
||||
}
|
||||
/>
|
||||
{width >= HIDE_VOL_WIDTH && (
|
||||
<PriceCell
|
||||
testId={`${txtId}-vol-${price}`}
|
||||
onClick={(value) =>
|
||||
onClick &&
|
||||
value &&
|
||||
onClick({
|
||||
size: addDecimal(value, positionDecimalPlaces),
|
||||
})
|
||||
}
|
||||
value={value}
|
||||
<OrderBookRowCell
|
||||
onClick={() => onClick({ price: addDecimal(price, decimalPlaces) })}
|
||||
>
|
||||
<NumericCell
|
||||
testId={`price-${price}`}
|
||||
value={BigInt(price)}
|
||||
valueFormatted={addDecimalsFixedFormatNumber(
|
||||
value,
|
||||
positionDecimalPlaces
|
||||
price,
|
||||
decimalPlaces
|
||||
)}
|
||||
className={classNames({
|
||||
'text-market-red dark:text-market-red': type === VolumeType.ask,
|
||||
'text-market-green-600 dark:text-market-green':
|
||||
type === VolumeType.bid,
|
||||
})}
|
||||
/>
|
||||
</OrderBookRowCell>
|
||||
{width >= HIDE_VOL_WIDTH && (
|
||||
<OrderBookRowCell
|
||||
onClick={() =>
|
||||
onClick({ size: addDecimal(volume, positionDecimalPlaces) })
|
||||
}
|
||||
>
|
||||
<NumericCell
|
||||
testId={`${txtId}-vol-${price}`}
|
||||
value={volume}
|
||||
valueFormatted={addDecimalsFixedFormatNumber(
|
||||
volume,
|
||||
positionDecimalPlaces ?? 0
|
||||
)}
|
||||
/>
|
||||
</OrderBookRowCell>
|
||||
)}
|
||||
{width >= HIDE_CUMULATIVE_VOL_WIDTH && (
|
||||
<CumulativeVol
|
||||
testId={`cumulative-vol-${price}`}
|
||||
<OrderBookRowCell
|
||||
onClick={() =>
|
||||
onClick &&
|
||||
cumulativeValue &&
|
||||
onClick({
|
||||
size: addDecimal(cumulativeValue, positionDecimalPlaces),
|
||||
size: addDecimal(cumulativeVolume, positionDecimalPlaces),
|
||||
})
|
||||
}
|
||||
positionDecimalPlaces={positionDecimalPlaces}
|
||||
cumulativeValue={cumulativeValue}
|
||||
/>
|
||||
>
|
||||
<NumericCell
|
||||
testId={`cumulative-vol-${price}`}
|
||||
value={cumulativeVolume}
|
||||
valueFormatted={addDecimalsFixedFormatNumber(
|
||||
cumulativeVolume,
|
||||
positionDecimalPlaces
|
||||
)}
|
||||
/>
|
||||
</OrderBookRowCell>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
OrderbookRow.displayName = 'OrderbookRow';
|
||||
|
||||
const OrderBookRowCell = ({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
onClick: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
className="overflow-hidden text-right text-ellipsis whitespace-nowrap hover:dark:bg-neutral-800 hover:bg-neutral-200"
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const CumulationBar = ({
|
||||
cumulativeVolume = 0,
|
||||
type,
|
||||
maxVol,
|
||||
}: {
|
||||
cumulativeVolume: number;
|
||||
type: VolumeType;
|
||||
maxVol: number;
|
||||
}) => {
|
||||
const width = (cumulativeVolume / maxVol) * 100;
|
||||
return (
|
||||
<div
|
||||
data-testid={`${VolumeType.bid === type ? 'bid' : 'ask'}-bar`}
|
||||
className={classNames(
|
||||
'absolute top-0 left-0 h-full',
|
||||
type === VolumeType.bid
|
||||
? 'bg-market-green/10 dark:bg-market-green/10'
|
||||
: 'bg-market-red/10 dark:bg-market-red/10'
|
||||
)}
|
||||
style={{
|
||||
width: `${width}%`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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(<OrderbookMid {...props} />);
|
||||
expect(screen.getByTestId(/last-traded/)).toHaveTextContent(
|
||||
props.lastTradedPrice
|
||||
);
|
||||
expect(screen.getByText(props.assetSymbol)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('spread')).toHaveTextContent('(2)');
|
||||
|
||||
// rerender with no change should not show the icon
|
||||
rerender(<OrderbookMid {...props} />);
|
||||
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<OrderbookMid {...props} lastTradedPrice="101" bestAskPrice="102" />
|
||||
);
|
||||
expect(screen.getByTestId('icon-arrow-up')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('spread')).toHaveTextContent('(3)');
|
||||
|
||||
// rerender again with the same price, should still be set to 'up'
|
||||
rerender(
|
||||
<OrderbookMid
|
||||
{...props}
|
||||
lastTradedPrice="101"
|
||||
bestAskPrice="102"
|
||||
bestBidPrice="98"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('icon-arrow-up')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('spread')).toHaveTextContent('(4)');
|
||||
|
||||
rerender(<OrderbookMid {...props} lastTradedPrice="100" />);
|
||||
expect(screen.getByTestId('icon-arrow-down')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,9 +9,9 @@ type Props = Omit<MockDataGeneratorParams, 'resolution'> & {
|
||||
|
||||
const OrderbookMockDataProvider = ({ decimalPlaces, ...props }: Props) => {
|
||||
return (
|
||||
<div className="absolute inset-0 dark:bg-black dark:text-neutral-200 bg-white text-neutral-800">
|
||||
<div className="absolute inset-0 bg-white dark:bg-black dark:text-neutral-200 text-neutral-800">
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0"
|
||||
className="absolute top-0 bottom-0 left-0"
|
||||
style={{ width: '400px' }}
|
||||
>
|
||||
<Orderbook
|
||||
@@ -19,6 +19,7 @@ const OrderbookMockDataProvider = ({ decimalPlaces, ...props }: Props) => {
|
||||
decimalPlaces={decimalPlaces}
|
||||
{...generateMockData({ ...props })}
|
||||
assetSymbol="USD"
|
||||
onClick={() => undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<Record<PriceChange, VegaIconNames>> = {
|
||||
up: VegaIconNames.ARROW_UP,
|
||||
down: VegaIconNames.ARROW_DOWN,
|
||||
none: VegaIconNames.BULLET,
|
||||
};
|
||||
const PRICE_CHANGE_CLASS_MAP: Readonly<Record<PriceChange, string>> = {
|
||||
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 (
|
||||
<div
|
||||
@@ -78,11 +57,11 @@ const OrderbookTable = ({
|
||||
onClick={onClick}
|
||||
decimalPlaces={decimalPlaces - Math.log10(resolution)}
|
||||
positionDecimalPlaces={positionDecimalPlaces}
|
||||
value={data.value}
|
||||
cumulativeValue={data.cumulativeVol.value}
|
||||
cumulativeRelativeValue={data.cumulativeVol.relativeValue}
|
||||
volume={data.volume}
|
||||
cumulativeVolume={data.cumulativeVol}
|
||||
type={type}
|
||||
width={width}
|
||||
maxVol={maxVol}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -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 (
|
||||
<div className="flex items-center justify-center text-base gap-2">
|
||||
{priceChangeRef.current !== 'none' && (
|
||||
<span
|
||||
className={classNames('flex flex-col justify-center', {
|
||||
'text-market-green-600 dark:text-market-green':
|
||||
priceChangeRef.current === 'up',
|
||||
'text-market-red dark:text-market-red':
|
||||
priceChangeRef.current === 'down',
|
||||
})}
|
||||
>
|
||||
<VegaIcon
|
||||
name={
|
||||
priceChangeRef.current === 'up'
|
||||
? VegaIconNames.ARROW_UP
|
||||
: VegaIconNames.ARROW_DOWN
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
// monospace sizing doesn't quite align with alpha
|
||||
className="font-mono text-[15px]"
|
||||
data-testid={`last-traded-${lastTradedPrice}`}
|
||||
title={t('Last traded price')}
|
||||
>
|
||||
{addDecimalsFormatNumber(lastTradedPrice, decimalPlaces)}
|
||||
</span>
|
||||
<span>{assetSymbol}</span>
|
||||
<span
|
||||
title={t('Spread')}
|
||||
className="font-mono text-xs text-muted"
|
||||
data-testid="spread"
|
||||
>
|
||||
({addDecimalsFormatNumber(spread, decimalPlaces)})
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 = (
|
||||
<span
|
||||
className={classNames(PRICE_CHANGE_CLASS_MAP[priceChangeRef.current])}
|
||||
>
|
||||
<VegaIcon name={PRICE_CHANGE_ICON_MAP[priceChangeRef.current]} />
|
||||
</span>
|
||||
);
|
||||
|
||||
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 (
|
||||
<div className="h-full text-xs grid grid-rows-[1fr_min-content]">
|
||||
@@ -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 (
|
||||
<div
|
||||
className="overflow-hidden grid"
|
||||
data-testid="orderbook-grid-element"
|
||||
style={{
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
width,
|
||||
height,
|
||||
gridTemplateRows: `1fr ${midHeight}px 1fr`, // cannot use tailwind here as tailwind will not parse a class string with interpolation
|
||||
}}
|
||||
>
|
||||
{askRows.length || bidRows.length ? (
|
||||
<>
|
||||
<OrderbookTable
|
||||
<OrderbookSide
|
||||
rows={askRows}
|
||||
type={VolumeType.ask}
|
||||
resolution={resolution}
|
||||
@@ -193,22 +205,16 @@ export const Orderbook = ({
|
||||
positionDecimalPlaces={positionDecimalPlaces}
|
||||
onClick={onClick}
|
||||
width={width}
|
||||
maxVol={maxVol}
|
||||
/>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{midPrice && (
|
||||
<>
|
||||
<span
|
||||
className="font-mono text-lg"
|
||||
data-testid={`middle-mark-price-${midPrice}`}
|
||||
>
|
||||
{addDecimalsFormatNumber(midPrice, decimalPlaces)}
|
||||
</span>
|
||||
<span className="text-base">{assetSymbol}</span>
|
||||
{priceChangeIcon}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<OrderbookTable
|
||||
<OrderbookMid
|
||||
lastTradedPrice={lastTradedPrice}
|
||||
decimalPlaces={decimalPlaces}
|
||||
assetSymbol={assetSymbol}
|
||||
bestAskPrice={bestAskPrice}
|
||||
bestBidPrice={bestBidPrice}
|
||||
/>
|
||||
<OrderbookSide
|
||||
rows={bidRows}
|
||||
type={VolumeType.bid}
|
||||
resolution={resolution}
|
||||
@@ -216,10 +222,11 @@ export const Orderbook = ({
|
||||
positionDecimalPlaces={positionDecimalPlaces}
|
||||
onClick={onClick}
|
||||
width={width}
|
||||
maxVol={maxVol}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="inset-0 absolute">
|
||||
<div className="absolute inset-0">
|
||||
<Splash>{t('No data')}</Splash>
|
||||
</div>
|
||||
)}
|
||||
@@ -228,59 +235,13 @@ export const Orderbook = ({
|
||||
}}
|
||||
</ReactVirtualizedAutoSizer>
|
||||
</div>
|
||||
<div className="border-t border-default flex">
|
||||
<button
|
||||
onClick={increaseResolution}
|
||||
disabled={resolutions.indexOf(resolution) >= resolutions.length - 1}
|
||||
className="flex items-center border-r border-default px-2 cursor-pointer"
|
||||
data-testid="plus-button"
|
||||
>
|
||||
<VegaIcon size={12} name={VegaIconNames.PLUS} />
|
||||
</button>
|
||||
<DropdownMenu
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => setOpen(open)}
|
||||
trigger={
|
||||
<DropdownMenuTrigger
|
||||
data-testid="resolution"
|
||||
className="flex justify-between px-1 items-center"
|
||||
style={{
|
||||
width: `${
|
||||
Math.max.apply(
|
||||
null,
|
||||
resolutions.map((item) => formatResolution(item).length)
|
||||
) + 3
|
||||
}ch`,
|
||||
}}
|
||||
>
|
||||
<VegaIcon
|
||||
size={12}
|
||||
name={
|
||||
isOpen ? VegaIconNames.CHEVRON_UP : VegaIconNames.CHEVRON_DOWN
|
||||
}
|
||||
/>
|
||||
<div className="text-xs text-left">
|
||||
{formatResolution(resolution)}
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent align="start">
|
||||
{resolutions.map((r) => (
|
||||
<DropdownMenuItem key={r} onClick={() => setResolution(r)}>
|
||||
{formatResolution(r)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<button
|
||||
onClick={decreaseResolution}
|
||||
disabled={resolutions.indexOf(resolution) <= 0}
|
||||
className="flex items-center border-x border-default px-2 cursor-pointer"
|
||||
data-testid="minus-button"
|
||||
>
|
||||
<VegaIcon size={12} name={VegaIconNames.MINUS} />
|
||||
</button>
|
||||
<div className="border-t border-default">
|
||||
<OrderbookControls
|
||||
lastTradedPrice={lastTradedPrice}
|
||||
resolution={resolution}
|
||||
decimalPlaces={decimalPlaces}
|
||||
setResolution={setResolution}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+6
-4
@@ -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`
|
||||
|
||||
@@ -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!) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }) => (
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
size="extra-small"
|
||||
onClick={onClick}
|
||||
data-testid="cancelAll"
|
||||
>
|
||||
<TradingButton size="extra-small" onClick={onClick} data-testid="cancelAll">
|
||||
{t('Cancel all')}
|
||||
</TradingButton>
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<BlockStatisticsQuery>({
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -5,6 +5,6 @@ import { toNumberParts } from '@vegaprotocol/utils';
|
||||
export const useNumberParts = (
|
||||
value: BigNumber | null | undefined,
|
||||
decimals: number
|
||||
): [integers: string, decimalPlaces: string] => {
|
||||
): [integers: string, decimalPlaces: string, separator: string | undefined] => {
|
||||
return useMemo(() => toNumberParts(value, decimals), [decimals, value]);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export const IconEyeOff = ({ size = 16 }: { size: number }) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M16 7.97v-.02-.01-.02-.02a.672.672 0 00-.17-.36c-.49-.63-1.07-1.2-1.65-1.72l-3.16 2.26a2.978 2.978 0 01-2.98 2.9c-.31 0-.6-.06-.88-.15L5.09 12.3c.44.19.9.36 1.37.47.97.23 1.94.24 2.92.05.88-.17 1.74-.54 2.53-.98 1.25-.7 2.39-1.67 3.38-2.75.18-.2.37-.41.53-.62.09-.1.15-.22.17-.36v-.02-.02-.01-.02-.03c.01-.02.01-.03.01-.04zm-.43-4.17c.25-.18.43-.46.43-.8 0-.55-.45-1-1-1-.22 0-.41.08-.57.2l-.01-.01-2.67 1.91c-.69-.38-1.41-.69-2.17-.87a6.8 6.8 0 00-2.91-.05c-.88.18-1.74.54-2.53.99-1.25.7-2.39 1.67-3.38 2.75-.18.2-.37.41-.53.62-.23.29-.23.63-.01.92.51.66 1.11 1.25 1.73 1.79.18.16.38.29.56.44l-2.09 1.5.01.01c-.25.18-.43.46-.43.8 0 .55.45 1 1 1 .22 0 .41-.08.57-.2l.01.01 14-10-.01-.01zm-10.41 5a3.03 3.03 0 01-.11-.8 2.99 2.99 0 012.99-2.98c.62 0 1.19.21 1.66.53L5.16 8.8z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
formatNumberPercentage,
|
||||
getUnlimitedThreshold,
|
||||
isNumeric,
|
||||
removeDecimal,
|
||||
toBigNum,
|
||||
quantumDecimalPlaces,
|
||||
toDecimal,
|
||||
toNumberParts,
|
||||
@@ -84,17 +86,17 @@ describe('number utils', () => {
|
||||
|
||||
describe('toNumberParts', () => {
|
||||
it.each([
|
||||
{ v: null, d: 3, o: ['0', '000'] },
|
||||
{ v: undefined, d: 3, o: ['0', '000'] },
|
||||
{ v: new BigNumber(123), d: 3, o: ['123', '00'] },
|
||||
{ v: new BigNumber(123.123), d: 3, o: ['123', '123'] },
|
||||
{ v: new BigNumber(123.123), d: 6, o: ['123', '123'] },
|
||||
{ v: new BigNumber(123.123), d: 0, o: ['123', ''] },
|
||||
{ v: new BigNumber(123), d: undefined, o: ['123', '00'] },
|
||||
{ v: null, d: 3, o: ['0', '000', '.'] },
|
||||
{ v: undefined, d: 3, o: ['0', '000', '.'] },
|
||||
{ v: new BigNumber(123), d: 3, o: ['123', '00', '.'] },
|
||||
{ v: new BigNumber(123.123), d: 3, o: ['123', '123', '.'] },
|
||||
{ v: new BigNumber(123.123), d: 6, o: ['123', '123', '.'] },
|
||||
{ v: new BigNumber(123.123), d: 0, o: ['123', '', '.'] },
|
||||
{ v: new BigNumber(123), d: undefined, o: ['123', '00', '.'] },
|
||||
{
|
||||
v: new BigNumber(30000),
|
||||
d: undefined,
|
||||
o: ['30,000', '00'],
|
||||
o: ['30,000', '00', '.'],
|
||||
},
|
||||
])('returns correct tuple given the different arguments', ({ v, d, o }) => {
|
||||
expect(toNumberParts(v, d)).toStrictEqual(o);
|
||||
@@ -153,10 +155,36 @@ describe('number utils', () => {
|
||||
{ v: 7, o: '0.0000001' },
|
||||
{ v: 8, o: '0.00000001' },
|
||||
{ v: 9, o: '0.000000001' },
|
||||
{ v: -1, o: '10' },
|
||||
{ v: -2, o: '100' },
|
||||
{ v: -3, o: '1000' },
|
||||
])('formats with toNumber given number correctly', ({ v, o }) => {
|
||||
expect(toDecimal(v)).toStrictEqual(o);
|
||||
});
|
||||
});
|
||||
|
||||
describe('positive and negative decimals should be handled correctly', () => {
|
||||
const baseNum = '2000';
|
||||
const methods = [removeDecimal, toBigNum];
|
||||
it.each([
|
||||
{ decimals: 0, result: ['2000', '2000'] },
|
||||
{ decimals: 1, result: ['20000', '200'] },
|
||||
{ decimals: -1, result: ['200', '20000'] },
|
||||
{ decimals: 2, result: ['200000', '20'] },
|
||||
{ decimals: -2, result: ['20', '200000'] },
|
||||
{ decimals: 3, result: ['2000000', '2'] },
|
||||
{ decimals: -3, result: ['2', '2000000'] },
|
||||
{ decimals: 4, result: ['20000000', '0.2'] },
|
||||
{ decimals: -4, result: ['0', '20000000'] }, // removeDecimal has toFixed(0) at the end
|
||||
])(
|
||||
'number methods should handle negative decimals',
|
||||
({ decimals, result }) => {
|
||||
methods.forEach((method, i) => {
|
||||
expect(method(baseNum, decimals).toString()).toEqual(result[i]);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('quantumDecimalPlaces', () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ const MAX_FRACTION_DIGITS = 20;
|
||||
|
||||
export function toDecimal(numberOfDecimals: number) {
|
||||
return new BigNumber(1)
|
||||
.dividedBy(Math.pow(10, numberOfDecimals))
|
||||
.dividedBy(new BigNumber(10).exponentiatedBy(numberOfDecimals))
|
||||
.toString(10);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ export function toBigNum(
|
||||
rawValue: string | number,
|
||||
decimals: number
|
||||
): BigNumber {
|
||||
return new BigNumber(rawValue || 0).dividedBy(Math.pow(10, decimals));
|
||||
const divides = new BigNumber(10).exponentiatedBy(decimals);
|
||||
return new BigNumber(rawValue || 0).dividedBy(divides);
|
||||
}
|
||||
|
||||
export function addDecimal(
|
||||
@@ -48,7 +49,8 @@ export function removeDecimal(
|
||||
value: string | BigNumber,
|
||||
decimals: number
|
||||
): string {
|
||||
return new BigNumber(value || 0).times(Math.pow(10, decimals)).toFixed(0);
|
||||
const times = new BigNumber(10).exponentiatedBy(decimals);
|
||||
return new BigNumber(value || 0).times(times).toFixed(0);
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
|
||||
@@ -163,15 +165,15 @@ export const formatNumberPercentage = (value: BigNumber, decimals?: number) => {
|
||||
export const toNumberParts = (
|
||||
value: BigNumber | null | undefined,
|
||||
decimals = 18
|
||||
): [integers: string, decimalPlaces: string] => {
|
||||
): [integers: string, decimalPlaces: string, separator: string] => {
|
||||
if (!value) {
|
||||
return ['0', '0'.repeat(decimals)];
|
||||
return ['0', '0'.repeat(decimals), '.'];
|
||||
}
|
||||
const separator = getDecimalSeparator() || '.';
|
||||
const [integers, decimalsPlaces] = formatNumber(value, decimals)
|
||||
.toString()
|
||||
.split(separator);
|
||||
return [integers, decimalsPlaces || ''];
|
||||
return [integers, decimalsPlaces || '', separator];
|
||||
};
|
||||
|
||||
export const isNumeric = (
|
||||
|
||||
@@ -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 === '') {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</ConnectDialogContent>
|
||||
@@ -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 = ({
|
||||
</div>
|
||||
{connectors['snap'] !== undefined ? (
|
||||
<div>
|
||||
{isSnapRunning ? (
|
||||
{snapStatus === SnapStatus.INSTALLED ? (
|
||||
<ConnectionOption
|
||||
type="snap"
|
||||
text={
|
||||
@@ -267,22 +268,34 @@ const ConnectorList = ({
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ConnectionOption
|
||||
type="snap"
|
||||
text={
|
||||
<>
|
||||
<div className="flex items-center justify-center w-full h-full text-base gap-1">
|
||||
{t('Install Vega MetaMask Snap')}
|
||||
</div>
|
||||
<div className="absolute top-0 flex items-center h-8 right-1">
|
||||
<VegaIcon name={VegaIconNames.METAMASK} size={24} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
onClick={() => {
|
||||
requestSnap(DEFAULT_SNAP_ID);
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<ConnectionOption
|
||||
type="snap"
|
||||
disabled={snapStatus === SnapStatus.NOT_SUPPORTED}
|
||||
text={
|
||||
<>
|
||||
<div className="flex items-center justify-center w-full h-full text-base gap-1">
|
||||
{t('Install Vega MetaMask Snap')}
|
||||
</div>
|
||||
<div className="absolute top-0 flex items-center h-8 right-1">
|
||||
<VegaIcon name={VegaIconNames.METAMASK} size={24} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
onClick={() => {
|
||||
requestSnap(DEFAULT_SNAP_ID);
|
||||
}}
|
||||
/>
|
||||
{snapStatus === SnapStatus.NOT_SUPPORTED ? (
|
||||
<p className="pt-2 text-sm text-default">
|
||||
{t('No MetaMask version that supports snaps detected.')}{' '}
|
||||
{t('Learn more about')}{' '}
|
||||
<ExternalLink href="https://metamask.io/snaps/">
|
||||
MetaMask Snaps
|
||||
</ExternalLink>
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -118,14 +118,10 @@ export const getSnap = async (
|
||||
snapId: string,
|
||||
version?: string
|
||||
): Promise<Snap | undefined> => {
|
||||
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 <T>(
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>(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;
|
||||
};
|
||||
@@ -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. (<a name="0002-WCON-0013" href="#0002-WCON-0013">0002-WCON-0013</a>)
|
||||
- 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). (<a name="0002-WCON-0013" href="#0002-WCON-0013">0002-WCON-0013</a>)
|
||||
- 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). (<a name="0002-WCON-0014" href="#0002-WCON-0014">0002-WCON-0014</a>)
|
||||
- I can install and connect vega metamask snap wallet (<a name="0002-WCON-0015" href="#0002-WCON-0015">0002-WCON-0015</a>)
|
||||
|
||||
- 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. (<a name="0002-WCON-041" href="#0002-WCON-041">0002-WCON-041</a>)
|
||||
|
||||
@@ -23,13 +23,16 @@
|
||||
- **Must** There is a link to try out trading on Fairground when I'm on Mainnet (<a name="0007-FUGS-008" href="#0007-FUGS-008">0007-FUGS-008</a>)
|
||||
- **Must** There is a link to trade with real funds on Mainnet when I am on Fairground (<a name="0007-FUGS-010" href="#0007-FUGS-010">0007-FUGS-010</a>)
|
||||
- **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 (<a name="0007-FUGS-011" href="#0007-FUGS-011">0007-FUGS-011</a>)
|
||||
- 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 (<a name="0007-FUGS-018" href="#0007-FUGS-018">0007-FUGS-018</a>)
|
||||
- If I dismiss the popup, I land on the default market (<a name="0007-FUGS-012" href="#0007-FUGS-012">0007-FUGS-012</a>)
|
||||
|
||||
## 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 (<a name="0007-FUGS-013" href="#0007-FUGS-013">0007-FUGS-013</a>)
|
||||
- **Must** Remove buttons from pane containers that prompt to connect wallet (<a name="0007-FUGS-014" href="#0007-FUGS-014">0007-FUGS-014</a>)
|
||||
- **Must** We've replaced "connect wallet" in the top right with "get started" (<a name="0007-FUGS-015" href="#0007-FUGS-015">0007-FUGS-015</a>)
|
||||
- **Must** When I press the get started CTA, I see the wallet connect popup (<a name="0007-FUGS-016" href="#0007-FUGS-016">0007-FUGS-016</a>)
|
||||
- **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) (<a name="0007-FUGS-017" href="#0007-FUGS-017">0007-FUGS-017</a>)
|
||||
- **Must** We've replaced "connect wallet" in the top right with "get started" (<a name="0007-FUGS-014" href="#0007-FUGS-014">0007-FUGS-014</a>)
|
||||
- **Must** When I press the get started CTA, I see the wallet connect popup (<a name="0007-FUGS-015" href="#0007-FUGS-015">0007-FUGS-015</a>)
|
||||
- **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) (<a name="0007-FUGS-016" href="#0007-FUGS-016">0007-FUGS-016</a>)
|
||||
|
||||
## When onboarding process has been accomplished:
|
||||
|
||||
- I can see telemetry approval toast: on environment other than mainnet telemetry is enabled by default (<a name="0007-FUGS-018" href="#0007-FUGS-018">0007-FUGS-018</a>)
|
||||
|
||||
@@ -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 (<a name="6003-ORDB-007" href="#6003-ORDB-007">6003-ORDB-007</a>)
|
||||
- I **Must** be able to set a resolution of data (<a name="6003-ORDB-008" href="#6003-ORDB-008">6003-ORDB-008</a>)
|
||||
- When I click specific price, it **Must** be copied to deal ticket form (<a name="6003-ORDB-009" href="#6003-ORDB-009">6003-ORDB-009</a>)
|
||||
- Order is removed from orderbook if traded away(<a name="6003-ORDB-010" href="#6003-ORDB-010">6003-ORDB-010</a>)
|
||||
- Order is removed from orderbook if traded away (<a name="6003-ORDB-010" href="#6003-ORDB-010">6003-ORDB-010</a>)
|
||||
- Spread (bestAsk - bestOffer) is show in the mid secion (<a name="6003-ORDB-011" href="#6003-ORDB-011">6003-ORDB-011</a>)
|
||||
- Cumulative volume is displayed visually (volume bars) as a proportion of the entire book volume (<a name="6003-ORDB-012" href="#6003-ORDB-012">6003-ORDB-012</a>)
|
||||
- Mid section shows the last traded price movement using an arrow (<a name="6003-ORDB-013" href="#6003-ORDB-013">6003-ORDB-013</a>)
|
||||
|
||||
@@ -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 (<a name="7003-SORD-059" href="#7003-SORD-059">7003-SORD-059</a>)
|
||||
|
||||
- **must** see an option to select expire for stop order limit and market (<a name="7002-SORD-071" href="#7002-SORD-071">7002-SORD-071</a>)
|
||||
|
||||
- **must** select strategy for stop order limit and market (<a name="7002-SORD-072" href="#7002-SORD-072">7002-SORD-072</a>)
|
||||
- **must** see strategy submit (<a name="7002-SORD-073" href="#7002-SORD-073">7002-SORD-073</a>)
|
||||
if it is oco then submit strategy is not available (<a name="7002-SORD-081" href="#7002-SORD-081">7002-SORD-081</a>)
|
||||
- **must** see strategy cancel (<a name="7002-SORD-074" href="#7002-SORD-074">7002-SORD-074</a>)
|
||||
- **must** see expiry time/date (<a name="7002-SORD-075" href="#7002-SORD-075">7002-SORD-075</a>)
|
||||
|
||||
- **must** see an option to select oco for stop order limit and market (<a name="7002-SORD-082" href="#7002-SORD-082">7002-SORD-082</a>)
|
||||
- **must** see a type to select market or limit (<a name="7002-SORD-083" href="#7002-SORD-083">7002-SORD-083</a>)
|
||||
- **must** see for market type
|
||||
- **must** see rises above (<a name="7002-SORD-084" href="#7002-SORD-084">7002-SORD-084</a>)
|
||||
- **must** see falls below (<a name="7002-SORD-085" href="#7002-SORD-085">7002-SORD-085</a>)
|
||||
- **must** enter a trigger value (<a name="7002-SORD-086" href="#7002-SORD-086">7002-SORD-086</a>)
|
||||
- **must** select a trigger price (<a name="7002-SORD-087" href="#7002-SORD-087">7002-SORD-087</a>)
|
||||
- **must** select a trigger trailing percent offset (<a name="7002-SORD-088" href="#7002-SORD-088">7002-SORD-088</a>)
|
||||
- **must** see for limit type
|
||||
- **must** enter a price (<a name="7002-SORD-089" href="#7002-SORD-089">7002-SORD-089</a>)
|
||||
- **must** select a trigger direction (<a name="7002-SORD-090" href="#7002-SORD-090">7002-SORD-090</a>)
|
||||
- **must** see rises above (<a name="7002-SORD-099" href="#7002-SORD-099">7002-SORD-099</a>)
|
||||
- **must** see falls below (<a name="7002-SORD-091" href="#7002-SORD-091">7002-SORD-091</a>)
|
||||
- if the user has not set a preference: trigger direction **must** default to `opposite to the one selected in the stop order` (<a name="7002-SORD-092" href="#7002-SORD-092">7002-SORD-092</a>)
|
||||
- **must** enter a trigger value (<a name="7002-SORD-093" href="#7002-SORD-093">7002-SORD-093</a>)
|
||||
- **must** select a trigger type (<a name="7002-SORD-094" href="#7002-SORD-094">7002-SORD-094</a>)
|
||||
- **must** see a trigger price (<a name="7002-SORD-095" href="#7002-SORD-095">7002-SORD-095</a>)
|
||||
- **must** see a trigger trailing percent offset (<a name="7002-SORD-096" href="#7002-SORD-096">7002-SORD-096</a>)
|
||||
- if the user has not set a preference: trigger type **must** default to `price` (<a name="7002-SORD-097" href="#7002-SORD-097">7002-SORD-097</a>)
|
||||
- if the trigger price is already met on submission, **must** warn the user that the stop order will be triggered immediately (<a name="7002-SORD-098" href="#7002-SORD-098">7002-SORD-098</a>)
|
||||
|
||||
... so I can control in more detail how my order is executed
|
||||
|
||||
## Auto Populating a deal ticket non-manual methods
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user