diff --git a/.vscode/extensions.json b/.vscode/extensions.json index f8f8a9a2a..abcd264c4 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,6 +4,7 @@ "esbenp.prettier-vscode", "firsttris.vscode-jest-runner", "dbaeumer.vscode-eslint", - "stevejpurves.cucumber" + "stevejpurves.cucumber", + "streetsidesoftware.code-spell-checker" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..a82c1df40 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "cSpell.language": "en-GB", + "cSpell.words": ["vegaprotocol"], + "workbench.colorCustomizations": { + "activityBar.background": "#000000", + "activityBar.foreground": "#ffffff", + "activityBar.inactiveForeground": "#474B0A", // vega-yellow-dark + "activityBar.activeBorder": "#DFFF0B", // vega-yellow + "titleBar.activeBackground": "#DFFF0B", // vega-yellow + "titleBar.activeForeground": "#000000", + "titleBar.inactiveBackground": "#474B0A", // vega-yellow-dark + "titleBar.inactiveForeground": "#000000", + "activityBarBadge.background": "#DFFF0B", + "activityBarBadge.foreground": "#000000" // vega-yellow + } +} diff --git a/apps/trading/pages/_app.page.tsx b/apps/trading/pages/_app.page.tsx index 4bf2d3c89..d2a09da05 100644 --- a/apps/trading/pages/_app.page.tsx +++ b/apps/trading/pages/_app.page.tsx @@ -14,9 +14,18 @@ import { AppLoader } from '../components/app-loader'; import { VegaWalletConnectButton } from '../components/vega-wallet-connect-button'; import './styles.css'; import { useGlobalStore } from '../stores'; +import { + AssetDetailsDialog, + useAssetDetailsDialogStore, +} from '@vegaprotocol/market-list'; function AppBody({ Component, pageProps }: AppProps) { const store = useGlobalStore(); + const { + isAssetDetailsDialogOpen, + assetDetailsDialogSymbol, + setAssetDetailsDialogOpen, + } = useAssetDetailsDialogStore(); const [theme, toggleTheme] = useThemeSwitcher(); return ( @@ -54,6 +63,11 @@ function AppBody({ Component, pageProps }: AppProps) { dialogOpen={store.vegaWalletManageDialog} setDialogOpen={(open) => store.setVegaWalletManageDialog(open)} /> + setAssetDetailsDialogOpen(open)} + /> diff --git a/apps/trading/pages/markets/trade-grid.tsx b/apps/trading/pages/markets/trade-grid.tsx index dab4bea53..0c0335b61 100644 --- a/apps/trading/pages/markets/trade-grid.tsx +++ b/apps/trading/pages/markets/trade-grid.tsx @@ -26,10 +26,12 @@ import { useGlobalStore } from '../../stores'; import { AccountsContainer } from '@vegaprotocol/accounts'; import { DepthChartContainer } from '@vegaprotocol/market-depth'; import { CandlesChartContainer } from '@vegaprotocol/candles-chart'; +import { useAssetDetailsDialogStore } from '@vegaprotocol/market-list'; import { Tab, Tabs, PriceCellChange, + Button, Tooltip, ResizablePanel, } from '@vegaprotocol/ui-toolkit'; @@ -58,9 +60,13 @@ export const TradeMarketHeader = ({ market, className, }: TradeMarketHeaderProps) => { + const { setAssetDetailsDialogOpen, setAssetDetailsDialogSymbol } = + useAssetDetailsDialogStore(); const candlesClose: string[] = (market?.candles || []) .map((candle) => candle?.close) .filter((c): c is CandleClose => c !== null); + const symbol = + market.tradableInstrument.instrument.product?.settlementAsset?.symbol; const headerItemClassName = 'whitespace-nowrap flex flex-col '; const itemClassName = 'font-sans font-normal mb-0 text-black-60 dark:text-white-80 text-ui-small'; @@ -132,15 +138,20 @@ export const TradeMarketHeader = ({ : '-'} - {market.tradableInstrument.instrument.product?.settlementAsset - ?.symbol && ( + {symbol && (
{t('Settlement asset')} - { - market.tradableInstrument.instrument.product?.settlementAsset - ?.symbol - } +
)} diff --git a/apps/trading/stores/global.ts b/apps/trading/stores/global.ts index 39ec6a0ad..c4286c620 100644 --- a/apps/trading/stores/global.ts +++ b/apps/trading/stores/global.ts @@ -1,4 +1,3 @@ -import type { SetState } from 'zustand'; import create from 'zustand'; interface GlobalStore { @@ -14,7 +13,7 @@ interface GlobalStore { setMarketId: (marketId: string) => void; } -export const useGlobalStore = create((set: SetState) => ({ +export const useGlobalStore = create((set) => ({ vegaWalletConnectDialog: false, setVegaWalletConnectDialog: (isOpen: boolean) => { set({ vegaWalletConnectDialog: isOpen }); diff --git a/libs/accounts/src/lib/accounts-table.tsx b/libs/accounts/src/lib/accounts-table.tsx index fd44f5746..f42f55f33 100644 --- a/libs/accounts/src/lib/accounts-table.tsx +++ b/libs/accounts/src/lib/accounts-table.tsx @@ -1,5 +1,9 @@ import { forwardRef } from 'react'; -import type { ColumnApi, ValueFormatterParams } from 'ag-grid-community'; +import type { + ColumnApi, + GroupCellRendererParams, + ValueFormatterParams, +} from 'ag-grid-community'; import { PriceCell, addDecimalsFormatNumber, @@ -12,6 +16,7 @@ import { AgGridColumn } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react'; import type { Accounts_party_accounts } from './__generated__/Accounts'; import { getId } from './accounts-data-provider'; +import { useAssetDetailsDialogStore } from '@vegaprotocol/market-list'; interface AccountsTableProps { data: Accounts_party_accounts[] | null; @@ -85,6 +90,8 @@ const comparator = ( export const AccountsTable = forwardRef( ({ data }, ref) => { + const { setAssetDetailsDialogOpen, setAssetDetailsDialogSymbol } = + useAssetDetailsDialogStore(); return ( ( sortable sortingOrder={['asc', 'desc']} comparator={comparator} + cellRenderer={({ value }: GroupCellRendererParams) => + value && value.length > 0 ? ( + + ) : ( + '' + ) + } /> { }); }); }); + it('shows "View asset details" button when an asset is selected', async () => { + render(); + expect(await screen.getByTestId('view-asset-details')).toBeInTheDocument(); + }); + + it('does not shows "View asset details" button when no asset is selected', async () => { + render(); + expect(await screen.queryAllByTestId('view-asset-details')).toHaveLength(0); + }); }); diff --git a/libs/deposits/src/lib/deposit-form.tsx b/libs/deposits/src/lib/deposit-form.tsx index acff042da..47738f0be 100644 --- a/libs/deposits/src/lib/deposit-form.tsx +++ b/libs/deposits/src/lib/deposit-form.tsx @@ -25,6 +25,7 @@ import type { ReactNode } from 'react'; import { useMemo } from 'react'; import { Controller, useForm, useWatch } from 'react-hook-form'; import { DepositLimits } from './deposit-limits'; +import { useAssetDetailsDialogStore } from '@vegaprotocol/market-list'; interface FormFields { asset: string; @@ -64,6 +65,8 @@ export const DepositForm = ({ allowance, isFaucetable, }: DepositFormProps) => { + const { setAssetDetailsDialogOpen, setAssetDetailsDialogSymbol } = + useAssetDetailsDialogStore(); const { account } = useWeb3React(); const { keypair } = useVegaWallet(); const { @@ -180,6 +183,18 @@ export const DepositForm = ({ {t(`Get ${selectedAsset.symbol}`)} )} + {!errors.asset?.message && selectedAsset && ( + + )} ( + + false} + > + +); + +describe('AssetDetailsDialog', () => { + it('should show no data message given unknown asset symbol', () => { + render(); + expect(screen.getByText('No data')).toBeInTheDocument(); + }); + + const cases = [ + ['tDAI', 'tDAI', 'tDAI TEST', '21,000,000'], + ['VEGA', 'VEGA', 'Vega (testnet)', '64,999,723,000,000,000,000,000,000'], + ['tUSDC', 'tUSDC', 'tUSDC TEST', '21,000,000'], + ]; + it.each(cases)( + 'should show correct data given %p symbol', + async (requestedSymbol, symbol, name, totalSupply) => { + render(); + expect((await screen.findByTestId('symbol_value')).textContent).toContain( + symbol + ); + expect((await screen.findByTestId('name_value')).textContent).toContain( + name + ); + expect( + (await screen.findByTestId('totalsupply_value')).textContent + ).toContain(totalSupply); + } + ); +}); diff --git a/libs/market-list/src/lib/components/asset-details-dialog/asset-details-dialog.tsx b/libs/market-list/src/lib/components/asset-details-dialog/asset-details-dialog.tsx new file mode 100644 index 000000000..23042d31b --- /dev/null +++ b/libs/market-list/src/lib/components/asset-details-dialog/asset-details-dialog.tsx @@ -0,0 +1,205 @@ +import { gql, useQuery } from '@apollo/client'; +import { formatNumber, t, toBigNum } from '@vegaprotocol/react-helpers'; +import type { Asset } from '@vegaprotocol/react-helpers'; +import { + Button, + Dialog, + Icon, + Intent, + Splash, + Tooltip, +} from '@vegaprotocol/ui-toolkit'; +import type { + AssetsConnection, + AssetsConnection_assetsConnection_edges_node_source_ERC20, +} from './__generated__/AssetsConnection'; +import create from 'zustand'; + +export type AssetDetailsDialogStore = { + isAssetDetailsDialogOpen: boolean; + assetDetailsDialogSymbol: string | Asset; + setAssetDetailsDialogOpen: (isOpen: boolean) => void; + setAssetDetailsDialogSymbol: (symbol: string | Asset) => void; +}; + +export const useAssetDetailsDialogStore = create( + (set) => ({ + isAssetDetailsDialogOpen: false, + assetDetailsDialogSymbol: '', + setAssetDetailsDialogOpen: (isOpen: boolean) => { + set({ isAssetDetailsDialogOpen: isOpen }); + }, + setAssetDetailsDialogSymbol: (symbol: string | Asset) => { + set({ assetDetailsDialogSymbol: symbol }); + }, + }) +); + +type AssetDetails = { + key: string; + label: string; + value: string; + tooltip: string; +}[]; + +export const ASSETS_CONNECTION_QUERY = gql` + query AssetsConnection { + assetsConnection { + edges { + node { + id + name + symbol + totalSupply + decimals + quantum + source { + ... on ERC20 { + contractAddress + lifetimeLimit + withdrawThreshold + } + } + } + } + } + } +`; + +export interface AssetDetailsDialogProps { + assetSymbol: string | Asset; + open: boolean; + onChange: (open: boolean) => void; +} + +export const AssetDetailsDialog = ({ + assetSymbol, + open, + onChange, +}: AssetDetailsDialogProps) => { + const { data } = useQuery(ASSETS_CONNECTION_QUERY); + const symbol = + typeof assetSymbol === 'string' ? assetSymbol : assetSymbol.symbol; + const asset = data?.assetsConnection.edges?.find( + (e) => e?.node.symbol === symbol + ); + + let details: AssetDetails = []; + if (asset != null) { + details = [ + { + key: 'name', + label: t('Name'), + value: asset.node.name, + tooltip: '', // t('Name of the asset (e.g: Great British Pound)') + }, + { + key: 'symbol', + label: t('Symbol'), + value: asset.node.symbol, + tooltip: '', // t('Symbol of the asset (e.g: GBP)') + }, + { + key: 'decimals', + label: t('Decimals'), + value: asset.node.decimals.toString(), + tooltip: t('Number of decimal / precision handled by this asset'), + }, + { + key: 'quantum', + label: t('Quantum'), + value: asset.node.quantum, + tooltip: t('The minimum economically meaningful amount in the asset'), + }, + { + key: 'totalsupply', + label: t('Total supply'), + value: formatNumber(toBigNum(asset.node.totalSupply, 0)), + tooltip: t('Total circulating supply for the asset'), + }, + { + key: 'contractaddress', + label: t('Contract address'), + value: ( + asset.node + .source as AssetsConnection_assetsConnection_edges_node_source_ERC20 + ).contractAddress, + tooltip: t( + 'The address of the contract for the token, on the ethereum network' + ), + }, + { + key: 'withdrawalthreshold', + label: t('Withdrawal threshold'), + value: ( + asset.node + .source as AssetsConnection_assetsConnection_edges_node_source_ERC20 + ).withdrawThreshold, + tooltip: t( + 'The maximum allowed per withdraw note: this is a temporary measure for restricted mainnet' + ), + }, + { + key: 'lifetimelimit', + label: t('Lifetime limit'), + value: ( + asset.node + .source as AssetsConnection_assetsConnection_edges_node_source_ERC20 + ).lifetimeLimit, + tooltip: t( + 'The lifetime limits deposit per address note: this is a temporary measure for restricted mainnet' + ), + }, + ]; + } + + const content = asset ? ( +
+ {details + .filter(({ value }) => value && value.length > 0) + .map(({ key, label, value, tooltip }) => ( +
+
+ {tooltip.length > 0 ? ( + + + {label} + + + ) : ( + {label} + )} +
+
+ {value} +
+
+ ))} +
+ ) : ( +
+ {t('No data')} +
+ ); + + return ( + } + open={open} + onChange={(isOpen) => onChange(isOpen)} + > + {content} + + + ); +}; diff --git a/libs/market-list/src/lib/components/asset-details-dialog/index.ts b/libs/market-list/src/lib/components/asset-details-dialog/index.ts new file mode 100644 index 000000000..2fca25b42 --- /dev/null +++ b/libs/market-list/src/lib/components/asset-details-dialog/index.ts @@ -0,0 +1 @@ +export * from './asset-details-dialog'; diff --git a/libs/market-list/src/lib/components/index.ts b/libs/market-list/src/lib/components/index.ts index 00dfebb45..a3a4ada6f 100644 --- a/libs/market-list/src/lib/components/index.ts +++ b/libs/market-list/src/lib/components/index.ts @@ -1,4 +1,5 @@ export * from './markets-container'; +export * from './asset-details-dialog'; export * from './select-market-columns'; export * from './select-market-table'; export * from './select-market'; diff --git a/libs/market-list/src/lib/components/markets-container/market-list-table.tsx b/libs/market-list/src/lib/components/markets-container/market-list-table.tsx index 9464ab42c..30de7b3f0 100644 --- a/libs/market-list/src/lib/components/markets-container/market-list-table.tsx +++ b/libs/market-list/src/lib/components/markets-container/market-list-table.tsx @@ -1,5 +1,8 @@ import { forwardRef } from 'react'; -import type { ValueFormatterParams } from 'ag-grid-community'; +import type { + GroupCellRendererParams, + ValueFormatterParams, +} from 'ag-grid-community'; import { PriceFlashCell, addDecimalsFormatNumber, @@ -18,6 +21,7 @@ import type { MarketList_markets, MarketList_markets_data, } from '../../__generated__'; +import { useAssetDetailsDialogStore } from '../asset-details-dialog'; type Props = AgGridReactProps | AgReactUiProps; @@ -29,6 +33,8 @@ type MarketListTableValueFormatterParams = Omit< }; export const MarketListTable = forwardRef((props, ref) => { + const { setAssetDetailsDialogOpen, setAssetDetailsDialogSymbol } = + useAssetDetailsDialogStore(); return ( ((props, ref) => { + value && value.length > 0 ? ( + + ) : ( + '' + ) + } /> { rowModelType="infinite" datasource={{ getRows }} ref={gridRef} - onRowClicked={({ data }: { data: MarketList_markets }) => - push(`/markets/${data.id}`) - } + onRowClicked={(rowEvent: RowClickedEvent) => { + const { data, event } = rowEvent; + // filters out clicks on the symbol column because it should display asset details + if ((event?.target as HTMLElement).tagName.toUpperCase() === 'BUTTON') + return; + push(`/markets/${(data as MarketList_markets).id}`); + }} /> ); diff --git a/libs/ui-toolkit/src/components/button/button.tsx b/libs/ui-toolkit/src/components/button/button.tsx index c27046447..ff6470fb1 100644 --- a/libs/ui-toolkit/src/components/button/button.tsx +++ b/libs/ui-toolkit/src/components/button/button.tsx @@ -15,9 +15,10 @@ import { } from '../../utils/class-names'; import classnames from 'classnames'; +type Variant = 'primary' | 'secondary' | 'trade' | 'accent' | 'inline-link'; interface CommonProps { children?: ReactNode; - variant?: 'primary' | 'secondary' | 'trade' | 'accent' | 'inline-link'; + variant?: Variant; className?: string; prependIconName?: IconName; appendIconName?: IconName; @@ -33,7 +34,7 @@ export interface AnchorButtonProps export const getButtonClasses = ( className?: string, - variant?: 'primary' | 'secondary' | 'trade' | 'accent' | 'inline-link', + variant?: Variant, boxShadow?: boolean ) => { const paddingLeftProvided = includesLeftPadding(className);