-
- {name}
+
{Array.from(rewards.values()).map(({ rewardType, amount }, i) => (
data.id;
const defaultColDef = {
sortable: true,
- filter: true,
+ filter: false,
resizable: true,
filterParams: { buttons: ['reset'] },
minWidth: 100,
@@ -20,7 +22,9 @@ const components = {
PriceFlashCell,
};
-type Props = TypedDataAgGrid;
+type Props = TypedDataAgGrid & {
+ filterSummary?: ReactNode;
+};
export type DataGridSlice = {
gridStore: DataGridStore;
@@ -45,7 +49,7 @@ export const useMarketsStore = create()(
})
);
-export const MarketListTable = (props: Props) => {
+export const MarketListTable = ({ filterSummary, ...props }: Props) => {
const columnDefs = useMarketsColumnDefs();
return (
@@ -55,14 +59,49 @@ export const MarketListTable = (props: Props) => {
columnDefs={columnDefs}
components={components}
rowHeight={60}
+ rowClass={
+ '!border-b !last:border-b-0 mb-1 border-vega-clight-600 dark:border-vega-cdark-600'
+ }
headerHeight={40}
domLayout="autoHeight"
autoSizeStrategy={{
type: 'fitGridWidth',
}}
+ isFullWidthRow={(params) => {
+ const data = params.rowNode.data;
+ return isFilterSummaryRow(data);
+ }}
+ fullWidthCellRenderer={FullWidthCellRenderer}
+ getRowHeight={(params) => {
+ const data = params.data;
+ if (isFilterSummaryRow(data)) {
+ return 30;
+ }
+ return 60;
+ }}
+ pinnedTopRowData={
+ filterSummary
+ ? [
+ {
+ id: 'summary',
+ filterSummary,
+ },
+ ]
+ : []
+ }
{...props}
/>
);
};
+const isFilterSummaryRow = (data: unknown) =>
+ Boolean(data && typeof data === 'object' && 'filterSummary' in data);
+
+const FullWidthCellRenderer = ({ data }: ICellRendererParams) => {
+ if (isFilterSummaryRow(data)) {
+ return {data.filterSummary}
;
+ }
+ return null;
+};
+
export default MarketListTable;
diff --git a/apps/trading/client-pages/markets/markets-page.tsx b/apps/trading/client-pages/markets/markets-page.tsx
index 992da314bd..3da013bc8e 100644
--- a/apps/trading/client-pages/markets/markets-page.tsx
+++ b/apps/trading/client-pages/markets/markets-page.tsx
@@ -1,19 +1,26 @@
-import { Sparkline, TinyScroll } from '@vegaprotocol/ui-toolkit';
-import { OpenMarkets } from './open-markets';
-import { Proposed } from './proposed';
-import { Closed } from './closed';
+import {
+ MultiSelect,
+ MultiSelectOption,
+ Sparkline,
+ TinyScroll,
+ TradingInput,
+ VegaIcon,
+ VegaIconNames,
+} from '@vegaprotocol/ui-toolkit';
import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary';
import { usePageTitle } from '../../lib/hooks/use-page-title';
import { Card } from '../../components/card';
import { useDataProvider } from '@vegaprotocol/data-provider';
import {
- activeMarketsWithCandlesProvider,
+ type MarketMaybeWithData,
calcCandleVolumePrice,
+ marketsWithCandlesProvider,
+ retrieveAssets,
type MarketMaybeWithCandles,
} from '@vegaprotocol/markets';
import { useYesterday } from '@vegaprotocol/react-helpers';
-import { useEffect, useState } from 'react';
+import { type ReactNode, useEffect } from 'react';
import { Interval } from '@vegaprotocol/types';
import { formatNumber } from '@vegaprotocol/utils';
import { TopMarketList } from './top-market-list';
@@ -24,18 +31,41 @@ import {
useTotalVolume24hCandles,
} from '../../lib/hooks/use-markets-stats';
import { useTotalValueLocked } from '../../lib/hooks/use-total-volume-locked';
+import uniq from 'lodash/uniq';
+import trim from 'lodash/trim';
+import flatten from 'lodash/flatten';
+import compact from 'lodash/compact';
+import uniqBy from 'lodash/uniqBy';
+import orderBy from 'lodash/orderBy';
+import { getChainName } from '@vegaprotocol/web3';
+import { type AssetFieldsFragment } from '@vegaprotocol/assets';
+import {
+ DEFAULT_FILTERS,
+ type IMarketState,
+ type IMarketType,
+ MarketType,
+ MarketState,
+ filterMarkets,
+ useMarketFiltersStore,
+ filterMarket,
+} from '../../lib/hooks/use-market-filters';
+import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
+import MarketListTable from './market-list-table';
+import type { CellClickedEvent, IRowNode } from 'ag-grid-community';
+import { FilterSummary } from '../../components/market-selector/filter-summary';
const POLLING_TIME = 2000;
export const MarketsPage = () => {
const t = useT();
const yesterday = useYesterday();
+
const {
- data: activeMarkets,
+ data: allMarkets,
error,
reload,
} = useDataProvider({
- dataProvider: activeMarketsWithCandlesProvider,
+ dataProvider: marketsWithCandlesProvider,
variables: {
since: new Date(yesterday).toISOString(),
interval: Interval.INTERVAL_I1H,
@@ -52,16 +82,16 @@ export const MarketsPage = () => {
usePageTitle(t('Markets'));
- const topGainers = useTopGainers(activeMarkets);
- const newListings = useNewListings(activeMarkets);
- const totalVolume24hCandles = useTotalVolume24hCandles(activeMarkets);
+ const topGainers = useTopGainers(allMarkets);
+ const newListings = useNewListings(allMarkets);
+ const totalVolume24hCandles = useTotalVolume24hCandles(allMarkets);
const totalVolumeSparkline = (
);
const { tvl, loading: tvlLoading, error: tvlError } = useTotalValueLocked();
- const totalVolume24h = activeMarkets?.reduce((acc, market) => {
+ const totalVolume24h = allMarkets?.reduce((acc, market) => {
return (
acc +
Number(
@@ -74,10 +104,24 @@ export const MarketsPage = () => {
);
}, 0);
+ const marketAssets = uniqBy(
+ compact(
+ flatten(
+ allMarkets?.map((m) => {
+ const product = m.tradableInstrument?.instrument?.product;
+ if (product) {
+ return retrieveAssets(product);
+ }
+ })
+ )
+ ),
+ (a) => a.id
+ );
+
return (
-
+
@@ -125,78 +169,279 @@ export const MarketsPage = () => {
-
+
);
};
-export const MarketTables = ({
- activeMarkets,
+export const MarketTable = ({
+ markets,
+ marketAssets,
error,
}: {
- activeMarkets: MarketMaybeWithCandles[] | null;
+ markets: MarketMaybeWithCandles[] | null;
+ marketAssets: AssetFieldsFragment[];
error: Error | undefined;
}) => {
const t = useT();
- const [activeTab, setActiveTab] = useState('open-markets');
-
- const marketTabs: {
- [key: string]: { id: string; name: string };
- } = {
- open: {
- id: 'open-markets',
- name: t('Open'),
- },
- proposed: {
- id: 'proposed-markets',
- name: t('Proposed'),
- },
- closed: {
- id: 'closed-markets',
- name: t('Closed'),
- },
+
+ const {
+ marketTypes,
+ marketStates,
+ assets,
+ searchTerm,
+ setMarketTypes,
+ setMarketStates,
+ setAssets,
+ setSearchTerm,
+ reset,
+ } = useMarketFiltersStore((state) => ({
+ marketTypes: state.marketTypes,
+ setMarketTypes: state.setMarketTypes,
+ marketStates: state.marketStates,
+ setMarketStates: state.setMarketStates,
+ assets: state.assets,
+ setAssets: state.setAssets,
+ searchTerm: state.searchTerm,
+ setSearchTerm: state.setSearchTerm,
+ reset: state.reset,
+ }));
+
+ const isMarketTypeSelected = (marketType: IMarketType) =>
+ marketTypes.includes(marketType);
+
+ const marketTypeFilterOptions: Record
= {
+ [MarketType.PERPETUAL]: t('Perpetuals'),
+ [MarketType.FUTURE]: t('Futures'),
+ [MarketType.SPOT]: t('Spot'),
+ };
+
+ const marketStateFilterOptions: Record = {
+ [MarketState.OPEN]: t('Open'),
+ [MarketState.CLOSED]: t('Closed'),
+ [MarketState.PROPOSED]: t('Proposed'),
+ };
+ let marketStateTrigger: string | undefined = undefined;
+ if (marketStates.length > 0) {
+ if (marketStates.length === 1) {
+ marketStateTrigger = marketStateFilterOptions[marketStates[0]];
+ }
+ if (marketStates.length > 1) {
+ marketStateTrigger = t('Status ({{count}})', {
+ count: marketStates.length,
+ });
+ }
+ }
+
+ const assetFilterOptions = orderBy(marketAssets, (a) => a.symbol, 'asc');
+ let assetTrigger: string | undefined = undefined;
+ if (assets.length > 0) {
+ if (assets.length === 1) {
+ assetTrigger = marketAssets?.find((a) => a.id === assets[0])?.symbol;
+ }
+ if (assets.length > 1) {
+ assetTrigger = t('Assets ({{count}})', { count: assets.length });
+ }
+ }
+
+ const filteredMarkets = filterMarkets(markets || [], {
+ marketTypes,
+ marketStates,
+ assets,
+ searchTerm,
+ });
+ const defaultMarkets = filterMarkets(markets || [], DEFAULT_FILTERS);
+
+ let filterSummary: ReactNode = undefined;
+ if (filteredMarkets.length != defaultMarkets.length) {
+ const diff = defaultMarkets.length - filteredMarkets.length;
+ filterSummary = ;
+ }
+
+ const handleOnSelect = useMarketClickHandler();
+
+ const isExternalFilterPresent = () => true;
+ const doesExternalFilterPass = (
+ rowData: IRowNode
+ ) => {
+ const market = rowData.data;
+ if (!market) return false;
+ return filterMarket(market, {
+ marketTypes,
+ marketStates,
+ assets,
+ searchTerm,
+ });
};
return (
-
- {Object.keys(marketTabs).map((key: string) => (
+
+ {/** MARKET TYPE FILTER */}
+
- ))}
+ {Object.keys(marketTypeFilterOptions).map((key) => {
+ const marketType = key as IMarketType;
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {Object.keys(marketStateFilterOptions).map((key) => {
+ const marketState = key as IMarketState;
+ const isChecked = marketStates.includes(marketState);
+ return (
+ {
+ if (checked) {
+ setMarketStates(uniq([...marketStates, marketState]));
+ } else {
+ setMarketStates(
+ marketStates.filter((s) => s !== marketState)
+ );
+ }
+ }}
+ key={key}
+ >
+ {marketStateFilterOptions[marketState]}
+
+ );
+ })}
+
+
+
+
+ {assetFilterOptions.map((asset) => {
+ const chainName = getChainName(
+ asset.source.__typename === 'ERC20'
+ ? Number(asset.source.chainId)
+ : undefined
+ );
+ const isChecked = assets.includes(asset.id);
+ return (
+ {
+ if (checked) {
+ setAssets(uniq([...assets, asset.id]));
+ } else {
+ setAssets(assets.filter((a) => a !== asset.id));
+ }
+ }}
+ key={asset.id}
+ >
+ {asset.symbol}{' '}
+ ({chainName})
+
+ );
+ })}
+
+
+ {/** MARKET NAME FILTER */}
+
+ }
+ placeholder={t('Search by market')}
+ className="text-sm border !placeholder:text-secondary"
+ onChange={(ev) => {
+ const value = trim(ev.target.value);
+ setSearchTerm(value);
+ }}
+ value={searchTerm}
+ defaultValue={''}
+ />
+
+
- {activeTab === 'open-markets' && (
-
-
-
- )}
- {activeTab === 'proposed-markets' && (
-
-
-
- )}
- {activeTab === 'closed-markets' && (
-
-
-
- )}
+
+ ) => {
+ if (!data) return;
+
+ // prevent navigating to the market page if any of the below cells are clicked
+ // event.preventDefault or event.stopPropagation do not seem to apply for ag-grid
+ const colId = column.getColId();
+
+ if (
+ [
+ 'tradableInstrument.instrument.product.settlementAsset.symbol',
+ 'market-actions',
+ ].includes(colId)
+ ) {
+ return;
+ }
+
+ // @ts-ignore metaKey exists
+ handleOnSelect(data.id, event ? event.metaKey : false);
+ }}
+ overlayNoRowsTemplate={error ? error.message : t('No markets')}
+ suppressNoRowsOverlay
+ isExternalFilterPresent={isExternalFilterPresent}
+ doesExternalFilterPass={doesExternalFilterPass}
+ />
+
);
diff --git a/apps/trading/client-pages/markets/open-markets.tsx b/apps/trading/client-pages/markets/open-markets.tsx
index 33f39e173e..6e2b27beea 100644
--- a/apps/trading/client-pages/markets/open-markets.tsx
+++ b/apps/trading/client-pages/markets/open-markets.tsx
@@ -41,6 +41,7 @@ export const OpenMarkets = ({
handleOnSelect(data.id, event ? event.metaKey : false);
}}
overlayNoRowsTemplate={error ? error.message : t('No markets')}
+ suppressNoRowsOverlay
/>
);
};
diff --git a/apps/trading/client-pages/markets/use-column-defs.tsx b/apps/trading/client-pages/markets/use-column-defs.tsx
index 89631ba001..9126ea1f53 100644
--- a/apps/trading/client-pages/markets/use-column-defs.tsx
+++ b/apps/trading/client-pages/markets/use-column-defs.tsx
@@ -117,7 +117,7 @@ export const priceValueFormatter = (
data: MarketMaybeWithData | undefined,
formatDecimalPlaces?: number
): string => {
- if (data?.tradableInstrument.instrument.product.__typename === 'Spot') {
+ if (isSpotMarket(data)) {
const quoteAsset = data && getQuoteAsset(data);
return data?.data?.lastTradedPrice === undefined
? '-'
@@ -126,7 +126,10 @@ export const priceValueFormatter = (
data.decimalPlaces
)} ${quoteAsset?.symbol}`;
}
- const quoteName = data && getQuoteName(data);
+ let quoteName: string | undefined = undefined;
+ if (data?.tradableInstrument?.instrument?.product) {
+ quoteName = data && getQuoteName(data);
+ }
return data?.data?.bestOfferPrice === undefined
? '-'
@@ -166,7 +169,7 @@ export const useMarketsColumnDefs = () => {
const state = data?.data?.marketState;
const tooltip = getMarketStateTooltip(state, tradingMode);
const productType =
- data?.tradableInstrument.instrument.product.__typename;
+ data?.tradableInstrument?.instrument.product.__typename;
return (
@@ -179,7 +182,7 @@ export const useMarketsColumnDefs = () => {
}
- secondary={data?.tradableInstrument.instrument.name}
+ secondary={data?.tradableInstrument?.instrument.name}
/>
@@ -194,7 +197,7 @@ export const useMarketsColumnDefs = () => {
maxWidth: 150,
cellClass: 'text-sm text-right',
valueGetter: ({ data }: VegaValueGetterParams
) => {
- if (data && isSpot(data.tradableInstrument.instrument.product)) {
+ if (isSpotMarket(data)) {
return data?.data?.lastTradedPrice === undefined
? undefined
: toBigNum(
@@ -264,7 +267,12 @@ export const useMarketsColumnDefs = () => {
if (!data) return '-';
const candles = data.candles;
const vol = candles ? calcCandleVolume(candles) : '0';
- const quoteName = getQuoteName(data);
+ let quoteName: string | undefined = undefined;
+ try {
+ quoteName = getQuoteName(data);
+ } catch {
+ quoteName = '';
+ }
const volPrice = candles
? calcCandleVolumePrice(
candles,
@@ -312,7 +320,7 @@ export const useMarketsColumnDefs = () => {
MarketMaybeWithData,
'data.openInterest'
>) => {
- if (!data || isSpot(data.tradableInstrument.instrument.product)) {
+ if (!data || isSpotMarket(data) || !openInterestValues(data)) {
return -;
}
return (
@@ -326,3 +334,10 @@ export const useMarketsColumnDefs = () => {
[chainId, t]
);
};
+
+const isSpotMarket = (
+ market: Pick | undefined | null
+) => {
+ const product = market?.tradableInstrument?.instrument?.product;
+ return product && isSpot(product);
+};
diff --git a/apps/trading/components/accounts-container/sidebar-accounts-container.tsx b/apps/trading/components/accounts-container/sidebar-accounts-container.tsx
index 1f47c4ddee..967c3d4007 100644
--- a/apps/trading/components/accounts-container/sidebar-accounts-container.tsx
+++ b/apps/trading/components/accounts-container/sidebar-accounts-container.tsx
@@ -1,4 +1,3 @@
-import { useState } from 'react';
import { useT } from '../../lib/use-t';
import { SwapContainer } from '../swap';
import { WithdrawContainer } from '../withdraw-container';
@@ -10,21 +9,36 @@ import React from 'react';
import { DepositContainer } from '../deposit-container';
import { TransferContainer } from '@vegaprotocol/accounts';
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
+import { create } from 'zustand';
-enum View {
+export enum SidebarAccountsViewType {
Deposit = 'Deposit',
Swap = 'Swap',
Transfer = 'Transfer',
Withdraw = 'Withdraw',
}
-type InnerView = [view: View, assetId: string];
+type InnerView = [view: SidebarAccountsViewType, assetId: string];
+
+type SidebarAccountsInnerViewStore = {
+ view: InnerView | undefined;
+ setView: (view: InnerView | undefined) => void;
+};
+export const useSidebarAccountsInnerView =
+ create()((set) => ({
+ view: undefined,
+ setView: (view) => set({ view }),
+ }));
export const SidebarAccountsContainer = ({
pinnedAssets,
}: Pick) => {
const t = useT();
- const [innerView, setInnerView] = useState(undefined);
+
+ const [innerView, setInnerView] = useSidebarAccountsInnerView((state) => [
+ state.view,
+ state.setView,
+ ]);
return (
<>
@@ -54,16 +68,16 @@ export const SidebarAccountsContainer = ({
orderByBalance
hideZeroBalance
onClickDeposit={(assetId) => {
- setInnerView([View.Deposit, assetId]);
+ setInnerView([SidebarAccountsViewType.Deposit, assetId]);
}}
onClickSwap={(assetId) => {
- setInnerView([View.Swap, assetId]);
+ setInnerView([SidebarAccountsViewType.Swap, assetId]);
}}
onClickTransfer={(assetId) => {
- setInnerView([View.Transfer, assetId]);
+ setInnerView([SidebarAccountsViewType.Transfer, assetId]);
}}
onClickWithdraw={(assetId) => {
- setInnerView([View.Withdraw, assetId]);
+ setInnerView([SidebarAccountsViewType.Withdraw, assetId]);
}}
/>
@@ -80,24 +94,24 @@ const InnerContainer = ({
}) => {
const [view, assetId] = innerView;
switch (view) {
- case View.Deposit:
+ case SidebarAccountsViewType.Deposit:
return ;
- case View.Swap:
+ case SidebarAccountsViewType.Swap:
return (
{
if (!assetId) return;
- setInnerView?.([View.Deposit, assetId]);
+ setInnerView?.([SidebarAccountsViewType.Deposit, assetId]);
}}
/>
);
- case View.Transfer:
+ case SidebarAccountsViewType.Transfer:
return ;
- case View.Withdraw:
+ case SidebarAccountsViewType.Withdraw:
return ;
}
};
diff --git a/apps/trading/components/asset-card/asset-card.tsx b/apps/trading/components/asset-card/asset-card.tsx
index 7b88fb79da..3fccea7229 100644
--- a/apps/trading/components/asset-card/asset-card.tsx
+++ b/apps/trading/components/asset-card/asset-card.tsx
@@ -67,7 +67,8 @@ export const AssetCard = ({
asset.decimals,
asset.quantum
)}
- {allocatedRatio && ` (${formatNumber(allocatedRatio)}%)`}
+ {allocatedRatio &&
+ ` (${formatNumber(allocatedRatio.times(100))}%)`}
)}
diff --git a/apps/trading/components/deposit-container/deposit-container.tsx b/apps/trading/components/deposit-container/deposit-container.tsx
index b3cea584e5..7e09b98f69 100644
--- a/apps/trading/components/deposit-container/deposit-container.tsx
+++ b/apps/trading/components/deposit-container/deposit-container.tsx
@@ -147,6 +147,7 @@ const DepositForm = ({
return (
+ {filterSummary ? (
+
{filterSummary}
+ ) : null}
@@ -219,7 +262,6 @@ interface ListItemData {
data: MarketMaybeWithDataAndCandles[];
onSelect: (marketId: string) => void;
currentMarketId?: string;
- allProducts: boolean;
}
const ListItem = ({
@@ -236,7 +278,6 @@ const ListItem = ({
currentMarketId={data.currentMarketId}
style={style}
onSelect={data.onSelect}
- allProducts={data.allProducts}
/>
);
@@ -248,21 +289,19 @@ const List = ({
onSelect,
noItems,
currentMarketId,
- allProducts,
}: ListItemData & {
loading: boolean;
height: number;
itemSize: number;
noItems: string;
- allProducts: boolean;
}) => {
const itemKey = useCallback(
(index: number, data: ListItemData) => data.data[index].id,
[]
);
const itemData = useMemo(
- () => ({ data, onSelect, currentMarketId, allProducts }),
- [data, onSelect, currentMarketId, allProducts]
+ () => ({ data, onSelect, currentMarketId }),
+ [data, onSelect, currentMarketId]
);
if (!data || loading) {
return (
diff --git a/apps/trading/components/market-selector/product-selector.tsx b/apps/trading/components/market-selector/product-selector.tsx
index 67ee4adb67..cd4a3aa5e6 100644
--- a/apps/trading/components/market-selector/product-selector.tsx
+++ b/apps/trading/components/market-selector/product-selector.tsx
@@ -3,54 +3,65 @@ import { Link } from 'react-router-dom';
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { Links } from '../../lib/links';
import { useT } from '../../lib/use-t';
-
-// Make sure these match the available __typename properties on product
-export const Product = {
- All: 'All',
- Future: 'Future',
- Spot: 'Spot',
- Perpetual: 'Perpetual',
-} as const;
-
-export type ProductType = keyof typeof Product;
+import {
+ type IMarketType,
+ MarketType,
+} from '../../lib/hooks/use-market-filters';
export const ProductSelector = ({
- product,
+ marketTypes,
onSelect,
}: {
- product: ProductType;
- onSelect: (product: ProductType) => void;
+ marketTypes: IMarketType[];
+ onSelect: (marketType?: IMarketType) => void;
}) => {
const t = useT();
+
const ProductTypeMapping: {
- [key in ProductType]: string;
+ [key in IMarketType]: string;
} = {
- [Product.All]: t('All'),
- [Product.Future]: t('Futures'),
- [Product.Spot]: t('Spot'),
- [Product.Perpetual]: t('Perpetuals'),
+ [MarketType.PERPETUAL]: t('Perpetuals'),
+ [MarketType.FUTURE]: t('Futures'),
+ [MarketType.SPOT]: t('Spot'),
};
+
+ const buttons = [MarketType.PERPETUAL, MarketType.FUTURE, MarketType.SPOT];
+
+ const getStyles = (selected: boolean) =>
+ classNames(
+ 'text-sm px-3 py-1.5 rounded hover:text-vega-clight-50 dark:hover:text-vega-cdark-50',
+ {
+ 'bg-vega-clight-500 dark:bg-vega-cdark-500 text-default': selected,
+ 'text-secondary': !selected,
+ }
+ );
+
return (
- {Object.keys(Product).map((t) => {
- const classes = classNames(
- 'text-sm px-3 py-1.5 rounded hover:text-vega-clight-50 dark:hover:text-vega-cdark-50',
- {
- 'bg-vega-clight-500 dark:bg-vega-cdark-500 text-default':
- t === product,
- 'text-secondary': t !== product,
- }
- );
+
+ {buttons.map((t) => {
return (
);
})}
diff --git a/apps/trading/components/market-selector/sort-dropdown.tsx b/apps/trading/components/market-selector/sort-dropdown.tsx
index c2ba4097fb..942d496b02 100644
--- a/apps/trading/components/market-selector/sort-dropdown.tsx
+++ b/apps/trading/components/market-selector/sort-dropdown.tsx
@@ -9,41 +9,43 @@ import {
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { MarketSelectorButton } from './market-selector-button';
-
-export const Sort = {
- Gained: 'Gained',
- Lost: 'Lost',
- New: 'New',
- TopTraded: 'TopTraded',
-} as const;
-
-export type SortType = keyof typeof Sort;
+import {
+ type ISortOption,
+ SortOption,
+} from '../../lib/hooks/use-market-filters';
export const SortTypeMapping: {
- [key in SortType]: string;
+ [key in ISortOption]: string;
} = {
- [Sort.TopTraded]: 'Top traded',
- [Sort.Gained]: 'Top gaining',
- [Sort.Lost]: 'Top losing',
- [Sort.New]: 'New markets',
+ [SortOption.TOP_TRADED]: 'Top traded',
+ [SortOption.GAINED]: 'Top gaining',
+ [SortOption.LOST]: 'Top losing',
+ [SortOption.NEW]: 'New markets',
};
const SortIconMapping: {
- [key in SortType]: VegaIconNames;
+ [key in ISortOption]: VegaIconNames;
} = {
- [Sort.Gained]: VegaIconNames.TREND_UP,
- [Sort.Lost]: VegaIconNames.TREND_DOWN,
- [Sort.New]: VegaIconNames.STAR,
- [Sort.TopTraded]: VegaIconNames.ARROW_UP,
+ [SortOption.TOP_TRADED]: VegaIconNames.TREND_UP,
+ [SortOption.GAINED]: VegaIconNames.TREND_DOWN,
+ [SortOption.LOST]: VegaIconNames.STAR,
+ [SortOption.NEW]: VegaIconNames.ARROW_UP,
};
export const SortDropdown = ({
currentSort,
onSelect,
}: {
- currentSort: SortType;
- onSelect: (sort: SortType) => void;
+ currentSort: ISortOption;
+ onSelect: (sortOrder: ISortOption) => void;
}) => {
+ const options = [
+ SortOption.GAINED,
+ SortOption.LOST,
+ SortOption.NEW,
+ SortOption.TOP_TRADED,
+ ];
+
return (
onSelect(value as SortType)}
+ onValueChange={(value) => onSelect(value as ISortOption)}
>
- {Object.keys(Sort).map((key) => {
+ {options.map((option) => {
return (
- {' '}
- {SortTypeMapping[key as SortType]}
+ {' '}
+ {SortTypeMapping[option]}
diff --git a/apps/trading/components/market-selector/use-market-selector-list.spec.tsx b/apps/trading/components/market-selector/use-market-selector-list.spec.tsx
deleted file mode 100644
index 11ccae40c8..0000000000
--- a/apps/trading/components/market-selector/use-market-selector-list.spec.tsx
+++ /dev/null
@@ -1,605 +0,0 @@
-import merge from 'lodash/merge';
-import { renderHook } from '@testing-library/react';
-import { useMarketSelectorList } from './use-market-selector-list';
-import { isMarketActive } from '../../lib/utils';
-import { Product } from './product-selector';
-import { Sort } from './sort-dropdown';
-import {
- createMarketFragment,
- createMarketsDataFragment,
-} from '@vegaprotocol/mock';
-import { MarketState } from '@vegaprotocol/types';
-import { useMarketList } from '@vegaprotocol/markets';
-import type { Filter } from './market-selector';
-import { subDays } from 'date-fns';
-
-jest.mock('@vegaprotocol/markets', () => ({
- ...jest.requireActual('@vegaprotocol/markets'),
- useMarketList: jest.fn(),
-}));
-const mockUseMarketList = useMarketList as jest.Mock;
-
-describe('useMarketSelectorList', () => {
- const setup = (initialArgs?: Partial) => {
- const defaultArgs: Filter = {
- searchTerm: '',
- product: Product.Future,
- sort: Sort.TopTraded,
- assets: [],
- };
- return renderHook((args) => useMarketSelectorList(args), {
- initialProps: merge(defaultArgs, initialArgs),
- });
- };
-
- it('returns all markets active and suspended markets', () => {
- const markets = [
- createMarketFragment({
- id: 'market-0',
- // @ts-ignore candles get joined outside this type
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- }),
- createMarketFragment({
- id: 'market-1',
- // @ts-ignore candles get joined outside this type
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_SUSPENDED,
- }),
- }),
- createMarketFragment({
- id: 'market-2',
- // @ts-ignore candles get joined outside this type
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_CLOSED,
- }),
- }),
- createMarketFragment({
- id: 'market-3',
- // @ts-ignore candles get joined outside this type
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_CLOSED,
- }),
- }),
- createMarketFragment({
- id: 'market-4',
- // @ts-ignore candles get joined outside this type
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_PENDING,
- }),
- }),
- ];
- mockUseMarketList.mockReturnValue({
- data: markets,
- loading: false,
- error: undefined,
- });
- const { result } = setup();
- const expectedFilteredMarkets = markets.filter((m) =>
- // @ts-ignore candles get joined outside this type
- isMarketActive(m.data.marketState)
- );
- expect(result.current).toEqual({
- data: markets,
- markets: expectedFilteredMarkets,
- loading: false,
- error: undefined,
- });
- });
-
- it('filters by product', () => {
- const markets = [
- createMarketFragment({
- id: 'market-0',
- // @ts-ignore candles get joined outside this type
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- tradableInstrument: {
- instrument: {
- product: {
- __typename: 'Future',
- },
- },
- },
- }),
- // createMarketFragment({
- // id: 'market-1',
- // tradableInstrument: {
- // instrument: {
- // product: {
- // __typename: 'Spot',
- // },
- // },
- // },
- // }),
- createMarketFragment({
- id: 'market-2',
- // @ts-ignore candles get joined outside this type
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- tradableInstrument: {
- instrument: {
- product: {
- __typename: 'Perpetual',
- },
- },
- },
- }),
- ];
-
- mockUseMarketList.mockReturnValue({
- data: markets,
- loading: false,
- error: undefined,
- });
- const { result, rerender } = setup();
- expect(result.current.markets).toEqual([markets[0]]);
- // rerender({
- // searchTerm: '',
- // product: Product.Spot as 'Future',
- // sort: Sort.TopTraded,
- // assets: [],
- // });
- // expect(result.current.markets).toEqual([markets[1]]);
- rerender({
- searchTerm: '',
- product: Product.Perpetual as 'Future',
- sort: Sort.TopTraded,
- assets: [],
- });
- // expect(result.current.markets).toEqual([markets[2]]);
- rerender({
- searchTerm: '',
- product: Product.All,
- sort: Sort.TopTraded,
- assets: [],
- });
- expect(result.current.markets).toEqual(markets);
- });
-
- // eslint-disable-next-line jest/no-disabled-tests
- it.skip('filters by asset', () => {
- const markets = [
- createMarketFragment({
- id: 'market-0',
- // @ts-ignore candles get joined outside this type
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- tradableInstrument: {
- instrument: {
- product: {
- __typename: 'Future',
- settlementAsset: {
- id: 'asset-0',
- },
- },
- },
- },
- }),
- createMarketFragment({
- id: 'market-1',
- // @ts-ignore candles get joined outside this type
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- tradableInstrument: {
- instrument: {
- product: {
- __typename: 'Future',
- settlementAsset: {
- id: 'asset-0',
- },
- },
- },
- },
- }),
- createMarketFragment({
- id: 'market-2',
- // @ts-ignore candles get joined outside this type
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- tradableInstrument: {
- instrument: {
- product: {
- __typename: 'Future',
- settlementAsset: {
- id: 'asset-1',
- },
- },
- },
- },
- }),
- createMarketFragment({
- id: 'market-3',
- // @ts-ignore candles get joined outside this type
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- tradableInstrument: {
- instrument: {
- product: {
- __typename: 'Future',
- settlementAsset: {
- id: 'asset-2',
- },
- },
- },
- },
- }),
- ];
-
- mockUseMarketList.mockReturnValue({
- data: markets,
- loading: false,
- error: undefined,
- });
- const { result, rerender } = setup({
- searchTerm: '',
- product: Product.Future,
- sort: Sort.TopTraded,
- assets: ['asset-0'],
- });
-
- expect(result.current.markets).toEqual([markets[0], markets[1]]);
-
- rerender({
- searchTerm: '',
- product: Product.Future,
- sort: Sort.TopTraded,
- assets: ['asset-0', 'asset-1'],
- });
-
- expect(result.current.markets).toEqual([
- markets[0],
- markets[1],
- markets[2],
- ]);
-
- rerender({
- searchTerm: '',
- product: Product.Future,
- sort: Sort.TopTraded,
- assets: ['asset-0', 'asset-1', 'asset-2'],
- });
-
- // all assets selected
- expect(result.current.markets).toEqual(markets);
-
- rerender({
- searchTerm: '',
- product: Product.Future,
- sort: Sort.TopTraded,
- assets: ['asset-invalid'],
- });
-
- expect(result.current.markets).toEqual([]);
- });
-
- it('filters by search term', () => {
- const markets = [
- createMarketFragment({
- id: 'market-0',
- // @ts-ignore candles get joined outside this type
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- tradableInstrument: {
- instrument: {
- code: 'abc',
- name: 'aaa',
- },
- },
- }),
- createMarketFragment({
- id: 'market-1',
- // @ts-ignore candles get joined outside this type
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- tradableInstrument: {
- instrument: {
- code: 'def',
- name: 'ggg',
- },
- },
- }),
- createMarketFragment({
- id: 'market-2',
- // @ts-ignore candles get joined outside this type
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- tradableInstrument: {
- instrument: {
- code: 'defg',
- name: 'gggh',
- },
- },
- }),
- createMarketFragment({
- id: 'market-3',
- // @ts-ignore candles get joined outside this type
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- tradableInstrument: {
- instrument: {
- code: 'ggg',
- name: 'foo',
- },
- },
- }),
- ];
-
- mockUseMarketList.mockReturnValue({
- data: markets,
- loading: false,
- error: undefined,
- });
- const { result, rerender } = setup({
- searchTerm: 'abc',
- product: Product.Future,
- sort: Sort.TopTraded,
- assets: [],
- });
- expect(result.current.markets).toEqual([markets[0]]);
- rerender({
- searchTerm: 'def',
- product: Product.Future,
- sort: Sort.TopTraded,
- assets: [],
- });
- expect(result.current.markets).toEqual([markets[1], markets[2]]);
- rerender({
- searchTerm: 'defg',
- product: Product.Future,
- sort: Sort.TopTraded,
- assets: [],
- });
- expect(result.current.markets).toEqual([markets[2]]);
- rerender({
- searchTerm: 'zzz',
- product: Product.Future,
- sort: Sort.TopTraded,
- assets: [],
- });
- expect(result.current.markets).toEqual([]);
-
- // by name
- rerender({
- searchTerm: 'aaa',
- product: Product.Future,
- sort: Sort.TopTraded,
- assets: [],
- });
- expect(result.current.markets).toEqual([markets[0]]);
- rerender({
- searchTerm: 'ggg',
- product: Product.Future,
- sort: Sort.TopTraded,
- assets: [],
- });
- expect(result.current.markets).toEqual([
- markets[1],
- markets[2],
- markets[3],
- ]);
- });
-
- it('sorts by top traded by default', () => {
- const markets = [
- createMarketFragment({
- id: 'market-0',
- // @ts-ignore data not on fragment
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- markPrice: '1',
- }),
- // @ts-ignore candles not on fragment
- candles: [
- {
- volume: '200',
- },
- ],
- }),
- createMarketFragment({
- id: 'market-1',
- // @ts-ignore data not on fragment
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- markPrice: '1',
- }),
- // @ts-ignore candles not on fragment
- candles: [
- {
- volume: '100',
- },
- ],
- }),
- createMarketFragment({
- id: 'market-2',
- // @ts-ignore data not on fragment
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- markPrice: '1',
- }),
- // @ts-ignore candles not on fragment
- candles: [
- {
- volume: '300',
- },
- ],
- }),
- createMarketFragment({
- id: 'market-3',
- // @ts-ignore data not on fragment
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- markPrice: '1',
- }),
- // @ts-ignore candles not on fragment
- candles: [
- {
- volume: '400',
- },
- ],
- }),
- ];
-
- mockUseMarketList.mockReturnValue({
- data: markets,
- loading: false,
- error: undefined,
- });
-
- const { result } = setup({
- searchTerm: '',
- product: Product.Future,
- sort: Sort.TopTraded,
- assets: [],
- });
-
- expect(result.current.markets).toEqual([
- markets[3],
- markets[2],
- markets[0],
- markets[1],
- ]);
- });
-
- it('sorts by gained', () => {
- const markets = [
- createMarketFragment({
- id: 'market-0',
- // @ts-ignore data not on fragment
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- // @ts-ignore actual fragment doesn't contain candles and is joined later
- candles: [
- {
- close: '100',
- },
- {
- close: '200',
- },
- ],
- }),
- createMarketFragment({
- id: 'market-1',
- // @ts-ignore data not on fragment
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- // @ts-ignore actual fragment doesn't contain candles and is joined later
- candles: [
- {
- close: '100',
- },
- {
- close: '1000',
- },
- ],
- }),
- createMarketFragment({
- id: 'market-2',
- // @ts-ignore data not on fragment
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- // @ts-ignore actual fragment doesn't contain candles and is joined later
- candles: [
- {
- close: '100',
- },
- {
- close: '400',
- },
- ],
- }),
- ];
- mockUseMarketList.mockReturnValue({
- data: markets,
- loading: false,
- error: undefined,
- });
- const { result, rerender } = setup({
- searchTerm: '',
- product: Product.Future,
- sort: Sort.Gained,
- assets: [],
- });
- expect(result.current.markets).toEqual([
- markets[1],
- markets[2],
- markets[0],
- ]);
- rerender({
- searchTerm: '',
- product: Product.Future,
- sort: Sort.Lost as 'Gained',
- assets: [],
- });
- expect(result.current.markets).toEqual([
- markets[0],
- markets[2],
- markets[1],
- ]);
- });
-
- it('sorts by open timestamp', () => {
- const markets = [
- createMarketFragment({
- id: 'market-0',
- // @ts-ignore data not on fragment
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- marketTimestamps: {
- open: subDays(new Date(), 3).toISOString(),
- },
- }),
- createMarketFragment({
- id: 'market-1',
- // @ts-ignore data not on fragment
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- marketTimestamps: {
- open: subDays(new Date(), 1).toISOString(),
- },
- }),
- createMarketFragment({
- id: 'market-2',
- // @ts-ignore data not on fragment
- data: createMarketsDataFragment({
- marketState: MarketState.STATE_ACTIVE,
- }),
- marketTimestamps: {
- open: subDays(new Date(), 2).toISOString(),
- },
- }),
- ];
- mockUseMarketList.mockReturnValue({
- data: markets,
- loading: false,
- error: undefined,
- });
- const { result } = setup({
- searchTerm: '',
- product: Product.Future,
- sort: Sort.New,
- assets: [],
- });
- expect(result.current.markets).toEqual([
- markets[1],
- markets[2],
- markets[0],
- ]);
- });
-});
diff --git a/apps/trading/components/market-selector/use-market-selector-list.ts b/apps/trading/components/market-selector/use-market-selector-list.ts
deleted file mode 100644
index 82f28db48c..0000000000
--- a/apps/trading/components/market-selector/use-market-selector-list.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import { useMemo } from 'react';
-import orderBy from 'lodash/orderBy';
-import {
- calcTradedFactor,
- getAsset,
- useMarketList,
-} from '@vegaprotocol/markets';
-import { priceChangePercentage } from '@vegaprotocol/utils';
-import type { Filter } from '../../components/market-selector/market-selector';
-import { Sort } from './sort-dropdown';
-import { Product } from './product-selector';
-import { isMarketActive } from '../../lib/utils';
-
-export const useMarketSelectorList = ({
- product,
- assets,
- sort,
- searchTerm,
-}: Filter) => {
- const { data, loading, error, reload } = useMarketList();
-
- const markets = useMemo(() => {
- if (!data?.length) return [];
- const markets = data
- // show only active markets, using m.data.marketState as this will be
- // data that will get refreshed when calling reload
- .filter((m) => {
- if (!m.data) return false;
- return isMarketActive(m.data.marketState);
- })
- // only selected product type
- .filter((m) => {
- if (
- product === Product.All ||
- m.tradableInstrument.instrument.product.__typename === product
- ) {
- return true;
- }
- return false;
- })
- .filter((m) => {
- if (assets.length === 0) return true;
- const asset = getAsset(m);
- return assets.includes(asset.id);
- })
- // filter based on search term
- .filter((m) => {
- const code = m.tradableInstrument.instrument.code.toLowerCase();
- const name = m.tradableInstrument.instrument.name.toLowerCase();
- if (
- code.includes(searchTerm.toLowerCase()) ||
- name.includes(searchTerm.toLowerCase())
- ) {
- return true;
- }
- return false;
- });
-
- if (sort === Sort.Gained || sort === Sort.Lost) {
- const dir = sort === Sort.Gained ? 'desc' : 'asc';
- return orderBy(
- markets,
- [
- (m) => {
- if (!m.candles?.length) return 0;
- return Number(
- priceChangePercentage(
- m.candles.filter((c) => c.close !== '').map((c) => c.close)
- )
- );
- },
- ],
- [dir]
- );
- }
-
- if (sort === Sort.New) {
- return orderBy(
- markets,
- [(m) => new Date(m.marketTimestamps.open).getTime()],
- ['desc']
- );
- }
-
- if (sort === Sort.TopTraded) {
- return orderBy(markets, [(m) => calcTradedFactor(m)], ['desc']);
- }
-
- return markets;
- }, [data, product, searchTerm, assets, sort]);
-
- return { markets, data, loading, error, reload };
-};
diff --git a/apps/trading/components/sidebar/sidebar.spec.tsx b/apps/trading/components/sidebar/sidebar.spec.tsx
index 02bb2da610..45c9852b3b 100644
--- a/apps/trading/components/sidebar/sidebar.spec.tsx
+++ b/apps/trading/components/sidebar/sidebar.spec.tsx
@@ -27,6 +27,8 @@ jest.mock('../asset-card', () => ({
jest.mock('../accounts-container/sidebar-accounts-container.tsx', () => ({
SidebarAccountsContainer: () => ,
+ SidebarAccountsViewType: '',
+ useSidebarAccountsInnerView: () => () => ({}),
}));
jest.mock('../margin-mode', () => ({
diff --git a/apps/trading/components/sidebar/sidebar.tsx b/apps/trading/components/sidebar/sidebar.tsx
index af4e108312..9b4fc52616 100644
--- a/apps/trading/components/sidebar/sidebar.tsx
+++ b/apps/trading/components/sidebar/sidebar.tsx
@@ -9,13 +9,16 @@ import {
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
import { MarketInfoAccordionContainer } from '@vegaprotocol/markets';
-import { useNavigate, useParams } from 'react-router-dom';
+import { useParams } from 'react-router-dom';
import { ErrorBoundary } from '../error-boundary';
import { NodeHealthContainer } from '../node-health';
import { AssetCard } from '../asset-card';
-import { Links } from '../../lib/links';
import { useT } from '../../lib/use-t';
-import { SidebarAccountsContainer } from '../accounts-container/sidebar-accounts-container';
+import {
+ SidebarAccountsContainer,
+ SidebarAccountsViewType,
+ useSidebarAccountsInnerView,
+} from '../accounts-container/sidebar-accounts-container';
import classNames from 'classnames';
import { MarginModeToggle } from '../margin-mode';
@@ -28,8 +31,8 @@ export enum ViewType {
export const Sidebar = ({ pinnedAssets }: { pinnedAssets?: string[] }) => {
const t = useT();
const params = useParams();
- const navigate = useNavigate();
const { view, setView } = useSidebar();
+ const setInnerView = useSidebarAccountsInnerView((state) => state.setView);
return (
@@ -58,7 +61,10 @@ export const Sidebar = ({ pinnedAssets }: { pinnedAssets?: string[] }) => {
{params.marketId && (
navigate(Links.DEPOSIT())}
+ onDeposit={(assetId) => {
+ setView(ViewType.Assets);
+ setInnerView([SidebarAccountsViewType.Deposit, assetId]);
+ }}
/>
)}
diff --git a/apps/trading/components/welcome-dialog/proposed-markets.tsx b/apps/trading/components/welcome-dialog/proposed-markets.tsx
index 389d85b855..4993076999 100644
--- a/apps/trading/components/welcome-dialog/proposed-markets.tsx
+++ b/apps/trading/components/welcome-dialog/proposed-markets.tsx
@@ -1,4 +1,5 @@
import { useMemo } from 'react';
+import compact from 'lodash/compact';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { proposalsDataProvider } from '@vegaprotocol/proposals';
import take from 'lodash/take';
@@ -35,12 +36,29 @@ export const ProposedMarkets = () => {
].includes(proposal.state)
),
3
- ).map((proposal) => ({
- id: proposal.id,
- displayName:
- proposal.terms.change.__typename === 'NewMarket' &&
- proposal.terms.change.instrument.code,
- }));
+ ).map((proposal) => {
+ if (proposal.__typename === 'Proposal') {
+ return {
+ id: proposal.id,
+ displayName:
+ proposal.terms.change.__typename === 'NewMarket' &&
+ proposal.terms.change.instrument.code,
+ };
+ }
+
+ if (proposal.__typename === 'BatchProposal') {
+ const subProposal = proposal.subProposals?.find(
+ (p) => p?.terms?.change.__typename === 'NewMarket'
+ );
+
+ if (subProposal?.terms?.change.__typename === 'NewMarket') {
+ return {
+ id: proposal.id,
+ displayName: subProposal.terms?.change.instrument.code,
+ };
+ }
+ }
+ });
const tokenLink = useLinks(DApp.Governance);
return useMemo(
@@ -52,7 +70,7 @@ export const ProposedMarkets = () => {
{t('Proposed markets')}
- {newMarkets.map(({ displayName, id }, i) => (
+ {compact(newMarkets).map(({ displayName, id }, i) => (
{displayName}
-
diff --git a/apps/trading/e2e/tests/deal_ticket/test_trading_deal_ticket_submit_account.py b/apps/trading/e2e/tests/deal_ticket/test_trading_deal_ticket_submit_account.py
index b83b671c1b..46af28eeb0 100644
--- a/apps/trading/e2e/tests/deal_ticket/test_trading_deal_ticket_submit_account.py
+++ b/apps/trading/e2e/tests/deal_ticket/test_trading_deal_ticket_submit_account.py
@@ -38,7 +38,7 @@ def test_should_display_info_and_button_for_deposit(continuous_market, page: Pag
"1,661,888.12901 tDAI is currently required.You have only 999,991.49731.Deposit tDAI"
)
page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click()
- expect(page.get_by_test_id("pathname-/portfolio/assets/deposit")
+ expect(page.get_by_test_id("deposit-form")
).to_be_visible()
diff --git a/apps/trading/e2e/tests/successor_market/test_succession_line.py b/apps/trading/e2e/tests/successor_market/test_succession_line.py
index 15d96d61bd..e29dd8ecd2 100644
--- a/apps/trading/e2e/tests/successor_market/test_succession_line.py
+++ b/apps/trading/e2e/tests/successor_market/test_succession_line.py
@@ -9,6 +9,8 @@
market_banner = "market-banner"
+# TODO: fix this flakey tests
+@pytest.mark.skip("flakey")
@pytest.mark.usefixtures("risk_accepted")
def test_succession_line(vega: VegaServiceNull, page: Page):
parent_market_id = setup_continuous_market(vega)
diff --git a/apps/trading/lib/hooks/use-market-filters.ts b/apps/trading/lib/hooks/use-market-filters.ts
new file mode 100644
index 0000000000..76890b41e3
--- /dev/null
+++ b/apps/trading/lib/hooks/use-market-filters.ts
@@ -0,0 +1,211 @@
+import type * as Types from '@vegaprotocol/types';
+import {
+ type MarketMaybeWithCandles,
+ isFuture,
+ isPerpetual,
+ isSpot,
+ OPEN_MARKETS_STATES,
+ CLOSED_MARKETS_STATES,
+ PROPOSED_MARKETS_STATES,
+ retrieveAssets,
+ calcTradedFactor,
+} from '@vegaprotocol/markets';
+import { create } from 'zustand';
+import intersection from 'lodash/intersection';
+import orderBy from 'lodash/orderBy';
+import { priceChangePercentage } from '@vegaprotocol/utils';
+import omit from 'lodash/omit';
+
+export const MarketType = {
+ FUTURE: 'FUTURE',
+ PERPETUAL: 'PERPETUAL',
+ SPOT: 'SPOT',
+} as const;
+
+export type IMarketType = keyof typeof MarketType;
+
+export const MarketState = {
+ OPEN: 'OPEN',
+ PROPOSED: 'PROPOSED',
+ CLOSED: 'CLOSED',
+} as const;
+
+export type IMarketState = keyof typeof MarketState;
+
+export const SortOption = {
+ GAINED: 'GAINED',
+ LOST: 'LOST',
+ NEW: 'NEW',
+ TOP_TRADED: 'TOP_TRADED',
+} as const;
+
+export type ISortOption = keyof typeof SortOption;
+
+export type Filters = {
+ marketTypes: IMarketType[];
+ marketStates: IMarketState[];
+ assets: string[];
+ searchTerm: string | undefined;
+ sortOrder: ISortOption;
+};
+
+type Actions = {
+ setMarketTypes: (marketTypes: IMarketType[]) => void;
+ setMarketStates: (marketSates: IMarketState[]) => void;
+ setAssets: (assets: string[]) => void;
+ setSearchTerm: (searchTerm: string) => void;
+ setSortOrder: (sortOrder: ISortOption) => void;
+ reset: () => void;
+};
+
+export const DEFAULT_FILTERS: Filters = {
+ marketTypes: [],
+ marketStates: ['OPEN'],
+ assets: [],
+ searchTerm: '',
+ sortOrder: SortOption.TOP_TRADED,
+};
+
+export const useMarketFiltersStore = create()((set) => ({
+ ...DEFAULT_FILTERS,
+ setMarketTypes: (marketTypes) => set({ marketTypes }),
+ setMarketStates: (marketStates) => set({ marketStates }),
+ setAssets: (assets) => set({ assets }),
+ setSearchTerm: (searchTerm) => set({ searchTerm }),
+ setSortOrder: (sortOrder) => set({ sortOrder }),
+ reset: () => set(omit(DEFAULT_FILTERS, 'sortOrder')),
+}));
+
+const isOfTypes = (
+ market: MarketMaybeWithCandles,
+ marketTypes: IMarketType[]
+) => {
+ let marketType: IMarketType | undefined = undefined;
+ const product = market?.tradableInstrument?.instrument?.product;
+ if (product) {
+ if (isFuture(product)) marketType = MarketType.FUTURE;
+ if (isPerpetual(product)) marketType = MarketType.PERPETUAL;
+ if (isSpot(product)) marketType = MarketType.SPOT;
+ }
+ return marketType && marketTypes.includes(marketType);
+};
+
+const isOfStates = (
+ market: MarketMaybeWithCandles,
+ marketStates: IMarketState[]
+) => {
+ let states: Types.MarketState[] = [];
+ if (marketStates.includes('OPEN')) {
+ states = [...states, ...OPEN_MARKETS_STATES];
+ }
+ if (marketStates.includes('CLOSED')) {
+ states = [...states, ...CLOSED_MARKETS_STATES];
+ }
+ if (marketStates.includes('PROPOSED')) {
+ states = [...states, ...PROPOSED_MARKETS_STATES];
+ }
+
+ const marketState = market.data?.marketState;
+ return marketState && states.includes(marketState);
+};
+
+const isOfAssets = (market: MarketMaybeWithCandles, assets: string[]) => {
+ const product = market.tradableInstrument?.instrument?.product;
+ if (product) {
+ const marketAssets = retrieveAssets(product).map((a) => a.id);
+ return intersection(assets, marketAssets).length > 0;
+ }
+ return false;
+};
+
+const nameOrCodeMatches = (market: MarketMaybeWithCandles, term: string) => {
+ const name = market.tradableInstrument.instrument.name;
+ const code = market.tradableInstrument.instrument.code;
+ const re = new RegExp(term, 'ig');
+ return re.test(name) || re.test(code);
+};
+
+export const filterMarket = (
+ market: MarketMaybeWithCandles,
+ filters: Partial
+) => {
+ let passes = true;
+ const { marketTypes, marketStates, assets, searchTerm } = filters;
+
+ // filter by market type
+ if (
+ marketTypes &&
+ marketTypes.length > 0 &&
+ !isOfTypes(market, marketTypes)
+ ) {
+ passes = false;
+ }
+
+ // filter by state
+ if (
+ marketStates &&
+ marketStates.length > 0 &&
+ !isOfStates(market, marketStates)
+ ) {
+ passes = false;
+ }
+
+ // filter by asset
+ if (assets && assets.length > 0 && !isOfAssets(market, assets)) {
+ passes = false;
+ }
+
+ // filter by name or code
+ if (
+ searchTerm &&
+ searchTerm.length > 0 &&
+ !nameOrCodeMatches(market, searchTerm)
+ ) {
+ passes = false;
+ }
+
+ return passes;
+};
+
+export const filterMarkets = (
+ markets: MarketMaybeWithCandles[],
+ filters: Partial
+) => markets.filter((m) => filterMarket(m, filters));
+
+export const orderMarkets = (
+ markets: MarketMaybeWithCandles[],
+ sortOrder?: ISortOption
+) => {
+ if (!sortOrder) return markets;
+
+ switch (sortOrder) {
+ case SortOption.GAINED:
+ case SortOption.LOST: {
+ const dir = sortOrder === SortOption.GAINED ? 'desc' : 'asc';
+ return orderBy(
+ markets,
+ [
+ (m) => {
+ if (!m.candles?.length) return 0;
+ return Number(
+ priceChangePercentage(
+ m.candles.filter((c) => c.close !== '').map((c) => c.close)
+ )
+ );
+ },
+ ],
+ [dir]
+ );
+ }
+ case SortOption.NEW: {
+ return orderBy(
+ markets,
+ [(m) => new Date(m.marketTimestamps.open).getTime()],
+ ['desc']
+ );
+ }
+ case SortOption.TOP_TRADED: {
+ return orderBy(markets, [(m) => calcTradedFactor(m)], ['desc']);
+ }
+ }
+};
diff --git a/apps/trading/lib/hooks/use-markets-stats.spec.tsx b/apps/trading/lib/hooks/use-markets-stats.spec.tsx
index d66b4d1a9b..5125c1bb98 100644
--- a/apps/trading/lib/hooks/use-markets-stats.spec.tsx
+++ b/apps/trading/lib/hooks/use-markets-stats.spec.tsx
@@ -2,6 +2,7 @@ import type { Candle, MarketMaybeWithCandles } from '@vegaprotocol/markets';
import { useNewListings } from './use-markets-stats';
import { useTotalVolume24hCandles } from './use-markets-stats';
+import { MarketState } from '@vegaprotocol/types';
describe('Hooks for market stats', () => {
describe('useTotalVolume24hCandles', () => {
@@ -24,6 +25,9 @@ describe('Hooks for market stats', () => {
),
decimalPlaces: 2,
positionDecimalPlaces: 1,
+ data: {
+ marketState: MarketState.STATE_ACTIVE,
+ },
tradableInstrument: {
instrument: {
product: {
@@ -53,9 +57,18 @@ describe('Hooks for market stats', () => {
it('returns latest three markets by open time', () => {
const markets = [
- { marketTimestamps: { open: '2021-01-01T00:00:00Z' } },
- { marketTimestamps: { open: '2022-01-01T00:00:00Z' } },
- { marketTimestamps: { open: '2023-01-01T00:00:00Z' } },
+ {
+ marketTimestamps: { open: '2021-01-01T00:00:00Z' },
+ data: { marketState: MarketState.STATE_ACTIVE },
+ },
+ {
+ marketTimestamps: { open: '2022-01-01T00:00:00Z' },
+ data: { marketState: MarketState.STATE_ACTIVE },
+ },
+ {
+ marketTimestamps: { open: '2023-01-01T00:00:00Z' },
+ data: { marketState: MarketState.STATE_ACTIVE },
+ },
] as MarketMaybeWithCandles[];
expect(useNewListings(markets)).toEqual([
markets[2],
diff --git a/apps/trading/lib/hooks/use-markets-stats.tsx b/apps/trading/lib/hooks/use-markets-stats.tsx
index 5ca114118b..846d585b2d 100644
--- a/apps/trading/lib/hooks/use-markets-stats.tsx
+++ b/apps/trading/lib/hooks/use-markets-stats.tsx
@@ -1,7 +1,12 @@
-import { getAsset, type MarketMaybeWithCandles } from '@vegaprotocol/markets';
+import {
+ filterAndSortMarkets,
+ getAsset,
+ type MarketMaybeWithCandles,
+} from '@vegaprotocol/markets';
import { priceChangePercentage, toBigNum, toQUSD } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
-import { orderBy } from 'lodash';
+import compact from 'lodash/compact';
+import orderBy from 'lodash/orderBy';
/**
* useTotalVolume24hCandles returns 24 hr candles with total volume
@@ -11,9 +16,10 @@ import { orderBy } from 'lodash';
* @returns
*/
export const useTotalVolume24hCandles = (
- activeMarkets: MarketMaybeWithCandles[] | null
+ markets: MarketMaybeWithCandles[] | null
): number[] => {
const candles = [];
+ const activeMarkets = filterAndSortMarkets(compact(markets));
if (!activeMarkets || activeMarkets.length === 0) return [];
for (let i = 0; i < 24; i++) {
const totalVolume24hr = activeMarkets.reduce((acc, market) => {
@@ -34,12 +40,13 @@ export const useTotalVolume24hCandles = (
/**
* useTopGainers returns the top 3 markets with highest gains, i.e. sorted by biggest 24h change
*
- * @param activeMarkets
+ * @param markets
* @returns MarketMaybeWithCandles[]
*/
export const useTopGainers = (
- activeMarkets: MarketMaybeWithCandles[] | null
+ markets: MarketMaybeWithCandles[] | null
): MarketMaybeWithCandles[] => {
+ const activeMarkets = filterAndSortMarkets(compact(markets));
return orderBy(
activeMarkets,
[
@@ -63,8 +70,9 @@ export const useTopGainers = (
* @returns MarketMaybeWithCandles[]
*/
export const useNewListings = (
- activeMarkets: MarketMaybeWithCandles[] | null
+ markets: MarketMaybeWithCandles[] | null
): MarketMaybeWithCandles[] => {
+ const activeMarkets = filterAndSortMarkets(compact(markets));
return orderBy(
activeMarkets,
[(m) => new Date(m.marketTimestamps.open).getTime()],
diff --git a/libs/i18n/src/locales/en/trading.json b/libs/i18n/src/locales/en/trading.json
index e9bc196fcd..69da82b047 100644
--- a/libs/i18n/src/locales/en/trading.json
+++ b/libs/i18n/src/locales/en/trading.json
@@ -6,6 +6,8 @@
"{{amount}} $VEGA staked": "{{amount}} $VEGA staked",
"{{assetSymbol}} Reward pot": "{{assetSymbol}} Reward pot",
"{{checkedAssets}} Assets": "{{checkedAssets}} Assets",
+ "{{count}} results excluded due to the applied filters. <0>Remove filters0>.": "{{count}} results excluded due to the applied filters. <0>Remove filters0>.",
+ "{{count}} results included due to the applied filters. <0>Remove filters0>.": "{{count}} results included due to the applied filters. <0>Remove filters0>.",
"{{distance}} ago": "{{distance}} ago",
"{{entity}} scope": "{{entity}} scope",
"{{instrumentCode}} liquidity provision": "{{instrumentCode}} liquidity provision",
@@ -42,6 +44,7 @@
"Asset": "Asset",
"Asset (1)": "Asset (1)",
"Assets": "Assets",
+ "Assets ({{count}})": "Assets ({{count}})",
"Available to withdraw this epoch": "Available to withdraw this epoch",
"Avatar URL": "Avatar URL",
"Average position": "Average position",
@@ -453,6 +456,7 @@
"Search": "Search",
"See all markets": "See all markets",
"See all the live games on the cards below. <0>Find out how to create one0>.": "See all the live games on the cards below. <0>Find out how to create one0>.",
+ "Search by market": "Search by market",
"See details of {{count}} rewards": "See details of {{count}} rewards",
"Select from wallet": "Select from wallet",
"Select market": "Select market",
@@ -484,6 +488,7 @@
"Start trading on the worlds most advanced decentralised exchange.": "Start trading on the worlds most advanced decentralised exchange.",
"Start trading": "Start trading",
"State": "State",
+ "State ({{count}})": "State ({{count}})",
"Status": "Status",
"Stop orders": "Stop orders",
"Stop": "Stop",
diff --git a/libs/markets/src/lib/market-utils.ts b/libs/markets/src/lib/market-utils.ts
index a9c803672f..e1125bec31 100644
--- a/libs/markets/src/lib/market-utils.ts
+++ b/libs/markets/src/lib/market-utils.ts
@@ -118,7 +118,9 @@ export const totalFeesFactorsPercentage = (fees: Market['fees']['factors']) => {
: undefined;
};
-export const filterAndSortMarkets = (markets: MarketMaybeWithData[]) => {
+type MarketsFilter = (markets: T[]) => T[];
+
+export const filterAndSortMarkets: MarketsFilter = (markets) => {
const tradingModesOrdering = [
MarketTradingMode.TRADING_MODE_CONTINUOUS,
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
@@ -148,39 +150,43 @@ export const filterAndSortMarkets = (markets: MarketMaybeWithData[]) => {
);
};
-export const filterActiveMarkets = (markets: MarketMaybeWithData[]) => {
+export const OPEN_MARKETS_STATES = [
+ MarketState.STATE_ACTIVE,
+ MarketState.STATE_SUSPENDED,
+ MarketState.STATE_SUSPENDED_VIA_GOVERNANCE,
+ MarketState.STATE_PENDING,
+];
+
+export const CLOSED_MARKETS_STATES = [
+ MarketState.STATE_SETTLED,
+ MarketState.STATE_TRADING_TERMINATED,
+ MarketState.STATE_CLOSED,
+ MarketState.STATE_CANCELLED,
+];
+
+export const PROPOSED_MARKETS_STATES = [MarketState.STATE_PROPOSED];
+
+export const filterActiveMarkets: MarketsFilter = (markets) => {
return markets.filter((m) => {
return (
- m.data?.marketState &&
- [
- MarketState.STATE_ACTIVE,
- MarketState.STATE_SUSPENDED,
- MarketState.STATE_SUSPENDED_VIA_GOVERNANCE,
- MarketState.STATE_PENDING,
- ].includes(m.data.marketState)
+ m.data?.marketState && OPEN_MARKETS_STATES.includes(m.data.marketState)
);
});
};
-export const filterClosedMarkets = (markets: MarketMaybeWithData[]) => {
+export const filterClosedMarkets: MarketsFilter = (markets) => {
return markets.filter((m) => {
return (
- m.data?.marketState &&
- [
- MarketState.STATE_SETTLED,
- MarketState.STATE_TRADING_TERMINATED,
- MarketState.STATE_CLOSED,
- MarketState.STATE_CANCELLED,
- ].includes(m.data.marketState)
+ m.data?.marketState && CLOSED_MARKETS_STATES.includes(m.data.marketState)
);
});
};
-export const filterProposedMarkets = (markets: MarketMaybeWithData[]) => {
+export const filterProposedMarkets: MarketsFilter = (markets) => {
return markets.filter((m) => {
return (
m.data?.marketState &&
- [MarketState.STATE_PROPOSED].includes(m.data?.marketState)
+ PROPOSED_MARKETS_STATES.includes(m.data?.marketState)
);
});
};
diff --git a/libs/markets/src/lib/markets-provider.ts b/libs/markets/src/lib/markets-provider.ts
index 5017b5eea9..adc16e929e 100644
--- a/libs/markets/src/lib/markets-provider.ts
+++ b/libs/markets/src/lib/markets-provider.ts
@@ -136,9 +136,10 @@ export const proposedMarketsProvider = makeDerivedDataProvider<
never
>([marketsWithDataProvider], ([markets]) => filterProposedMarkets(markets));
-export type MarketMaybeWithCandles = MarketFieldsWithAccountsFragment & {
- candles?: Candle[];
-};
+export type MarketMaybeWithCandles = MarketFieldsWithAccountsFragment &
+ MarketMaybeWithData & {
+ candles?: Candle[];
+ };
const addCandles = (
markets: T[],
@@ -162,6 +163,18 @@ export const activeMarketsWithCandlesProvider = makeDerivedDataProvider<
(parts) => addCandles(parts[0] as Market[], parts[1] as MarketCandles[])
);
+export const marketsWithCandlesProvider = makeDerivedDataProvider<
+ MarketMaybeWithCandles[],
+ never,
+ MarketsCandlesQueryVariables
+>(
+ [
+ (callback, client) => marketsWithDataProvider(callback, client, undefined),
+ marketsCandlesProvider,
+ ],
+ (parts) => addCandles(parts[0] as Market[], parts[1] as MarketCandles[])
+);
+
export type MarketMaybeWithData = Market & { data?: MarketData };
const addData = (markets: T[], marketsData: MarketData[]) =>
diff --git a/libs/markets/src/lib/product.ts b/libs/markets/src/lib/product.ts
index 89ad61d7eb..de11465e2f 100644
--- a/libs/markets/src/lib/product.ts
+++ b/libs/markets/src/lib/product.ts
@@ -17,6 +17,16 @@ export const isFuture = (product: Product): product is FutureFragment =>
export const isPerpetual = (product: Product): product is PerpetualFragment =>
product.__typename === 'Perpetual';
+export const retrieveAssets = (product: Product) => {
+ if (isSpot(product)) {
+ return [product.baseAsset, product.quoteAsset];
+ }
+ if (isPerpetual(product) || isFuture(product)) {
+ return [product.settlementAsset];
+ }
+ return [];
+};
+
export const getDataSourceSpecForSettlementData = (product: Product) => {
if (isFuture(product)) {
return product.dataSourceSpecForSettlementData;
diff --git a/libs/proposals/src/components/asset-proposal-notification.tsx b/libs/proposals/src/components/asset-proposal-notification.tsx
index 7c5d9dd9ab..e5bff90e6c 100644
--- a/libs/proposals/src/components/asset-proposal-notification.tsx
+++ b/libs/proposals/src/components/asset-proposal-notification.tsx
@@ -1,7 +1,7 @@
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import * as Schema from '@vegaprotocol/types';
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
-import { useUpdateProposal } from '../lib';
+import { useAssetUpdateProposal } from '../lib';
import { useT } from '../use-t';
type AssetProposalNotificationProps = {
@@ -12,7 +12,7 @@ export const AssetProposalNotification = ({
}: AssetProposalNotificationProps) => {
const t = useT();
const tokenLink = useLinks(DApp.Governance);
- const { data: proposal } = useUpdateProposal({
+ const { data: proposal } = useAssetUpdateProposal({
id: assetId,
proposalType: Schema.ProposalType.TYPE_UPDATE_ASSET,
});
diff --git a/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts b/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts
index cecb0f511e..0f0fccd8c4 100644
--- a/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts
+++ b/libs/proposals/src/lib/proposals-data-provider/__generated__/Proposals.ts
@@ -796,4 +796,4 @@ export function useMarketViewLiveProposalsSubscription(baseOptions?: Apollo.Subs
return Apollo.useSubscription(MarketViewLiveProposalsDocument, options);
}
export type MarketViewLiveProposalsSubscriptionHookResult = ReturnType;
-export type MarketViewLiveProposalsSubscriptionResult = Apollo.SubscriptionResult;
+export type MarketViewLiveProposalsSubscriptionResult = Apollo.SubscriptionResult;
\ No newline at end of file
diff --git a/libs/proposals/src/lib/proposals-data-provider/proposals-data-provider.tsx b/libs/proposals/src/lib/proposals-data-provider/proposals-data-provider.tsx
index a6aa611407..424378e301 100644
--- a/libs/proposals/src/lib/proposals-data-provider/proposals-data-provider.tsx
+++ b/libs/proposals/src/lib/proposals-data-provider/proposals-data-provider.tsx
@@ -15,6 +15,7 @@ import {
type MarketViewProposalsQueryVariables,
type MarketViewLiveProposalsSubscriptionVariables,
type SubProposalFragment,
+ type BatchproposalListFieldsFragment,
} from './__generated__/Proposals';
export type ProposalFragment =
@@ -25,11 +26,16 @@ export type ProposalFragments = Array;
const getData = (responseData: ProposalsListQuery | null) =>
responseData?.proposalsConnection?.edges
?.filter((edge) => Boolean(edge?.proposalNode))
- .map((edge) => edge?.proposalNode as ProposalListFieldsFragment) || null;
+ .map(
+ (edge) =>
+ edge?.proposalNode as
+ | ProposalListFieldsFragment
+ | BatchproposalListFieldsFragment
+ ) || null;
export const proposalsDataProvider = makeDataProvider<
ProposalsListQuery,
- ProposalListFieldsFragment[],
+ Array,
never,
never,
ProposalsListQueryVariables
diff --git a/libs/proposals/src/lib/proposals-hooks/use-update-proposal.spec.ts b/libs/proposals/src/lib/proposals-hooks/use-update-proposal.spec.ts
index 3bd75346df..d4846596fb 100644
--- a/libs/proposals/src/lib/proposals-hooks/use-update-proposal.spec.ts
+++ b/libs/proposals/src/lib/proposals-hooks/use-update-proposal.spec.ts
@@ -2,21 +2,13 @@
import { renderHook } from '@testing-library/react';
import * as Schema from '@vegaprotocol/types';
-import type {
- ProposalListFieldsFragment,
- UpdateAssetFieldsFragment,
- UpdateMarketFieldsFragment,
-} from '../proposals-data-provider';
-import {
- isChangeProposed,
- UpdateAssetFields,
- UpdateMarketFields,
- useUpdateProposal,
-} from './use-update-proposal';
+import type { ProposalListFieldsFragment } from '../proposals-data-provider';
+import { useAssetUpdateProposal } from './use-update-proposal';
-type Proposal = Pick &
- Pick &
- Pick;
+type Proposal = Pick<
+ ProposalListFieldsFragment,
+ '__typename' | 'id' | 'terms' | 'state'
+>;
const generateUpdateAssetProposal = (
id: string,
@@ -24,6 +16,7 @@ const generateUpdateAssetProposal = (
lifetimeLimit = '',
withdrawThreshold = ''
): Proposal => ({
+ __typename: 'Proposal',
id,
state: Schema.ProposalState.STATE_OPEN,
terms: {
@@ -43,140 +36,19 @@ const generateUpdateAssetProposal = (
},
});
-type RiskParameters =
- | {
- __typename: 'UpdateMarketLogNormalRiskModel';
- logNormal?: {
- __typename?: 'LogNormalRiskModel';
- riskAversionParameter: number;
- tau: number;
- params: {
- __typename?: 'LogNormalModelParams';
- mu: number;
- r: number;
- sigma: number;
- };
- } | null;
- }
- | {
- __typename: 'UpdateMarketSimpleRiskModel';
- simple?: {
- __typename?: 'SimpleRiskModelParams';
- factorLong: number;
- factorShort: number;
- } | null;
- };
-
-const generateRiskParameters = (
- type:
- | 'UpdateMarketLogNormalRiskModel'
- | 'UpdateMarketSimpleRiskModel' = 'UpdateMarketLogNormalRiskModel'
-): RiskParameters => {
- if (type === 'UpdateMarketSimpleRiskModel')
- return {
- __typename: 'UpdateMarketSimpleRiskModel',
- simple: {
- __typename: 'SimpleRiskModelParams',
- factorLong: 0,
- factorShort: 0,
- },
- };
-
- return {
- __typename: 'UpdateMarketLogNormalRiskModel',
- logNormal: {
- __typename: 'LogNormalRiskModel',
- params: {
- __typename: 'LogNormalModelParams',
- mu: 0,
- r: 0,
- sigma: 0,
- },
- riskAversionParameter: 0,
- tau: 0,
- },
- };
-};
-
-const generateUpdateMarketProposal = (
- id: string,
- code = '',
- quoteName = '',
- priceMonitoring = false,
- liquidityMonitoring = false,
- riskParameters = false,
- riskParametersType:
- | 'UpdateMarketLogNormalRiskModel'
- | 'UpdateMarketSimpleRiskModel' = 'UpdateMarketLogNormalRiskModel'
-): Proposal => ({
- state: Schema.ProposalState.STATE_OPEN,
- terms: {
- __typename: 'ProposalTerms',
- closingDatetime: '',
- enactmentDatetime: undefined,
- change: {
- __typename: 'UpdateMarket',
- marketId: id,
- updateMarketConfiguration: {
- __typename: undefined,
- instrument: {
- __typename:
- code.length > 0 || quoteName.length > 0
- ? 'UpdateInstrumentConfiguration'
- : undefined,
- code,
- product: {
- dataSourceSpecBinding: {
- settlementDataProperty: '',
- tradingTerminationProperty: '',
- },
- dataSourceSpecForSettlementData: {
- sourceType: {
- __typename: 'DataSourceDefinitionInternal',
- },
- },
- dataSourceSpecForTradingTermination: {
- sourceType: {
- __typename: 'DataSourceDefinitionInternal',
- },
- },
- __typename:
- quoteName.length > 0 ? 'UpdateFutureProduct' : undefined,
- quoteName,
- },
- },
- priceMonitoringParameters: {
- __typename: priceMonitoring ? 'PriceMonitoringParameters' : undefined,
- triggers: priceMonitoring
- ? [
- {
- auctionExtensionSecs: 1,
- horizonSecs: 2,
- probability: 3,
- __typename: 'PriceMonitoringTrigger',
- },
- ]
- : [],
- },
- liquidityMonitoringParameters: {
- __typename: liquidityMonitoring
- ? 'LiquidityMonitoringParameters'
- : undefined,
- targetStakeParameters: {
- __typename: undefined,
- scalingFactor: 0,
- timeWindow: 0,
- },
- },
- riskParameters: riskParameters
- ? generateRiskParameters(riskParametersType)
- : {
- __typename: riskParametersType,
- },
+const generateUpdateMarketProposal = (id: string) =>
+ ({
+ state: Schema.ProposalState.STATE_OPEN,
+ terms: {
+ __typename: 'ProposalTerms',
+ closingDatetime: '',
+ enactmentDatetime: undefined,
+ change: {
+ __typename: 'UpdateMarket',
+ marketId: id,
},
},
- },
-});
+ } as Proposal);
const mockDataProviderData: {
data: Proposal[];
@@ -199,36 +71,25 @@ jest.mock('@vegaprotocol/data-provider', () => ({
useDataProvider: jest.fn((args) => mockDataProvider()),
}));
-describe('useUpdateProposal', () => {
+describe('useAssetUpdateProposal', () => {
it('returns update proposal for a given asset', () => {
const { result } = renderHook(() =>
- useUpdateProposal({
+ useAssetUpdateProposal({
id: '456',
proposalType: Schema.ProposalType.TYPE_UPDATE_ASSET,
})
);
- const change = result.current.data?.terms
- .change as UpdateAssetFieldsFragment;
- expect(change.__typename).toEqual('UpdateAsset');
- expect(change.assetId).toEqual('456');
- });
- it('returns update proposal for a given market', () => {
- const { result } = renderHook(() =>
- useUpdateProposal({
- id: '123',
- proposalType: Schema.ProposalType.TYPE_UPDATE_MARKET,
- })
- );
- const change = result.current.data?.terms
- .change as UpdateMarketFieldsFragment;
- expect(change.__typename).toEqual('UpdateMarket');
- expect(change.marketId).toEqual('123');
+ expect(result.current.data).toMatchObject({
+ __typename: 'Proposal',
+ });
+ // @ts-expect-error terms present as mock only includes a normal proposal
+ expect(result.current.data?.terms?.change?.assetId).toEqual('456');
});
it('does not return a proposal if not found', () => {
const { result } = renderHook(() =>
- useUpdateProposal({
+ useAssetUpdateProposal({
id: '789',
proposalType: Schema.ProposalType.TYPE_UPDATE_MARKET,
})
@@ -236,68 +97,3 @@ describe('useUpdateProposal', () => {
expect(result.current.data).toBeFalsy();
});
});
-
-describe('isChangeProposed', () => {
- it('returns false if a change for the specified asset field is not proposed', () => {
- const proposal = generateUpdateAssetProposal('123');
- expect(isChangeProposed(proposal, UpdateAssetFields.Quantum)).toBeFalsy();
- expect(
- isChangeProposed(proposal, UpdateAssetFields.LifetimeLimit)
- ).toBeFalsy();
- expect(
- isChangeProposed(proposal, UpdateAssetFields.WithdrawThreshold)
- ).toBeFalsy();
- });
-
- it('returns true if a change for the specified asset field is proposed', () => {
- const proposal = generateUpdateAssetProposal('123', '100', '100', '100');
- expect(isChangeProposed(proposal, UpdateAssetFields.Quantum)).toBeTruthy();
- expect(
- isChangeProposed(proposal, UpdateAssetFields.LifetimeLimit)
- ).toBeTruthy();
- expect(
- isChangeProposed(proposal, UpdateAssetFields.WithdrawThreshold)
- ).toBeTruthy();
- });
-
- it('returns false if a change for the specified market field is not proposed', () => {
- const proposal = generateUpdateMarketProposal('123');
- expect(isChangeProposed(proposal, UpdateMarketFields.Code)).toBeFalsy();
- expect(
- isChangeProposed(proposal, UpdateMarketFields.QuoteName)
- ).toBeFalsy();
- expect(
- isChangeProposed(proposal, UpdateMarketFields.PriceMonitoring)
- ).toBeFalsy();
- expect(
- isChangeProposed(proposal, UpdateMarketFields.LiquidityMonitoring)
- ).toBeFalsy();
- expect(
- isChangeProposed(proposal, UpdateMarketFields.RiskParameters)
- ).toBeFalsy();
- });
-
- it('returns true if a change for the specified market field is proposed', () => {
- const proposal = generateUpdateMarketProposal(
- '123',
- 'ABCDEF',
- 'qABCDEFq',
- true,
- true,
- true
- );
- expect(isChangeProposed(proposal, UpdateMarketFields.Code)).toBeFalsy();
- expect(
- isChangeProposed(proposal, UpdateMarketFields.QuoteName)
- ).toBeFalsy();
- expect(
- isChangeProposed(proposal, UpdateMarketFields.PriceMonitoring)
- ).toBeFalsy();
- expect(
- isChangeProposed(proposal, UpdateMarketFields.LiquidityMonitoring)
- ).toBeFalsy();
- expect(
- isChangeProposed(proposal, UpdateMarketFields.RiskParameters)
- ).toBeFalsy();
- });
-});
diff --git a/libs/proposals/src/lib/proposals-hooks/use-update-proposal.ts b/libs/proposals/src/lib/proposals-hooks/use-update-proposal.ts
index a59f8438df..6509949cfa 100644
--- a/libs/proposals/src/lib/proposals-hooks/use-update-proposal.ts
+++ b/libs/proposals/src/lib/proposals-hooks/use-update-proposal.ts
@@ -3,7 +3,10 @@ import { useDataProvider } from '@vegaprotocol/data-provider';
import { useMemo } from 'react';
import first from 'lodash/first';
import { proposalsDataProvider } from '../proposals-data-provider';
-import type { ProposalListFieldsFragment } from '../proposals-data-provider';
+import type {
+ BatchproposalListFieldsFragment,
+ ProposalListFieldsFragment,
+} from '../proposals-data-provider';
type UseUpdateProposalProps = {
id?: string;
@@ -13,23 +16,15 @@ type UseUpdateProposalProps = {
};
type UseUpdateProposal = {
- data: ProposalListFieldsFragment | undefined;
+ data:
+ | ProposalListFieldsFragment
+ | BatchproposalListFieldsFragment
+ | undefined;
loading: boolean;
error: Error | undefined;
};
-const changeCondition = {
- [Schema.ProposalType.TYPE_UPDATE_ASSET]: (
- id: string,
- change: ProposalListFieldsFragment['terms']['change']
- ) => change.__typename === 'UpdateAsset' && change.assetId === id,
- [Schema.ProposalType.TYPE_UPDATE_MARKET]: (
- id: string,
- change: ProposalListFieldsFragment['terms']['change']
- ) => change.__typename === 'UpdateMarket' && change.marketId === id,
-};
-
-export const useUpdateProposal = ({
+export const useAssetUpdateProposal = ({
id,
proposalType,
}: UseUpdateProposalProps): UseUpdateProposal => {
@@ -46,159 +41,44 @@ export const useUpdateProposal = ({
variables,
});
- const proposal = id
- ? first(
- (data || []).filter(
- (proposal) =>
- [
- Schema.ProposalState.STATE_OPEN,
- Schema.ProposalState.STATE_PASSED,
- Schema.ProposalState.STATE_WAITING_FOR_NODE_VOTE,
- ].includes(proposal.state) &&
- changeCondition[proposalType](id, proposal.terms.change)
- )
- )
- : undefined;
-
- return { data: proposal, loading, error };
-};
-
-export enum UpdateMarketFields {
- Code,
- QuoteName,
- PriceMonitoring,
- LiquidityMonitoring,
- RiskParameters,
-}
-
-export enum UpdateAssetFields {
- Quantum,
- LifetimeLimit,
- WithdrawThreshold,
-}
-
-export type UpdateProposalField = UpdateAssetFields | UpdateMarketFields;
+ const openAssetProposals = (data || []).filter((proposal) => {
+ if (
+ ![
+ Schema.ProposalState.STATE_OPEN,
+ Schema.ProposalState.STATE_PASSED,
+ Schema.ProposalState.STATE_WAITING_FOR_NODE_VOTE,
+ ].includes(proposal.state)
+ ) {
+ return false;
+ }
-const fieldGetters = {
- [UpdateMarketFields.Code]: (
- change: ProposalListFieldsFragment['terms']['change']
- ) => {
- if (change.__typename === 'UpdateMarket') {
- const proposed =
- change.updateMarketConfiguration.__typename !== undefined &&
- change.updateMarketConfiguration.instrument.__typename !== undefined;
- return (
- proposed && change.updateMarketConfiguration.instrument.code.length > 0
- );
+ if (proposal.__typename === 'Proposal') {
+ if (
+ proposal.terms.change.__typename === 'UpdateAsset' &&
+ proposal.terms.change.assetId === id
+ ) {
+ return true;
+ }
}
- return false;
- },
- [UpdateMarketFields.QuoteName]: (
- change: ProposalListFieldsFragment['terms']['change']
- ) => {
- if (change.__typename === 'UpdateMarket') {
- const proposed =
- change.updateMarketConfiguration.__typename !== undefined &&
- change.updateMarketConfiguration.instrument.__typename !== undefined &&
- change.updateMarketConfiguration.instrument.product.__typename !==
- undefined;
- return (
- proposed &&
- 'quoteName' in change.updateMarketConfiguration.instrument.product &&
- change.updateMarketConfiguration.instrument.product.quoteName.length > 0
+
+ if (proposal.__typename === 'BatchProposal') {
+ const assetChange = proposal?.subProposals?.find(
+ (p) => p?.terms?.change.__typename === 'UpdateAsset'
);
+
+ if (
+ assetChange &&
+ assetChange.terms?.change.__typename === 'UpdateAsset' &&
+ assetChange.terms.change.assetId === id
+ ) {
+ return true;
+ }
}
+
return false;
- },
- [UpdateMarketFields.PriceMonitoring]: (
- change: ProposalListFieldsFragment['terms']['change']
- ) => {
- if (change.__typename === 'UpdateMarket') {
- const proposed =
- change.updateMarketConfiguration.__typename !== undefined &&
- change.updateMarketConfiguration.priceMonitoringParameters
- .__typename !== undefined &&
- change.updateMarketConfiguration.priceMonitoringParameters.triggers
- ?.length;
- return proposed;
- }
- return false;
- },
- [UpdateMarketFields.LiquidityMonitoring]: (
- change: ProposalListFieldsFragment['terms']['change']
- ) => {
- if (change.__typename === 'UpdateMarket') {
- const proposed =
- change.updateMarketConfiguration.__typename !== undefined &&
- change.updateMarketConfiguration.liquidityMonitoringParameters
- .__typename !== undefined;
- return proposed;
- }
- return false;
- },
- [UpdateMarketFields.RiskParameters]: (
- change: ProposalListFieldsFragment['terms']['change']
- ) => {
- if (change.__typename === 'UpdateMarket') {
- const proposed =
- change.updateMarketConfiguration.__typename !== undefined &&
- change.updateMarketConfiguration.riskParameters.__typename !==
- undefined;
- const log =
- change.updateMarketConfiguration.riskParameters.__typename ===
- 'UpdateMarketLogNormalRiskModel' &&
- change.updateMarketConfiguration.riskParameters.logNormal !== undefined;
- const simple =
- change.updateMarketConfiguration.riskParameters.__typename ===
- 'UpdateMarketSimpleRiskModel' &&
- change.updateMarketConfiguration.riskParameters.simple !== undefined;
- return proposed && (log || simple);
- }
- return false;
- },
- [UpdateAssetFields.Quantum]: (
- change: ProposalListFieldsFragment['terms']['change']
- ) => {
- if (change.__typename === 'UpdateAsset') {
- const proposed = change.quantum.length > 0;
- return proposed;
- }
- return false;
- },
- [UpdateAssetFields.LifetimeLimit]: (
- change: ProposalListFieldsFragment['terms']['change']
- ) => {
- if (change.__typename === 'UpdateAsset') {
- const proposed =
- change.source.__typename === 'UpdateERC20' &&
- change.source.lifetimeLimit.length > 0;
- return proposed;
- }
- return false;
- },
- [UpdateAssetFields.WithdrawThreshold]: (
- change: ProposalListFieldsFragment['terms']['change']
- ) => {
- if (change.__typename === 'UpdateAsset') {
- const proposed =
- change.source.__typename === 'UpdateERC20' &&
- change.source.withdrawThreshold.length > 0;
- return proposed;
- }
- return false;
- },
-};
+ });
+
+ const proposal = first(openAssetProposals);
-export const isChangeProposed = (
- proposal: Pick,
- field: UpdateProposalField
-) => {
- if (proposal) {
- return (
- (proposal.terms.change.__typename === 'UpdateAsset' ||
- proposal.terms.change.__typename === 'UpdateMarket') &&
- fieldGetters[field](proposal.terms.change)
- );
- }
- return false;
+ return { data: proposal, loading, error };
};
diff --git a/libs/ui-toolkit/src/components/index.ts b/libs/ui-toolkit/src/components/index.ts
index 504bbdc228..868013a8e6 100644
--- a/libs/ui-toolkit/src/components/index.ts
+++ b/libs/ui-toolkit/src/components/index.ts
@@ -66,5 +66,6 @@ export * from './trading-dropdown';
export * from './trading-form-group';
export * from './trading-input-error';
export * from './trading-input';
+export * from './trading-multi-select';
export * from './trading-radio-group';
export * from './trading-select';
diff --git a/libs/ui-toolkit/src/components/trading-multi-select/index.ts b/libs/ui-toolkit/src/components/trading-multi-select/index.ts
new file mode 100644
index 0000000000..dbd80422ea
--- /dev/null
+++ b/libs/ui-toolkit/src/components/trading-multi-select/index.ts
@@ -0,0 +1 @@
+export * from './multi-select';
diff --git a/libs/ui-toolkit/src/components/trading-multi-select/multi-select.stories.tsx b/libs/ui-toolkit/src/components/trading-multi-select/multi-select.stories.tsx
new file mode 100644
index 0000000000..6627a1e5ae
--- /dev/null
+++ b/libs/ui-toolkit/src/components/trading-multi-select/multi-select.stories.tsx
@@ -0,0 +1,27 @@
+import type { StoryFn, Meta } from '@storybook/react';
+import { FormGroup } from '../form-group';
+import { MultiSelect, MultiSelectOption } from './multi-select';
+
+export default {
+ component: MultiSelect,
+ title: 'Multi select component',
+} as Meta;
+
+const Template: StoryFn = (props) => (
+
+
+
+);
+
+export const Default = Template.bind({});
+Default.args = {
+ placeholder: 'Select an option',
+ children: (
+ <>
+ Option A
+ Option B
+ Option C
+ Option D
+ >
+ ),
+};
diff --git a/libs/ui-toolkit/src/components/trading-multi-select/multi-select.tsx b/libs/ui-toolkit/src/components/trading-multi-select/multi-select.tsx
new file mode 100644
index 0000000000..80b7532f5f
--- /dev/null
+++ b/libs/ui-toolkit/src/components/trading-multi-select/multi-select.tsx
@@ -0,0 +1,97 @@
+import * as DropdownPrimitive from '@radix-ui/react-dropdown-menu';
+import classNames from 'classnames';
+import { forwardRef, type ReactNode } from 'react';
+import { VegaIcon, VegaIconNames } from '../icon';
+
+type MultiSelectProps = React.ComponentProps & {
+ trigger?: ReactNode;
+ placeholder?: string;
+};
+
+export const MultiSelect = ({
+ children,
+ trigger,
+ placeholder,
+ ...props
+}: MultiSelectProps) => (
+
+
+
+
+
+
+ {children}
+
+
+
+);
+
+export const MultiSelectOption = forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ children, checked, ...props }, forwardedRef) => (
+ {
+ e.preventDefault();
+ }}
+ ref={forwardedRef}
+ {...props}
+ >
+
+
{children}
+
+));
+
+export const PseudoCheckbox = ({
+ checked,
+}: {
+ checked?: boolean | 'indeterminate';
+}) => (
+
+ {checked && (
+
+ )}
+
+);
diff --git a/package.json b/package.json
index 21c4b27e33..920c20e956 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "nx-monorepo",
- "version": "0.26.18",
+ "version": "0.26.19",
"license": "MIT",
"scripts": {
"start": "nx serve",