Compare commits

..
Author SHA1 Message Date
maciek 41fe5c05f1 Merge remote-tracking branch 'origin/develop' into chore/3667-refactor-docs-links-etc 2023-05-17 08:47:06 +02:00
maciek 6e3c133e44 chore: refactor createDocsUrl for fully static - fix failing build-specs 2023-05-17 08:46:52 +02:00
MaciekandMatthew Russell 59993e3ea6 chore(trading): suppress sentry logs when eth fails (#3726)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-05-16 23:31:53 -07:00
dexturr 54161a6dba chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-17 06:07:24 +00:00
Matthew Russell 26f524ea07 feat(trading): sidebar market list adjustments (#3792) 2023-05-16 18:45:17 -07:00
dexturr db13e6f21d chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-17 00:17:38 +00:00
dexturr 4a679813ba chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-16 18:07:35 +00:00
Matthew Russell a4279d5b5e feat(trading): sidebar market list (#3776) 2023-05-16 16:57:36 +00:00
maciek 9b9893499d chore: refactor createDocsUrl for fully static - fix failing lints 2023-05-16 17:58:56 +02:00
maciek df204332ed chore: refactor createDocsUrl for fully static - fix failing unit test in explorer 2023-05-16 17:52:05 +02:00
maciek b8c7f65526 chore: refactor createDocsUrl for fully static - fix failing unit test in governance 2023-05-16 17:43:37 +02:00
maciek 172c8f0322 Merge remote-tracking branch 'origin/develop' into chore/3667-refactor-docs-links-etc 2023-05-16 15:54:23 +02:00
maciek 293a3544e2 chore: refactor createDocsUrl for fully static use across all apps 2023-05-16 15:54:05 +02:00
m.ray 1a274a67c3 feat(trading): oracle banner check termination and settlement data oracles (#3707) 2023-05-16 15:12:22 +02:00
dexturr 1e9ff31fd7 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-16 12:08:54 +00:00
maciek 01728d5b85 chore: move docs links resolver to utils 2023-05-16 12:17:02 +02:00
Sam Keen aa2b755b93 fix(governance): reward aggregation for validator types (#3773) 2023-05-16 08:40:36 +01:00
Matthew Russell 4a7296312a fix(smart-contracts,deposits): add different abi for tether (#3775) 2023-05-16 08:40:05 +01:00
dexturr 1418a2259f chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-16 06:07:24 +00:00
dexturr 7e5ef0157a chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-16 00:14:15 +00:00
dexturr 0cf764f11e chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-15 18:09:01 +00:00
Sam Keen 1c626aeeb2 fix(governance): useWeb3ConnectStore instead of app state for eth connect d… (#3750) 2023-05-15 16:21:20 +01:00
Bartłomiej Głownia c4cec47b0b feat(trading): add oracle dialog trigger to oracle banner, display status reas… (#3727) 2023-05-15 16:20:30 +01:00
dexturr 1a67c41bb4 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-15 12:08:57 +00:00
Matthew Russell fc66240390 fix(governance): check value of telemetry local storage entry (#3762) 2023-05-15 09:48:42 +01:00
Maciek fc0da98683 fix(trading): match link as active more strictly (#3754) 2023-05-15 10:08:21 +02:00
Maciek 4d0dafb113 fix(trading): match link as active more strictly (#3754) 2023-05-15 10:08:04 +02:00
dexturr eda431be9e chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-15 06:07:48 +00:00
190 changed files with 3083 additions and 2123 deletions
@@ -3,6 +3,7 @@ import { Header } from './header';
import { MemoryRouter } from 'react-router-dom';
jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'),
NetworkSwitcher: () => (
<div data-testid="network-switcher">NetworkSwitcher</div>
),
+1 -1
View File
@@ -204,7 +204,7 @@ const AppContainer = () => {
]);
useEffect(() => {
if (ENV.dsn && telemetryOn) {
if (ENV.dsn && telemetryOn === 'true') {
Sentry.init({
dsn: ENV.dsn,
tracesSampleRate: 0.1,
@@ -1,23 +1,14 @@
import { Button } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
export const EthConnectPrompt = () => {
const { t } = useTranslation();
const { appDispatch } = useAppState();
const { open } = useWeb3ConnectStore();
return (
<Button
variant="default"
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
isOpen: true,
})
}
onClick={open}
fill={true}
data-testid="connect-to-eth-btn"
>
@@ -2,10 +2,7 @@ import { Button } from '@vegaprotocol/ui-toolkit';
import { useWeb3React } from '@web3-react/core';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
export const EthWalletContainer = ({
children,
@@ -14,21 +11,12 @@ export const EthWalletContainer = ({
}) => {
const { account } = useWeb3React();
const { t } = useTranslation();
const { appDispatch } = useAppState();
const { open } = useWeb3ConnectStore();
if (!account) {
return (
<div className="w-full text-center">
<Button
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
isOpen: true,
})
}
>
{t('connectEthWallet')}
</Button>
<Button onClick={open}>{t('connectEthWallet')}</Button>
</div>
);
}
@@ -27,7 +27,11 @@ import {
import { Loader } from '@vegaprotocol/ui-toolkit';
import colors from 'tailwindcss/colors';
import { useBalances } from '../../lib/balances/balances-store';
import { useEthereumConfig, useWeb3Disconnect } from '@vegaprotocol/web3';
import {
useEthereumConfig,
useWeb3ConnectStore,
useWeb3Disconnect,
} from '@vegaprotocol/web3';
import { getChainName } from '@vegaprotocol/web3';
const removeLeadingAddressSymbol = (key: string) => {
@@ -189,6 +193,7 @@ export const EthWallet = () => {
const pendingTxs = usePendingTransactions();
const disconnect = useWeb3Disconnect(connector);
const { config } = useEthereumConfig();
const { open } = useWeb3ConnectStore();
return (
<WalletCard>
@@ -233,12 +238,7 @@ export const EthWallet = () => {
) : (
<Button
fill={true}
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
isOpen: true,
})
}
onClick={open}
data-testid="connect-to-eth-wallet-button"
>
{t('connectEthWalletToAssociate')}
@@ -1,6 +1,6 @@
import { Link } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import { ExternalLinks } from '@vegaprotocol/utils';
import { ExternalLinks } from '@vegaprotocol/environment';
export const DownloadWalletPrompt = () => {
const { t } = useTranslation();
@@ -25,20 +25,26 @@ export function Web3Connector({
connectors,
chainId,
}: Web3ConnectorProps) {
const { appState, appDispatch } = useAppState();
const { open, close, isOpen } = useWeb3ConnectStore();
const setDialogOpen = useCallback(
(isOpen: boolean) => {
appDispatch({ type: AppStateActionType.SET_ETH_WALLET_OVERLAY, isOpen });
if (isOpen) {
open();
} else {
close();
}
},
[appDispatch]
[open, close]
);
const appChainId = Number(chainId);
return (
<>
<Web3Content appChainId={appChainId}>{children}</Web3Content>
<Web3ConnectDialog
connectors={connectors}
dialogOpen={appState.ethConnectOverlay}
dialogOpen={isOpen}
setDialogOpen={setDialogOpen}
desiredChainId={appChainId}
/>
@@ -34,9 +34,6 @@ export interface AppState {
/** Whether or not the manage VEGA wallet overlay is open */
vegaWalletManageOverlay: boolean;
/** Whether or not the connect to Ethereum wallet overlay is open */
ethConnectOverlay: boolean;
/** Whether or not the transaction modal is open */
transactionOverlay: boolean;
/**
@@ -57,7 +54,6 @@ export enum AppStateActionType {
REFRESH_BALANCES,
SET_VEGA_WALLET_OVERLAY,
SET_VEGA_WALLET_MANAGE_OVERLAY,
SET_ETH_WALLET_OVERLAY,
SET_DRAWER,
REFRESH_ASSOCIATED_BALANCES,
SET_ASSOCIATION_BREAKDOWN,
@@ -81,10 +77,6 @@ export type AppStateAction =
type: AppStateActionType.SET_VEGA_WALLET_MANAGE_OVERLAY;
isOpen: boolean;
}
| {
type: AppStateActionType.SET_ETH_WALLET_OVERLAY;
isOpen: boolean;
}
| {
type: AppStateActionType.SET_DRAWER;
isOpen: boolean;
@@ -16,7 +16,6 @@ const initialAppState: AppState = {
totalSupply: new BigNumber(0),
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
transactionOverlay: false,
bannerMessage: '',
disconnectNotice: false,
@@ -45,12 +44,6 @@ function appStateReducer(state: AppState, action: AppStateAction): AppState {
vegaWalletOverlay: action.isOpen ? false : state.vegaWalletOverlay,
};
}
case AppStateActionType.SET_ETH_WALLET_OVERLAY: {
return {
...state,
ethConnectOverlay: action.isOpen,
};
}
case AppStateActionType.SET_DRAWER: {
return {
...state,
+2 -1
View File
@@ -12,7 +12,8 @@ import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
import { ProtocolUpgradeProposalsListItem } from '../proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item';
import Routes from '../routes';
import { ExternalLinks, removePaginationWrapper } from '@vegaprotocol/utils';
import { ExternalLinks } from '@vegaprotocol/environment';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
import {
@@ -53,7 +53,6 @@ const mockAppState: AppState = {
totalSupply: new BigNumber(65000000),
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
transactionOverlay: false,
bannerMessage: '',
disconnectNotice: false,
@@ -8,12 +8,11 @@ import { ProposalsListFilter } from '../proposals-list-filter';
import Routes from '../../../routes';
import { Button, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { Link } from 'react-router-dom';
import { createDocsLinks, ExternalLinks } from '@vegaprotocol/utils';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { useEnvironment } from '@vegaprotocol/environment';
import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment';
interface ProposalsListProps {
proposals: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
@@ -36,7 +35,6 @@ export const ProposalsList = ({
protocolUpgradeProposals,
lastBlockHeight,
}: ProposalsListProps) => {
const { VEGA_DOCS_URL } = useEnvironment();
const { t } = useTranslation();
const [filterString, setFilterString] = useState('');
const sortedProposals = proposals.reduce(
@@ -83,9 +81,9 @@ export const ProposalsList = ({
marginBottom={false}
title={t('pageTitleProposals')}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<div className="xs:justify-self-end" data-testid="new-proposal-link">
<ExternalLink href={createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}>
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE}>
<Button variant="primary" size="sm">
<div className="flex items-center gap-1">
{t('NewProposal')}
@@ -2,9 +2,10 @@ import { render, screen } from '@testing-library/react';
import { ProposalFormTerms } from './proposal-form-terms';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_DOCS_URL: 'https://docs.vega.xyz',
}),
...jest.requireActual('@vegaprotocol/environment'),
DocsLinks: {
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
},
}));
const renderComponent = () => {
@@ -5,8 +5,7 @@ import {
ExternalLink,
TextArea,
} from '@vegaprotocol/ui-toolkit';
import { createDocsLinks } from '@vegaprotocol/utils';
import { useEnvironment } from '@vegaprotocol/environment';
import { DocsLinks } from '@vegaprotocol/environment';
import type { UseFormRegisterReturn } from 'react-hook-form';
interface ProposalFormTermsProps {
@@ -22,23 +21,20 @@ export const ProposalFormTerms = ({
labelOverride,
docsLink,
}: ProposalFormTermsProps) => {
const { VEGA_DOCS_URL } = useEnvironment();
const { t } = useTranslation();
return (
<FormGroup
label={labelOverride || t('ProposalTerms')}
labelFor="proposal-terms"
>
{docsLink && VEGA_DOCS_URL && (
{docsLink && DocsLinks && (
<div className="mt-[-4px] mb-2 text-sm font-light">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${docsLink}`}
href={`${DocsLinks.PROPOSALS_GUIDE}${docsLink}`}
target="_blank"
>
{`${createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}${docsLink}`}
{`${DocsLinks.PROPOSALS_GUIDE}${docsLink}`}
</ExternalLink>
</div>
)}
@@ -16,7 +16,6 @@ const mockAppState: AppState = {
totalSupply: mockTotalSupply,
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
transactionOverlay: false,
bannerMessage: '',
disconnectNotice: false,
@@ -10,9 +10,10 @@ import type { MockedResponse } from '@apollo/client/testing';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_DOCS_URL: 'https://docs.vega.xyz',
}),
...jest.requireActual('@vegaprotocol/environment'),
DocsLinks: {
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
},
}));
const updateMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
@@ -5,7 +5,7 @@ import {
useProposalSubmit,
deadlineToRoundedHours,
} from '@vegaprotocol/proposals';
import { useEnvironment } from '@vegaprotocol/environment';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import {
ProposalFormDescription,
ProposalFormSubheader,
@@ -17,7 +17,6 @@ import {
import { ProposalMinRequirements } from '../../components/shared';
import { AsyncRenderer, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { Heading } from '../../../../components/heading';
import { createDocsLinks } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -42,7 +41,7 @@ export const ProposeFreeform = () => {
NetworkParams.governance_proposal_freeform_minProposerBalance,
NetworkParams.spam_protection_proposal_min_tokens,
]);
const { VEGA_DOCS_URL, VEGA_EXPLORER_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
@@ -111,17 +110,13 @@ export const ProposeFreeform = () => {
userAction={ProposalUserAction.CREATE}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
target="_blank"
>{`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}</ExternalLink>
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
</p>
)}
@@ -10,9 +10,10 @@ import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import type { MockedResponse } from '@apollo/client/testing';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_DOCS_URL: 'https://docs.vega.xyz',
}),
...jest.requireActual('@vegaprotocol/environment'),
DocsLinks: {
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
},
}));
const updateMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
@@ -1,10 +1,7 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useForm } from 'react-hook-form';
import {
createDocsLinks,
suitableForSyntaxHighlighter,
} from '@vegaprotocol/utils';
import { suitableForSyntaxHighlighter } from '@vegaprotocol/utils';
import { useNetworkParams } from '@vegaprotocol/network-parameters';
import {
getClosingTimestamp,
@@ -12,7 +9,7 @@ import {
useProposalSubmit,
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment } from '@vegaprotocol/environment';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import {
ProposalFormDescription,
ProposalFormSubheader,
@@ -84,7 +81,7 @@ export const ProposeNetworkParameter = () => {
loading: networkParamsLoading,
error: networkParamsError,
} = useNetworkParams();
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
@@ -175,17 +172,13 @@ export const ProposeNetworkParameter = () => {
userAction={ProposalUserAction.CREATE}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
target="_blank"
>{`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}</ExternalLink>
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
</p>
)}
@@ -10,9 +10,10 @@ import type { MockedResponse } from '@apollo/client/testing';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_DOCS_URL: 'https://docs.vega.xyz',
}),
...jest.requireActual('@vegaprotocol/environment'),
DocsLinks: {
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
},
}));
const newAssetNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
@@ -7,8 +7,8 @@ import {
useProposalSubmit,
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment } from '@vegaprotocol/environment';
import { createDocsLinks, validateJson } from '@vegaprotocol/utils';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -53,7 +53,7 @@ export const ProposeNewAsset = () => {
NetworkParams.governance_proposal_asset_minProposerBalance,
NetworkParams.spam_protection_proposal_min_tokens,
]);
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
@@ -142,17 +142,13 @@ export const ProposeNewAsset = () => {
userAction={ProposalUserAction.CREATE}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
target="_blank"
>{`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}</ExternalLink>
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
</p>
)}
@@ -10,9 +10,10 @@ import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_DOCS_URL: 'https://docs.vega.xyz',
}),
...jest.requireActual('@vegaprotocol/environment'),
DocsLinks: {
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
},
}));
const newMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
@@ -6,8 +6,8 @@ import {
useProposalSubmit,
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment } from '@vegaprotocol/environment';
import { createDocsLinks, validateJson } from '@vegaprotocol/utils';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -51,7 +51,7 @@ export const ProposeNewMarket = () => {
NetworkParams.governance_proposal_market_minProposerBalance,
NetworkParams.spam_protection_proposal_min_tokens,
]);
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
@@ -132,17 +132,13 @@ export const ProposeNewMarket = () => {
userAction={ProposalUserAction.CREATE}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
target="_blank"
>{`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}</ExternalLink>
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
</p>
)}
@@ -2,12 +2,11 @@ import Routes from '../../routes';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '@vegaprotocol/environment';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { Heading } from '../../../components/heading';
import { createDocsLinks } from '@vegaprotocol/utils';
export const Propose = () => {
const { VEGA_DOCS_URL, VEGA_EXPLORER_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
return (
@@ -15,14 +14,11 @@ export const Propose = () => {
<section className="pb-6">
<Heading title={t('NewProposal')} />
<div className="text-sm">
{VEGA_DOCS_URL && (
{DocsLinks && (
<p>
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}
target="_blank"
>
{createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE} target="_blank">
{DocsLinks.PROPOSALS_GUIDE}
</ExternalLink>
</p>
)}
@@ -1,6 +1,6 @@
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useEnvironment } from '@vegaprotocol/environment';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { Heading } from '../../../../components/heading';
import {
AsyncRenderer,
@@ -9,7 +9,7 @@ import {
InputError,
TextArea,
} from '@vegaprotocol/ui-toolkit';
import { createDocsLinks, validateJson } from '@vegaprotocol/utils';
import { validateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -39,7 +39,7 @@ export const ProposeRaw = () => {
NetworkParams.governance_proposal_freeform_minProposerBalance,
NetworkParams.spam_protection_proposal_min_tokens,
]);
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
@@ -79,14 +79,11 @@ export const ProposeRaw = () => {
spamProtectionMin={params.spam_protection_proposal_min_tokens}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}
target="_blank"
>
{createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE} target="_blank">
{DocsLinks.PROPOSALS_GUIDE}
</ExternalLink>
</p>
)}
@@ -10,9 +10,10 @@ import type { MockedResponse } from '@apollo/client/testing';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_DOCS_URL: 'https://docs.vega.xyz',
}),
...jest.requireActual('@vegaprotocol/environment'),
DocsLinks: {
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
},
}));
const updateAssetNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
@@ -6,8 +6,8 @@ import {
useProposalSubmit,
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment } from '@vegaprotocol/environment';
import { createDocsLinks, validateJson } from '@vegaprotocol/utils';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -51,7 +51,7 @@ export const ProposeUpdateAsset = () => {
NetworkParams.governance_proposal_updateAsset_minProposerBalance,
NetworkParams.spam_protection_proposal_min_tokens,
]);
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
@@ -132,17 +132,13 @@ export const ProposeUpdateAsset = () => {
userAction={ProposalUserAction.CREATE}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
target="_blank"
>{`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}</ExternalLink>
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
</p>
)}
@@ -7,8 +7,8 @@ import {
getEnactmentTimestamp,
useProposalSubmit,
} from '@vegaprotocol/proposals';
import { useEnvironment } from '@vegaprotocol/environment';
import { createDocsLinks, validateJson } from '@vegaprotocol/utils';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -98,7 +98,7 @@ export const ProposeUpdateMarket = () => {
const [selectedMarket, setSelectedMarket] = useState<string | undefined>(
undefined
);
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
@@ -181,17 +181,13 @@ export const ProposeUpdateMarket = () => {
userAction={ProposalUserAction.CREATE}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
target="_blank"
>{`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}</ExternalLink>
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
</p>
)}
@@ -79,7 +79,24 @@ export const generateEpochIndividualRewardsList = ({
epoch?.rewards.push(asset);
}
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
if (!asset.rewardTypes[rewardType]) {
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
} else {
const previousAmount = asset.rewardTypes[rewardType]?.amount;
const previousPercentageOfTotal =
asset.rewardTypes[rewardType]?.percentageOfTotal;
asset.rewardTypes[rewardType] = {
amount: previousAmount
? new BigNumber(previousAmount).plus(amount).toString()
: amount,
percentageOfTotal: previousPercentageOfTotal
? new BigNumber(previousPercentageOfTotal)
.plus(percentageOfTotal)
.toString()
: percentageOfTotal,
};
}
// totalAmount is the sum of all rewardTypes amounts
asset.totalAmount = Object.values(asset.rewardTypes).reduce(
@@ -11,7 +11,6 @@ import {
ExternalLink,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { createDocsLinks } from '@vegaprotocol/utils';
import {
useNetworkParams,
NetworkParams,
@@ -21,7 +20,7 @@ import { EpochCountdown } from '../../../components/epoch-countdown';
import { Heading, SubHeading } from '../../../components/heading';
import { EpochIndividualRewards } from '../epoch-individual-rewards/epoch-individual-rewards';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { useEnvironment } from '@vegaprotocol/environment';
import { DocsLinks } from '@vegaprotocol/environment';
import { ConnectToSeeRewards } from '../connect-to-see-rewards';
import { EpochTotalRewards } from '../epoch-total-rewards/epoch-total-rewards';
@@ -29,7 +28,6 @@ type RewardsView = 'total' | 'individual';
export const RewardsPage = () => {
const { t } = useTranslation();
const { VEGA_DOCS_URL } = useEnvironment();
const { pubKey, pubKeys } = useVegaWallet();
const [toggleRewardsView, setToggleRewardsView] =
useState<RewardsView>('total');
@@ -68,9 +66,9 @@ export const RewardsPage = () => {
<Heading title={t('pageTitleRewards')} />
<p className="mb-12">
{t('rewardsIntro')}{' '}
{VEGA_DOCS_URL && (
{DocsLinks && (
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).REWARDS_GUIDE}
href={DocsLinks.REWARDS_GUIDE}
target="_blank"
data-testid="rewards-guide-link"
className="text-white"
@@ -1,14 +1,12 @@
import { useTranslation } from 'react-i18next';
import { EpochData } from './epoch-data';
import { useEnvironment } from '@vegaprotocol/environment';
import { DocsLinks } from '@vegaprotocol/environment';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { createDocsLinks } from '@vegaprotocol/utils';
import { Heading } from '../../../components/heading';
import React from 'react';
export const Staking = () => {
const { t } = useTranslation();
const { VEGA_DOCS_URL } = useEnvironment();
return (
<>
@@ -16,9 +14,9 @@ export const Staking = () => {
<section>
<p className="mb-12">
{t('stakingIntro')}{' '}
{VEGA_DOCS_URL && (
{DocsLinks && (
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
href={DocsLinks.STAKING_GUIDE}
target="_blank"
data-testid="staking-guide-link"
className="text-white"
@@ -2,12 +2,8 @@ import { useMemo, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import BigNumber from 'bignumber.js';
import * as Schema from '@vegaprotocol/types';
import {
createDocsLinks,
removePaginationWrapper,
toBigNum,
} from '@vegaprotocol/utils';
import { useEnvironment } from '@vegaprotocol/environment';
import { removePaginationWrapper, toBigNum } from '@vegaprotocol/utils';
import { DocsLinks } from '@vegaprotocol/environment';
import { Link as UTLink, Toggle } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '../../../../lib/format-number';
@@ -42,7 +38,6 @@ export const ValidatorTables = ({
previousEpochData,
}: ValidatorsTableProps) => {
const { t } = useTranslation();
const { VEGA_DOCS_URL } = useEnvironment();
const {
appState: { decimals },
} = useAppState();
@@ -213,12 +208,12 @@ export const ValidatorTables = ({
<>
<SubHeading title={t('status-pending')} />
<p>
{VEGA_DOCS_URL && (
{DocsLinks && (
<>
<span>{t('pendingDescription1')} </span>
<span>
<UTLink
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
href={DocsLinks.STAKING_GUIDE}
target="_blank"
data-testid="validator-forum-link"
>
@@ -1,3 +1,4 @@
import { useEffect, useState } from 'react';
import { ENV } from '../../../config';
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
@@ -28,6 +29,7 @@ export const NodeContainer = ({
const { t } = useTranslation();
const { pubKey } = useVegaWallet();
const { delegationsPagination } = ENV;
const { data, loading, error, refetch } = useStakingQuery({
variables: {
partyId: pubKey || '',
@@ -38,6 +40,20 @@ export const NodeContainer = ({
: undefined,
},
});
const [isRefetching, setIsRefetching] = useState(false);
useEffect(() => {
if (error && error.message.includes('failed to get party for ID')) {
setIsRefetching(true);
// The API errors if there is a pubkey, but it hasn't interacted with the
// chain before. In that case, retry the query with empty pubKey
refetch({
partyId: '',
}).finally(() => setIsRefetching(false));
}
}, [error, refetch, delegationsPagination]);
const { data: previousEpochData } = usePreviousEpochQuery({
variables: {
epochId: (Number(data?.epoch.id) - 1).toString(),
@@ -47,7 +63,7 @@ export const NodeContainer = ({
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
if (error) {
if (error && !isRefetching) {
return (
<Callout intent={Intent.Danger} title={t('Something went wrong')}>
<pre>
@@ -59,7 +75,7 @@ export const NodeContainer = ({
);
}
if (loading) {
if (loading || isRefetching) {
return (
<Splash>
<SplashLoader />
@@ -1,7 +1,11 @@
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useEnvironment } from '@vegaprotocol/environment';
import { createDocsLinks, ExternalLinks, toBigNum } from '@vegaprotocol/utils';
import {
useEnvironment,
DocsLinks,
ExternalLinks,
} from '@vegaprotocol/environment';
import { toBigNum } from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types';
import {
Link as UTLink,
@@ -64,7 +68,7 @@ export const ValidatorTable = ({
stakedTotal,
previousEpochData,
}: ValidatorTableProps) => {
const { ETHERSCAN_URL, VEGA_DOCS_URL } = useEnvironment();
const { ETHERSCAN_URL } = useEnvironment();
const { t } = useTranslation();
const {
appState: { decimals },
@@ -90,9 +94,9 @@ export const ValidatorTable = ({
<>
<p className="mb-12">
{t('validatorFormIntro')}{' '}
{VEGA_DOCS_URL && (
{DocsLinks && (
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
href={DocsLinks.STAKING_GUIDE}
target="_blank"
data-testid="validator-table-staking-guide-link"
className="text-white"
+2 -1
View File
@@ -3,7 +3,8 @@ import { Trans, useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { Heading, SubHeading } from '../../components/heading';
import { ExternalLinks, toBigNum } from '@vegaprotocol/utils';
import { ExternalLinks } from '@vegaprotocol/environment';
import { toBigNum } from '@vegaprotocol/utils';
import { useAppState } from '../../contexts/app-state/app-state-context';
import { useDocumentTitle } from '../../hooks/use-document-title';
import type { RouteChildProps } from '..';
+166 -42
View File
@@ -81,8 +81,8 @@
"tranche_start": "2023-04-20T00:00:00.000Z",
"tranche_end": "2023-05-20T00:00:00.000Z",
"total_added": "19242.125",
"total_removed": "2249.511113406525",
"locked_amount": "3199.3076512827919033125",
"total_removed": "2667.900599530005",
"locked_amount": "1760.572777247300418465",
"deposits": [
{
"amount": "188",
@@ -276,6 +276,16 @@
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
"tx": "0x156ef84c345adecdf7b1c55756b1f4326716d1b0191ce1a208f614fc807ce033"
},
{
"amount": "219.675555560475",
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
"tx": "0x682c6453a6c681312a81e15f244e4885c0ceaab35c7dc041c02d536ff42ca62f"
},
{
"amount": "198.713930563005",
"user": "0xDd7a98557586ce21f770662319C2047C5a3bD605",
"tx": "0x9cdddebcfa4f7e189e435c3a0b5f69ecc6240d59b4d9f959739458f610d8fa95"
},
{
"amount": "202.093666077975",
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
@@ -355,6 +365,12 @@
"tranche_id": 56,
"tx": "0x156ef84c345adecdf7b1c55756b1f4326716d1b0191ce1a208f614fc807ce033"
},
{
"amount": "219.675555560475",
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
"tranche_id": 56,
"tx": "0x682c6453a6c681312a81e15f244e4885c0ceaab35c7dc041c02d536ff42ca62f"
},
{
"amount": "202.093666077975",
"user": "0x1447Efc62d5077d7AbB20492dF831Dd6c9Eb1756",
@@ -381,8 +397,8 @@
}
],
"total_tokens": "1207.5",
"withdrawn_tokens": "866.872728582",
"remaining_tokens": "340.627271418"
"withdrawn_tokens": "1086.548284142475",
"remaining_tokens": "120.951715857525"
},
{
"address": "0x33Ce1D9E53AFb7367E34749517C086405a651a95",
@@ -422,6 +438,12 @@
"tranche_id": 56,
"tx": "0x47f5bf2c758c5270dd1b6519ac649ddabef8199f8a0bae319e36f8ac5c9c142e"
},
{
"amount": "198.713930563005",
"user": "0xDd7a98557586ce21f770662319C2047C5a3bD605",
"tranche_id": 56,
"tx": "0x9cdddebcfa4f7e189e435c3a0b5f69ecc6240d59b4d9f959739458f610d8fa95"
},
{
"amount": "195.89040769089",
"user": "0xDd7a98557586ce21f770662319C2047C5a3bD605",
@@ -430,8 +452,8 @@
}
],
"total_tokens": "914.25",
"withdrawn_tokens": "624.076855605525",
"remaining_tokens": "290.173144394475"
"withdrawn_tokens": "822.79078616853",
"remaining_tokens": "91.45921383147"
},
{
"address": "0x9573BDF7FfC5519912d293e4D1f750eab2E471E7",
@@ -943,7 +965,7 @@
"tranche_start": "2023-04-06T00:00:00.000Z",
"tranche_end": "2023-05-06T00:00:00.000Z",
"total_added": "14610",
"total_removed": "6918.45090157707",
"total_removed": "7227.45090157707",
"locked_amount": "0",
"deposits": [
{
@@ -1163,6 +1185,11 @@
"user": "0x6324334064d1d807940191C658d925DA7d323b12",
"tx": "0xd4d9926de15e6dc4992c47e9fbb9d25bf873d7e331524994bb6139a2953ea96a"
},
{
"amount": "309",
"user": "0xf9c6C5F942B94Db9dedC550956865cf99D743959",
"tx": "0x4fa5d02d920d90835694cbab1e41a66d317b2bc35860d07a5ccd70d12182116b"
},
{
"amount": "106.53500000286",
"user": "0x6F32AA5A6198329c16e438512F992a0548C856f9",
@@ -1794,10 +1821,17 @@
"tx": "0x8a0328460c26941859c8ce400e7b18d41701fd620535f757f17e4c1d2c930831"
}
],
"withdrawals": [],
"withdrawals": [
{
"amount": "309",
"user": "0xf9c6C5F942B94Db9dedC550956865cf99D743959",
"tranche_id": 54,
"tx": "0x4fa5d02d920d90835694cbab1e41a66d317b2bc35860d07a5ccd70d12182116b"
}
],
"total_tokens": "309",
"withdrawn_tokens": "0",
"remaining_tokens": "309"
"withdrawn_tokens": "309",
"remaining_tokens": "0"
},
{
"address": "0xB12Cdb63E38f9a6ff2C0F68aBC3CA9beFCb7148c",
@@ -5011,7 +5045,7 @@
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "86666.297",
"total_removed": "0",
"locked_amount": "48435.2942072747597588603",
"locked_amount": "47902.6878201683513969045",
"deposits": [
{
"amount": "86666.297",
@@ -5077,7 +5111,7 @@
"tranche_end": "2023-06-01T00:00:00.000Z",
"total_added": "2500",
"total_removed": "0",
"locked_amount": "233.35129858567365",
"locked_amount": "202.53945996133505",
"deposits": [
{
"amount": "2500",
@@ -5110,7 +5144,7 @@
"tranche_end": "2023-11-01T00:00:00.000Z",
"total_added": "15000.000000000000015",
"total_removed": "0",
"locked_amount": "13857.715315519323013857715315519323",
"locked_amount": "13674.853751509662013674853751509662",
"deposits": [
{
"amount": "1.5e-14",
@@ -5218,7 +5252,7 @@
"tranche_end": "2023-09-01T00:00:00.000Z",
"total_added": "17500",
"total_removed": "0",
"locked_amount": "10365.7040999899345",
"locked_amount": "10152.36560864532825",
"deposits": [
{
"amount": "12500",
@@ -5484,8 +5518,8 @@
"tranche_start": "2023-02-01T00:00:00.000Z",
"tranche_end": "2023-08-01T00:00:00.000Z",
"total_added": "37500",
"total_removed": "18302.01762945",
"locked_amount": "16157.7295311540825",
"total_removed": "18500.56159815",
"locked_amount": "15692.99848449969375",
"deposits": [
{
"amount": "7500",
@@ -5504,6 +5538,11 @@
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0x0a46fa3cc0154727ae4c7abca97bd7e26e6835018f96cf3a3860a9fcc791b9a4"
},
{
"amount": "198.5439687",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0xc25a025176872981def8433ef0982ba306737aaf2cf0867f2259fd19492af2b5"
},
{
"amount": "164.727209925",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -5688,6 +5727,12 @@
"tranche_id": 34,
"tx": "0x0a46fa3cc0154727ae4c7abca97bd7e26e6835018f96cf3a3860a9fcc791b9a4"
},
{
"amount": "198.5439687",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 34,
"tx": "0xc25a025176872981def8433ef0982ba306737aaf2cf0867f2259fd19492af2b5"
},
{
"amount": "164.727209925",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -5870,8 +5915,8 @@
}
],
"total_tokens": "7500",
"withdrawn_tokens": "4092.25895865",
"remaining_tokens": "3407.74104135"
"withdrawn_tokens": "4290.80292735",
"remaining_tokens": "3209.19707265"
},
{
"address": "0x0B4e6fcE839B01ef43DA6F890FAC7B1Afb004600",
@@ -5915,7 +5960,7 @@
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "129999.45",
"total_removed": "0",
"locked_amount": "48391.1030956730271686715",
"locked_amount": "47858.982644879740029465",
"deposits": [
{
"amount": "129999.45",
@@ -5948,7 +5993,7 @@
"tranche_end": "2024-04-01T00:00:00.000Z",
"total_added": "54144.7663",
"total_removed": "0",
"locked_amount": "47633.78041726290106106372",
"locked_amount": "47301.94373541695517923511",
"deposits": [
{
"amount": "54144.7663",
@@ -5981,7 +6026,7 @@
"tranche_end": "2023-09-03T00:00:00.000Z",
"total_added": "62600",
"total_removed": "0",
"locked_amount": "19035.19782470827248",
"locked_amount": "18650.49049340436032",
"deposits": [
{
"amount": "10000",
@@ -6174,7 +6219,7 @@
"tranche_end": "2023-09-17T00:00:00.000Z",
"total_added": "5000",
"total_removed": "0",
"locked_amount": "1712.164034753932",
"locked_amount": "1681.4366121258245",
"deposits": [
{
"amount": "5000",
@@ -7243,7 +7288,7 @@
"tranche_end": "2023-06-02T00:00:00.000Z",
"total_added": "1939928.38",
"total_removed": "1709370.7872515768348",
"locked_amount": "95603.78713182322084178028",
"locked_amount": "83681.9872917193018111752",
"deposits": [
{
"amount": "1852091.69",
@@ -41115,7 +41160,7 @@
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "3732368.4671",
"total_removed": "715655.108029600523393",
"locked_amount": "171411.0598475595488585533025",
"locked_amount": "153091.4042231713229007715616",
"deposits": [
{
"amount": "1998.95815",
@@ -42507,8 +42552,8 @@
"tranche_start": "2022-06-05T00:00:00.000Z",
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "15870102.715470999700000001",
"total_removed": "882197.30251812695176852",
"locked_amount": "5907500.19821835867969082523486646844714787",
"total_removed": "885240.49984321970795352",
"locked_amount": "5842539.8756085922637222718130454678926737",
"deposits": [
{
"amount": "16249.93",
@@ -43037,6 +43082,16 @@
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
"tx": "0xdedcbf90e8f19c2a732ba2da663f59b5a19ad94800ddca6a3575019f19e9ca20"
},
{
"amount": "856.50757224183006",
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
"tx": "0xc8835acd2e148eeaacde2a0df0ecad19c22717c7c51b207fdeac9a1af3f98c88"
},
{
"amount": "2186.689752850926125",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0xb048cb2dcdfc30508770a3a112ddbacd63210f740cc887559660522bd506612c"
},
{
"amount": "10150.87581603206683",
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
@@ -45223,6 +45278,12 @@
"tranche_id": 2,
"tx": "0x989d22d5ba66353cb132b5643100a99e78133e5d40aef1ac91ed6ab0a2290299"
},
{
"amount": "2186.689752850926125",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 2,
"tx": "0xb048cb2dcdfc30508770a3a112ddbacd63210f740cc887559660522bd506612c"
},
{
"amount": "913.910324501590625",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -46779,8 +46840,8 @@
}
],
"total_tokens": "259998.8875",
"withdrawn_tokens": "161285.6283934563635",
"remaining_tokens": "98713.2591065436365"
"withdrawn_tokens": "163472.318146307289625",
"remaining_tokens": "96526.569353692710375"
},
{
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
@@ -47013,6 +47074,12 @@
"tranche_id": 2,
"tx": "0xa515dafd399b366ba7f510ea6314587eb19e83fa92dbd5c2a212f3541a0cc4ef"
},
{
"amount": "856.50757224183006",
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
"tranche_id": 2,
"tx": "0xc8835acd2e148eeaacde2a0df0ecad19c22717c7c51b207fdeac9a1af3f98c88"
},
{
"amount": "10150.87581603206683",
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
@@ -47303,8 +47370,8 @@
}
],
"total_tokens": "150551.801",
"withdrawn_tokens": "93772.59940382165655",
"remaining_tokens": "56779.20159617834345"
"withdrawn_tokens": "94629.10697606348661",
"remaining_tokens": "55922.69402393651339"
},
{
"address": "0x4d982Ab0823fD2f48e934a7be2bb0a5374a26148",
@@ -59316,8 +59383,8 @@
"tranche_start": "2022-06-05T00:00:00.000Z",
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "472355.6199999996",
"total_removed": "44796.7287294743416",
"locked_amount": "27161.0622602365460539456394114544",
"total_removed": "45345.4733895423416",
"locked_amount": "24258.2081069361612058598496600864",
"deposits": [
{
"amount": "3000",
@@ -65956,6 +66023,31 @@
"user": "0xAc5bd5a3890B12B0da7cF1Ad846F6b8b260B60CC",
"tx": "0x8a4862b1fdacb7e24492397fac234152aae4146d9cca2f0a476e918cbe130af7"
},
{
"amount": "84.67351598",
"user": "0x256274a216e0a349dC18cae08Ca45631176AF437",
"tx": "0x891254ca1ea63a87ea7e14c3a985f58a403f4a0baf9fba86582785da6d2ae806"
},
{
"amount": "84.673820396",
"user": "0xB3ae40b11af91BbD456022Bd42B4f36A8C0AEA9D",
"tx": "0x26941fa486bf2aa16da2748a2326f87c3491c4f6f939f4329bd1eb3171ce6c3d"
},
{
"amount": "189.69504693",
"user": "0xC198534Ee3D619770C21FFAF5e9F067F8bbC07FD",
"tx": "0x803ba71987573a0ab20d4da7eb1dc7fe5e2fd8184199f0ee5d7a949b3d9f4e3e"
},
{
"amount": "189.696721206",
"user": "0xf0d681c15447dCfaa6002f17078944FCe75B5e26",
"tx": "0xed29b9b07eff1d6c43d586e545bf5557390f5c6040924718d0f28968fa34a8d5"
},
{
"amount": "0.005555556",
"user": "0xf0d681c15447dCfaa6002f17078944FCe75B5e26",
"tx": "0xca745232557385fff568386375d868633bd9542981dd07a6c471b4633dd5c9b8"
},
{
"amount": "182.662252664",
"user": "0xb1169C6daAc76bAcaf0D8f87641Fc38fbabe569F",
@@ -70942,10 +71034,23 @@
"tx": "0x1c2c084e3efafb2ffa09c0b259bd229cce0d6da04dd092d911c7e6136a7ac8a9"
}
],
"withdrawals": [],
"withdrawals": [
{
"amount": "189.696721206",
"user": "0xf0d681c15447dCfaa6002f17078944FCe75B5e26",
"tranche_id": 5,
"tx": "0xed29b9b07eff1d6c43d586e545bf5557390f5c6040924718d0f28968fa34a8d5"
},
{
"amount": "0.005555556",
"user": "0xf0d681c15447dCfaa6002f17078944FCe75B5e26",
"tranche_id": 5,
"tx": "0xca745232557385fff568386375d868633bd9542981dd07a6c471b4633dd5c9b8"
}
],
"total_tokens": "200",
"withdrawn_tokens": "0",
"remaining_tokens": "200"
"withdrawn_tokens": "189.702276762",
"remaining_tokens": "10.297723238"
},
{
"address": "0x7Cb540c27E6cAD1133a4942293B9293489A2Df19",
@@ -71002,10 +71107,17 @@
"tx": "0x1c2c084e3efafb2ffa09c0b259bd229cce0d6da04dd092d911c7e6136a7ac8a9"
}
],
"withdrawals": [],
"withdrawals": [
{
"amount": "189.69504693",
"user": "0xC198534Ee3D619770C21FFAF5e9F067F8bbC07FD",
"tranche_id": 5,
"tx": "0x803ba71987573a0ab20d4da7eb1dc7fe5e2fd8184199f0ee5d7a949b3d9f4e3e"
}
],
"total_tokens": "200",
"withdrawn_tokens": "0",
"remaining_tokens": "200"
"withdrawn_tokens": "189.69504693",
"remaining_tokens": "10.30495307"
},
{
"address": "0xFFDAE9319517529E0081710bF5A018E06C841eFf",
@@ -88740,6 +88852,12 @@
}
],
"withdrawals": [
{
"amount": "84.673820396",
"user": "0xB3ae40b11af91BbD456022Bd42B4f36A8C0AEA9D",
"tranche_id": 5,
"tx": "0x26941fa486bf2aa16da2748a2326f87c3491c4f6f939f4329bd1eb3171ce6c3d"
},
{
"amount": "191.520738204",
"user": "0xB3ae40b11af91BbD456022Bd42B4f36A8C0AEA9D",
@@ -88754,8 +88872,8 @@
}
],
"total_tokens": "400",
"withdrawn_tokens": "293.716882292",
"remaining_tokens": "106.283117708"
"withdrawn_tokens": "378.390702688",
"remaining_tokens": "21.609297312"
},
{
"address": "0x256274a216e0a349dC18cae08Ca45631176AF437",
@@ -88768,6 +88886,12 @@
}
],
"withdrawals": [
{
"amount": "84.67351598",
"user": "0x256274a216e0a349dC18cae08Ca45631176AF437",
"tranche_id": 5,
"tx": "0x891254ca1ea63a87ea7e14c3a985f58a403f4a0baf9fba86582785da6d2ae806"
},
{
"amount": "191.524581432",
"user": "0x256274a216e0a349dC18cae08Ca45631176AF437",
@@ -88782,8 +88906,8 @@
}
],
"total_tokens": "400",
"withdrawn_tokens": "293.715512428",
"remaining_tokens": "106.284487572"
"withdrawn_tokens": "378.389028408",
"remaining_tokens": "21.610971592"
},
{
"address": "0xB0473C4FeBd722a6d7eE561329d3134030318132",
+1 -1
View File
@@ -20,7 +20,7 @@ module.exports = defineConfig({
chromeWebSecurity: false,
projectId: 'et4snf',
defaultCommandTimeout: 10000,
viewportWidth: 1440,
viewportWidth: 1800,
viewportHeight: 900,
responseTimeout: 50000,
requestTimeout: 20000,
@@ -7,6 +7,9 @@ const marketTitle = 'accordion-title';
const externalLink = 'external-link';
const accordionContent = 'accordion-content';
const providerName = 'provider-name';
const oracleBannerStatus = 'oracle-banner-status';
const oracleBannerDialogTrigger = 'oracle-banner-dialog-trigger';
const oracleFullProfile = 'oracle-full-profile';
describe('market info is displayed', { tags: '@smoke' }, () => {
beforeEach(() => {
@@ -29,7 +32,11 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
it('show oracle banner', () => {
cy.getByTestId(marketTitle).contains('Oracle').click();
cy.getByTestId('oracle-status').should('contain.text', 'COMPROMISED');
cy.getByTestId(oracleBannerStatus).should('contain.text', 'COMPROMISED');
cy.getByTestId(oracleBannerDialogTrigger)
.should('contain.text', 'Show more')
.click();
cy.getByTestId(oracleFullProfile).should('exist');
});
it('current fees displayed', () => {
@@ -192,7 +199,6 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
.within(() => {
cy.getByTestId('block-explorer-link').contains('Block explorer');
cy.getByTestId('github-link').contains('Oracle repository');
cy.getByTestId('verified-accounts').contains('0 proofs of ownership');
});
cy.getByTestId('dialog-close').click();
@@ -0,0 +1,105 @@
import {
AuctionTrigger,
MarketState,
MarketTradingMode,
} from '@vegaprotocol/types';
describe('markets selector', { tags: '@smoke' }, () => {
const list = 'market-selector-list';
const searchInput = 'search-term';
beforeEach(() => {
cy.window().then((window) => {
window.localStorage.setItem('marketId', 'market-1');
});
cy.mockTradingPage(
MarketState.STATE_ACTIVE,
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.mockSubscription();
cy.visit('/');
cy.wait('@Markets');
cy.wait('@MarketsData');
cy.wait('@MarketsCandles');
});
it('can toggle the sidebar', () => {
cy.getByTestId('market-selector').should('be.visible');
cy.getByTestId('sidebar-toggle').click();
cy.getByTestId('market-selector').should('not.exist');
cy.getByTestId('sidebar-toggle').click();
cy.getByTestId('market-selector').should('be.visible');
});
// need function keyword as we need 'this' to access market data
it('displays data as expected', () => {
// TODO: load data from mocks in. Using alias and wrap intermittently fails
const data = [
{
code: 'AAPL.MF21',
name: 'Apple Monthly (30 Jun 2022)',
markPrice: '46,126.90058',
change: '+200.00%',
},
{
code: 'BTCUSD.MF21',
name: 'ACTIVE MARKET',
markPrice: '46,126.90058',
change: '+200.00%',
},
{
code: 'ETHBTC.QM21',
name: 'ETHBTC Quarterly (30 Jun 2022)',
markPrice: '46,126.90058',
change: '+200.00%',
},
{
code: 'SOLUSD',
name: 'SUSPENDED MARKET',
markPrice: '84.41',
change: '+200.00%',
},
];
cy.getByTestId(list)
.find('a')
.each((item, i) => {
const market = data[i];
// 6001-MARK-021
expect(item.find('h3').text()).equals(market.code);
// 6001-MARK-022
expect(item.find('h4').text()).equals(market.name);
// 6001-MARK-024
expect(item.find('[data-testid="market-item-price"]').text()).equals(
market.markPrice
);
// 6001-MARK-023
expect(item.find('[data-testid="market-item-change"]').text()).equals(
market.change
);
// 6001-MARK-025
expect(item.find('[data-testid="sparkline-svg"]')).to.exist;
});
});
// 6001-MARK-27
it('can use the filter options', () => {
// product type
cy.getByTestId('product-Spot').click();
cy.getByTestId(list).contains('Spot markets coming soon.');
cy.getByTestId('product-Perpetual').click();
cy.getByTestId(list).contains('Perpetual markets coming soon.');
cy.getByTestId('product-Future').click();
cy.getByTestId(list).find('a').should('have.length', 4);
// 6001-MARK-29
cy.getByTestId(searchInput).clear().type('btc');
cy.getByTestId(list).find('a').should('have.length', 2);
cy.getByTestId(list).find('a').eq(0).contains('BTCUSD.MF21');
cy.getByTestId(list).find('a').eq(1).contains('ETHBTC.QM21');
cy.getByTestId(searchInput).clear();
cy.getByTestId(list).find('a').should('have.length', 4);
});
});
@@ -31,7 +31,7 @@ describe('Market trading page', () => {
// 7002-SORD-001
// 7002-SORD-002
it('must display market name', () => {
cy.getByTestId('popover-trigger').should('not.be.empty');
cy.getByTestId('header-title').should('not.be.empty');
});
it('must see market expiry', () => {
@@ -4,7 +4,6 @@ import { marketsQuery } from '@vegaprotocol/mock';
import { getDateTimeFormat } from '@vegaprotocol/utils';
const dialogCloseBtn = 'dialog-close';
const popoverTrigger = 'popover-trigger';
describe('markets table', { tags: '@smoke' }, () => {
beforeEach(() => {
@@ -31,36 +30,6 @@ describe('markets table', { tags: '@smoke' }, () => {
cy.getByTestId('sparkline-svg').should('be.visible');
});
it('renders market list drop down', () => {
openMarketDropDown();
cy.getByTestId('price').invoke('text').should('not.be.empty');
cy.getByTestId('trading-mode-col').should('not.be.empty');
cy.getByTestId('taker-fee').should('contain.text', '%');
cy.getByTestId('market-volume').should('not.be.empty');
cy.getByTestId('market-name').should('not.be.empty');
cy.getByTestId('trading-mode-col')
.contains('Monitoring auction - liquidity')
.eq(0)
.realHover();
cy.get('[data-testid="trading-mode-tooltip"] p').should('have.class', '');
cy.get(
'[data-testid="market-trading-mode"] [data-testid="item-value"]'
).realHover();
cy.get('[data-testid="trading-mode-tooltip"] p').should(
'have.class',
'mb-4'
);
});
it('able to select market from dropdown', () => {
openMarketDropDown();
cy.getByTestId('market-link-market-0').first().should('be.visible').click();
cy.contains('ACTIVE MARKET').should('be.visible');
cy.url().should('include', '/markets/market-0');
cy.getByTestId('popover-trigger').should('not.be.empty');
});
it('able to open and sort full market list - market page', () => {
const ExpectedSortedMarkets = [
'AAPL.MF21',
@@ -253,15 +222,3 @@ describe('markets table', { tags: '@smoke' }, () => {
});
});
});
function openMarketDropDown() {
cy.getByTestId(dialogCloseBtn).then((button) => {
if (button.is(':visible')) {
cy.get('[data-testid^="market-link-"]').should('not.be.empty');
cy.getByTestId(dialogCloseBtn).click();
}
cy.get('[data-testid^="ask-vol-"]').should('be.visible');
cy.getByTestId(popoverTrigger).click({ force: true });
cy.contains('Loading market data...').should('not.exist');
});
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import * as Schema from '@vegaprotocol/types';
import type { CyHttpMessages } from 'cypress/types/net-stubbing';
import type { Provider, Status } from '@vegaprotocol/oracles';
import type { Provider, Status } from '@vegaprotocol/market-info';
import {
accountsQuery,
assetQuery,
+6
View File
@@ -0,0 +1,6 @@
function ReactMarkdown({ children }) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
}
export default ReactMarkdown;
@@ -8,7 +8,6 @@ import {
import { tooltipMapping } from '@vegaprotocol/market-info';
import {
addDecimalsFormatNumber,
createDocsLinks,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
@@ -39,7 +38,7 @@ import { Link, useParams } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import { useMarket, useStaticMarketData } from '@vegaprotocol/market-list';
import { useEnvironment } from '@vegaprotocol/environment';
import { DocsLinks } from '@vegaprotocol/environment';
const enum LiquidityTabs {
Active = 'active',
@@ -147,7 +146,6 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
market?.tradableInstrument.instrument.product.settlementAsset.decimals || 0;
const symbol =
market?.tradableInstrument.instrument.product.settlementAsset.symbol;
const { VEGA_DOCS_URL } = useEnvironment();
const { params } = useNetworkParams([
NetworkParams.market_liquidity_stakeToCcyVolume,
@@ -216,10 +214,12 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
<div className="break-word">{marketId}</div>
</HeaderStat>
<HeaderStat heading={t('Learn more')}>
{VEGA_DOCS_URL && (
<ExternalLink href={createDocsLinks(VEGA_DOCS_URL).LIQUIDITY}>
{DocsLinks ? (
<ExternalLink href={DocsLinks.LIQUIDITY}>
{t('Providing liquidity')}
</ExternalLink>
) : (
(null as React.ReactNode)
)}
</HeaderStat>
</Header>
@@ -0,0 +1,83 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AssetDropdown } from './asset-dropdown';
const createAssets = (count = 3) => {
return new Array(count).fill(null).map((_, i) => ({
id: i.toString(),
symbol: 'asset-1',
}));
};
describe('AssetDropdown', () => {
const assets = createAssets();
it('renders and selects chosen assets', async () => {
const mockOnSelect = jest.fn();
render(
<AssetDropdown
checkedAssets={[]}
assets={assets}
onSelect={mockOnSelect}
onReset={jest.fn()}
/>
);
await userEvent.click(screen.getByRole('button'));
const items = screen.getAllByRole('menuitemcheckbox');
expect(items).toHaveLength(assets.length);
expect(items.map((i) => i.textContent)).toEqual(
assets.map((a) => a.symbol)
);
await userEvent.click(items[0]);
expect(mockOnSelect).toHaveBeenCalledWith(assets[0].id, true);
});
it('unselects already selected assets', async () => {
const mockOnSelect = jest.fn();
render(
<AssetDropdown
checkedAssets={[assets[0].id]}
assets={assets}
onSelect={mockOnSelect}
onReset={jest.fn()}
/>
);
await userEvent.click(screen.getByRole('button'));
const items = screen.getAllByRole('menuitemcheckbox');
await userEvent.click(items[0]);
expect(mockOnSelect).toHaveBeenCalledWith(assets[0].id, false);
});
it('can be reset clearing all assets', async () => {
const mockOnSelect = jest.fn();
const mockOnReset = jest.fn();
render(
<AssetDropdown
checkedAssets={assets.map((a) => a.id)}
assets={assets}
onSelect={mockOnSelect}
onReset={mockOnReset}
/>
);
await userEvent.click(screen.getByRole('button'));
const items = screen.getAllByRole('menuitemcheckbox');
// all should be checked
items.forEach((item) => {
expect(item).toBeChecked();
});
await userEvent.click(screen.getByText('Reset'));
expect(mockOnReset).toHaveBeenCalled();
});
it('doesnt render if no assets provided', async () => {
const { container } = render(
<AssetDropdown
checkedAssets={[]}
assets={[]}
onSelect={jest.fn()}
onReset={jest.fn()}
/>
);
expect(container).toBeEmptyDOMElement();
});
});
@@ -0,0 +1,58 @@
import { t } from '@vegaprotocol/i18n';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuItemIndicator,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from '@vegaprotocol/ui-toolkit';
export const AssetDropdown = ({
assets,
checkedAssets,
onSelect,
onReset,
}: {
assets: Array<{ id: string; symbol: string }> | undefined;
checkedAssets: string[];
onSelect: (id: string, checked: boolean) => void;
onReset: () => void;
}) => {
if (!assets?.length) {
return null;
}
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger data-testid="asset-trigger">
<span className="px-1">$</span>
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
<DropdownMenuItem onClick={onReset}>{t('Reset')}</DropdownMenuItem>
<DropdownMenuSeparator />
{assets?.map((a) => {
return (
<DropdownMenuCheckboxItem
key={a.id}
checked={checkedAssets.includes(a.id)}
onCheckedChange={(checked) => {
if (typeof checked === 'boolean') {
onSelect(a.id, checked);
}
}}
data-testid={`asset-id-${a.id}`}
>
{a.symbol}
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -5,13 +5,7 @@ import { MarketProposalNotification } from '@vegaprotocol/proposals';
import type { Market } from '@vegaprotocol/market-list';
import { getExpiryDate, getMarketExpiryDate } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
ColumnKind,
SelectMarketPopover,
} from '../../components/select-market';
import type { OnCellClickHandler } from '../../components/select-market';
import { Header, HeaderStat } from '../../components/header';
import { NO_MARKET } from './constants';
import { HeaderStat } from '../../components/header';
import { MarketMarkPrice } from '../../components/market-mark-price';
import { Last24hPriceChange, Last24hVolume } from '@vegaprotocol/market-info';
import { MarketState } from '../../components/market-state';
@@ -19,102 +13,90 @@ import { HeaderStatMarketTradingMode } from '../../components/market-trading-mod
import { MarketLiquiditySupplied } from '../../components/liquidity-supplied';
import { MarketState as State } from '@vegaprotocol/types';
interface TradeMarketHeaderProps {
interface HeaderStatsProps {
market: Market | null;
onSelect: (marketId: string, metaKey?: boolean) => void;
}
export const TradeMarketHeader = ({
market,
onSelect,
}: TradeMarketHeaderProps) => {
export const HeaderStats = ({ market }: HeaderStatsProps) => {
const { VEGA_EXPLORER_URL } = useEnvironment();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const asset = market?.tradableInstrument.instrument.product?.settlementAsset;
const onCellClick: OnCellClickHandler = (e, kind, value) => {
if (value && kind === ColumnKind.Asset) {
openAssetDetailsDialog(value, e.target as HTMLElement);
}
};
return (
<Header
title={
<SelectMarketPopover
marketCode={market?.tradableInstrument.instrument.code || NO_MARKET}
marketName={market?.tradableInstrument.instrument.name || NO_MARKET}
onSelect={onSelect}
onCellClick={onCellClick}
/>
}
>
<HeaderStat
heading={t('Expiry')}
description={
market && (
<ExpiryTooltipContent
market={market}
explorerUrl={VEGA_EXPLORER_URL}
/>
)
}
testId="market-expiry"
>
<ExpiryLabel market={market} />
</HeaderStat>
<HeaderStat heading={t('Price')} testId="market-price">
<MarketMarkPrice
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat heading={t('Change (24h)')} testId="market-change">
<Last24hPriceChange
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat
heading={t('Volume (24h)')}
testId="market-volume"
description={t(
'The total number of contracts traded in the last 24 hours.'
)}
>
<Last24hVolume
marketId={market?.id}
positionDecimalPlaces={market?.positionDecimalPlaces}
/>
</HeaderStat>
<HeaderStatMarketTradingMode
marketId={market?.id}
initialTradingMode={market?.tradingMode}
/>
<MarketState market={market} />
{asset ? (
<HeaderStat
heading={t('Settlement asset')}
testId="market-settlement-asset"
<div className="flex flex-col justify-end lg:pt-4">
<div className="xl:flex xl:gap-4 items-end">
<div
data-testid="header-summary"
className="flex flex-nowrap items-end xl:flex-1 w-full overflow-x-auto text-xs"
>
<div>
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(asset.id, e.target as HTMLElement);
}}
<HeaderStat
heading={t('Expiry')}
description={
market && (
<ExpiryTooltipContent
market={market}
explorerUrl={VEGA_EXPLORER_URL}
/>
)
}
testId="market-expiry"
>
<ExpiryLabel market={market} />
</HeaderStat>
<HeaderStat heading={t('Price')} testId="market-price">
<MarketMarkPrice
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat heading={t('Change (24h)')} testId="market-change">
<Last24hPriceChange
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat
heading={t('Volume (24h)')}
testId="market-volume"
description={t(
'The total number of contracts traded in the last 24 hours.'
)}
>
<Last24hVolume
marketId={market?.id}
positionDecimalPlaces={market?.positionDecimalPlaces}
/>
</HeaderStat>
<HeaderStatMarketTradingMode
marketId={market?.id}
initialTradingMode={market?.tradingMode}
/>
<MarketState market={market} />
{asset ? (
<HeaderStat
heading={t('Settlement asset')}
testId="market-settlement-asset"
>
{asset.symbol}
</ButtonLink>
</div>
</HeaderStat>
) : null}
<MarketLiquiditySupplied
marketId={market?.id}
assetDecimals={asset?.decimals || 0}
/>
<MarketProposalNotification marketId={market?.id} />
</Header>
<div>
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(asset.id, e.target as HTMLElement);
}}
>
{asset.symbol}
</ButtonLink>
</div>
</HeaderStat>
) : null}
<MarketLiquiditySupplied
marketId={market?.id}
assetDecimals={asset?.decimals || 0}
/>
<MarketProposalNotification marketId={market?.id} />
</div>
</div>
</div>
);
};
@@ -0,0 +1,111 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createMarketFragment } from '@vegaprotocol/mock';
import { MarketSelectorItem } from './market-selector-item';
import { MemoryRouter } from 'react-router-dom';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type {
MarketDataUpdateFieldsFragment,
MarketDataUpdateSubscription,
} from '@vegaprotocol/market-list';
import { MarketDataUpdateDocument } from '@vegaprotocol/market-list';
import {
AuctionTrigger,
MarketState,
MarketTradingMode,
} from '@vegaprotocol/types';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
describe('MarketSelectorItem', () => {
const market = createMarketFragment({
id: 'market-0',
decimalPlaces: 2,
// @ts-ignore fragment doesn't contain candles
candles: [{ close: '5' }, { close: '10' }],
});
const marketData: MarketDataUpdateFieldsFragment = {
__typename: 'ObservableMarketData',
marketId: market.id,
auctionEnd: null,
auctionStart: null,
bestBidPrice: '100',
bestBidVolume: '100',
bestOfferPrice: '100',
bestOfferVolume: '100',
bestStaticBidPrice: '100',
bestStaticBidVolume: '100',
bestStaticOfferPrice: '100',
bestStaticOfferVolume: '100',
indicativePrice: '100',
indicativeVolume: '100',
marketState: MarketState.STATE_ACTIVE,
marketTradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
marketValueProxy: '100',
markPrice: '50000',
midPrice: '100',
staticMidPrice: '100',
openInterest: '100',
suppliedStake: '1000000',
targetStake: '1000000',
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
priceMonitoringBounds: null,
};
const mock: MockedResponse<MarketDataUpdateSubscription> = {
request: {
query: MarketDataUpdateDocument,
variables: {
marketId: market.id,
},
},
result: {
data: {
marketsData: [marketData],
},
},
};
const mockOnSelect = jest.fn();
const renderJsx = () => {
return render(
<MemoryRouter>
<MockedProvider mocks={[mock]}>
<MarketSelectorItem
market={market}
currentMarketId={market.id}
style={{}}
onSelect={mockOnSelect}
/>
</MockedProvider>
</MemoryRouter>
);
};
it('renders market information', async () => {
renderJsx();
const link = screen.getByRole('link');
// link renders and is styled
expect(link).toHaveAttribute('href', '/markets/' + market.id);
expect(link).toHaveClass('ring-1');
expect(screen.getByTestId('market-item-price')).toHaveTextContent('-');
// candles are loaded immediately
expect(screen.getByTestId('market-item-change')).toHaveTextContent(
'+100.00%'
);
await waitFor(() => {
expect(screen.getByTestId('market-item-price')).toHaveTextContent(
addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
);
});
await userEvent.click(link);
expect(mockOnSelect).toHaveBeenCalledWith(market.id);
});
});
@@ -0,0 +1,115 @@
import type { CSSProperties } from 'react';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import {
addDecimalsFormatNumber,
formatNumber,
priceChangePercentage,
} from '@vegaprotocol/utils';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/market-list';
import { useMarketDataUpdateSubscription } from '@vegaprotocol/market-list';
import { Sparkline } from '@vegaprotocol/ui-toolkit';
export const MarketSelectorItem = ({
market,
style,
currentMarketId,
onSelect,
}: {
market: MarketMaybeWithDataAndCandles;
style: CSSProperties;
currentMarketId?: string;
onSelect?: (marketId: string) => void;
}) => {
const wrapperClasses = classNames(
'block bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg p-4',
'min-h-[120px]',
{
'ring-1 ring-vega-light-300 dark:ring-vega-dark-300':
currentMarketId === market.id,
}
);
return (
<div style={style} className="my-0.5 px-4">
<Link
to={`/markets/${market.id}`}
className={wrapperClasses}
onClick={() => {
onSelect && onSelect(market.id);
}}
>
<h3>{market.tradableInstrument.instrument.code}</h3>
<h4
title={market.tradableInstrument.instrument.name}
className="text-sm text-vega-light-300 dark:text-vega-dark-300 text-ellipsis whitespace-nowrap overflow-hidden"
>
{market.tradableInstrument.instrument.name}
</h4>
<MarketData market={market} />
</Link>
</div>
);
};
const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
const { data } = useMarketDataUpdateSubscription({
variables: {
marketId: market.id,
},
});
const marketData = data?.marketsData[0];
// use market data price if available as this is comes from
// the subscription
const price = marketData
? addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
: market.data
? addDecimalsFormatNumber(market.data.markPrice, market.decimalPlaces)
: '-';
return (
<div className="flex flex-nowrap justify-between items-center mt-1">
<div className="w-1/2">
<div
className="text-ellipsis whitespace-nowrap overflow-hidden"
data-testid="market-item-price"
>
{price}
</div>
{market.candles && (
<PriceChange candles={market.candles.map((c) => c.close)} />
)}
</div>
<div className="w-1/2 max-w-[120px]">
{market.candles ? (
<Sparkline
width={120}
height={20}
data={market.candles.filter(Boolean).map((c) => Number(c.close))}
/>
) : (
'-'
)}
</div>
</div>
);
};
const PriceChange = ({ candles }: { candles: string[] }) => {
const priceChange = candles ? priceChangePercentage(candles) : undefined;
const priceChangeClasses = classNames('text-xs', {
'text-vega-pink': priceChange && priceChange < 0,
'text-vega-green': priceChange && priceChange > 0,
});
let prefix = '';
if (priceChange && priceChange > 0) {
prefix = '+';
}
const formattedChange = formatNumber(Number(priceChange), 2);
return (
<div className={priceChangeClasses} data-testid="market-item-change">
{priceChange ? `${prefix}${formattedChange}%` : '-'}
</div>
);
};
@@ -0,0 +1,289 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MarketSelector } from './market-selector';
import { useMarketList } from '@vegaprotocol/market-list';
import { createMarketFragment } from '@vegaprotocol/mock';
import { MarketState } from '@vegaprotocol/types';
import { MemoryRouter } from 'react-router-dom';
import type { ReactNode } from 'react';
import type { SortType } from './sort-dropdown';
import { SortTypeMapping } from './sort-dropdown';
import { Sort } from './sort-dropdown';
import { subDays } from 'date-fns';
jest.mock('@vegaprotocol/market-list');
const mockUseMarketList = useMarketList as jest.Mock;
// mock market list items to avoid subscriptions starting
jest.mock('./market-selector-item', () => ({
MarketSelectorItem: (props: { market: { id: string } }) => (
<div data-testid={props.market.id} />
),
}));
// without a real DOM autosize won't render with an actual height or width
jest.mock('react-virtualized-auto-sizer', () => {
// eslint-disable-next-line react/display-name
return ({
children,
}: {
children: (size: { width: number; height: number }) => ReactNode;
}) => <div>{children({ width: 300, height: 1000 })}</div>;
});
describe('MarketSelector', () => {
const markets = [
createMarketFragment({
id: 'market-0',
tradableInstrument: {
instrument: {
code: 'a',
name: 'a',
product: {
settlementAsset: {
id: 'asset-0',
},
},
},
},
// @ts-ignore candles get joined outside this type
candles: [{ close: '100' }, { close: '200' }],
marketTimestamps: {
open: subDays(new Date(), 1).toISOString(),
},
}),
createMarketFragment({
id: 'market-1',
state: MarketState.STATE_SUSPENDED,
tradableInstrument: {
instrument: {
code: 'b',
name: 'b',
product: {
settlementAsset: {
id: 'asset-0',
},
},
},
},
// @ts-ignore candles get joined outside this type
candles: [{ close: '100' }, { close: '400' }],
marketTimestamps: {
open: subDays(new Date(), 2).toISOString(),
},
}),
createMarketFragment({
id: 'market-2',
state: MarketState.STATE_CLOSED,
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-1',
},
},
},
},
}),
createMarketFragment({
id: 'market-3',
state: MarketState.STATE_ACTIVE,
tradableInstrument: {
instrument: {
code: 'c',
name: 'c',
product: {
settlementAsset: {
id: 'asset-1',
},
},
},
},
// @ts-ignore candles get joined outside this type
candles: [{ close: '100' }, { close: '1000' }],
marketTimestamps: {
open: subDays(new Date(), 0).toISOString(),
},
}),
createMarketFragment({
id: 'market-4',
tradableInstrument: {
instrument: {
code: 'cd',
name: 'cd',
product: {
settlementAsset: {
id: 'asset-2',
},
},
},
},
// @ts-ignore candles get joined outside this type
candles: [{ close: '100' }, { close: '300' }],
marketTimestamps: {
open: subDays(new Date(), 3).toISOString(),
},
}),
];
const activeMarkets = markets.filter((m) =>
[MarketState.STATE_ACTIVE, MarketState.STATE_SUSPENDED].includes(m.state)
);
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
it('renders only active markets', () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(
activeMarkets.length
);
expect(screen.getByRole('link')).toHaveTextContent('All markets');
});
it('filters by product type', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.click(screen.getByTestId('product-Spot'));
expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(0);
expect(screen.getByTestId('no-items')).toHaveTextContent(
'Spot markets coming soon.'
);
await userEvent.click(screen.getByTestId('product-Perpetual'));
expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(0);
expect(screen.getByTestId('no-items')).toHaveTextContent(
'Perpetual markets coming soon.'
);
await userEvent.click(screen.getByTestId('product-Future'));
expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(
activeMarkets.length
);
expect(screen.queryByTestId('no-items')).not.toBeInTheDocument();
});
it('filters by search term', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.type(screen.getByTestId('search-term'), 'zzz');
expect(screen.getByTestId('no-items')).toHaveTextContent('No markets');
const input = screen.getByTestId('search-term');
await userEvent.clear(input);
await userEvent.type(input, 'a');
expect(input).toHaveValue('a');
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(1);
expect(screen.getByTestId('market-0')).toBeInTheDocument();
await userEvent.clear(input);
await userEvent.type(input, 'b');
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(1);
expect(screen.getByTestId('market-1')).toBeInTheDocument();
await userEvent.clear(input);
await userEvent.type(input, 'c');
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(2);
expect(screen.getByTestId('market-3')).toBeInTheDocument();
expect(screen.getByTestId('market-4')).toBeInTheDocument();
});
it('filters by asset', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.click(screen.getByTestId('asset-trigger'));
expect(screen.getAllByTestId(/asset-id/)).toHaveLength(3);
await userEvent.click(screen.getByTestId('asset-id-asset-0'));
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(2);
expect(screen.getByTestId('market-0')).toBeInTheDocument();
expect(screen.getByTestId('market-1')).toBeInTheDocument();
// reopen asset dropdown and add asset-1
await userEvent.click(screen.getByTestId('asset-trigger'));
await userEvent.click(screen.getByTestId('asset-id-asset-1'));
// all markets with asset-0 or asset-1 shown (no market id as market is closed)
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(3);
expect(screen.getByTestId('market-0')).toBeInTheDocument();
expect(screen.getByTestId('market-1')).toBeInTheDocument();
expect(screen.getByTestId('market-3')).toBeInTheDocument();
// reopen and uncheck asset-0
await userEvent.click(screen.getByTestId('asset-trigger'));
await userEvent.click(screen.getByTestId('asset-id-asset-0'));
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(1);
expect(screen.getByTestId('market-3')).toBeInTheDocument();
});
it('sorts by gained', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.click(screen.getByTestId('sort-trigger'));
const options = screen.getAllByTestId(/sort-item/);
expect(options.map((o) => o.textContent)).toEqual(
Object.entries(Sort)
.filter(([key]) => key !== Sort.None)
.map(([key]) => SortTypeMapping[key as SortType])
);
await userEvent.click(screen.getByTestId('sort-item-Gained'));
expect(
screen
.getAllByTestId(/market-\d/)
.map((el) => el.getAttribute('data-testid'))
).toEqual([markets[3].id, markets[1].id, markets[4].id, markets[0].id]);
});
it('sorts by lost', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.click(screen.getByTestId('sort-trigger'));
await userEvent.click(screen.getByTestId('sort-item-Lost'));
expect(
screen
.getAllByTestId(/market-\d/)
.map((el) => el.getAttribute('data-testid'))
).toEqual([markets[0].id, markets[4].id, markets[1].id, markets[3].id]);
});
it('sorts by new', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.click(screen.getByTestId('sort-trigger'));
await userEvent.click(screen.getByTestId('sort-item-New'));
expect(
screen
.getAllByTestId(/market-\d/)
.map((el) => el.getAttribute('data-testid'))
).toEqual([markets[3].id, markets[0].id, markets[1].id, markets[4].id]);
});
});
@@ -0,0 +1,276 @@
import { t } from '@vegaprotocol/i18n';
import uniqBy from 'lodash/uniqBy';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/market-list';
import {
Input,
TinyScroll,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { CSSProperties } from 'react';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useMarketSelectorList } from './use-market-selector-list';
import type { ProductType } from './product-selector';
import { Product, ProductSelector } from './product-selector';
import { AssetDropdown } from './asset-dropdown';
import type { SortType } from './sort-dropdown';
import { Sort, SortDropdown } from './sort-dropdown';
import { MarketSelectorItem } from './market-selector-item';
export type Filter = {
searchTerm: string;
product: ProductType;
sort: SortType;
assets: string[];
};
/**
* Fetches market data and filters it given a set of filter properties
* defined in Filter
*/
export const MarketSelector = ({
currentMarketId,
onSelect,
}: {
currentMarketId?: string;
onSelect?: (marketId: string) => void;
}) => {
const [filter, setFilter] = useState<Filter>({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: [],
});
const { markets, data, loading, error } = useMarketSelectorList(filter);
return (
<div
className="grid grid-rows-[min-content_1fr_min-content] h-full"
data-testid="market-selector"
>
<div className="px-4 pt-2 pb-4">
<ProductSelector
product={filter.product}
onSelect={(product) => {
setFilter((curr) => ({ ...curr, product }));
}}
/>
<div className="text-sm flex gap-1 items-stretch">
<div className="flex-1">
<Input
onChange={(e) =>
setFilter((curr) => ({ ...curr, searchTerm: e.target.value }))
}
value={filter.searchTerm}
type="text"
placeholder={t('Search')}
data-testid="search-term"
className="w-full"
appendElement={
filter.searchTerm.length ? (
<button
onClick={() =>
setFilter((curr) => ({ ...curr, searchTerm: '' }))
}
className="text-vega-light-200 dark:text-vega-dark-200"
>
<VegaIcon name={VegaIconNames.CROSS} />
</button>
) : (
<span />
)
}
/>
</div>
<AssetDropdown
assets={uniqBy(
data?.map(
(d) => d.tradableInstrument.instrument.product.settlementAsset
),
'id'
)}
checkedAssets={filter.assets}
onSelect={(id: string, checked) => {
setFilter((curr) => {
if (checked) {
if (curr.assets.includes(id)) {
return curr;
} else {
return { ...curr, assets: [...curr.assets, id] };
}
} else {
if (curr.assets.includes(id)) {
return {
...curr,
assets: curr.assets.filter((x) => x !== id),
};
}
}
return curr;
});
}}
onReset={() => setFilter((curr) => ({ ...curr, assets: [] }))}
/>
<SortDropdown
currentSort={filter.sort}
onSelect={(sort) => {
setFilter((curr) => {
if (curr.sort === sort) {
return { ...curr, sort: Sort.None };
}
return {
...curr,
sort,
};
});
}}
onReset={() => setFilter((curr) => ({ ...curr, sort: Sort.None }))}
/>
</div>
</div>
<div data-testid="market-selector-list">
<MarketList
data={markets}
loading={loading}
error={error}
searchTerm={filter.searchTerm}
currentMarketId={currentMarketId}
onSelect={onSelect}
noItems={
filter.product === Product.Perpetual
? t('Perpetual markets coming soon.')
: filter.product === Product.Spot
? t('Spot markets coming soon.')
: t('No markets')
}
/>
</div>
<div className="px-4 py-2">
<span className="inline-block border-b border-black dark:border-white">
<Link to={'/markets/all'} className="flex items-center gap-x-2">
{t('All markets')}
<VegaIcon name={VegaIconNames.ARROW_RIGHT} />
</Link>
</span>
</div>
</div>
);
};
const MarketList = ({
data,
error,
loading,
currentMarketId,
onSelect,
noItems,
}: {
data: MarketMaybeWithDataAndCandles[];
error: Error | undefined;
loading: boolean;
searchTerm: string;
currentMarketId?: string;
onSelect?: (marketId: string) => void;
noItems: string;
}) => {
if (error) {
return <div>{error.message}</div>;
}
return (
<AutoSizer>
{({ width, height }) => (
<TinyScroll>
<List
data={data}
loading={loading}
width={width}
height={height}
currentMarketId={currentMarketId}
onSelect={onSelect}
noItems={noItems}
/>
</TinyScroll>
)}
</AutoSizer>
);
};
const List = ({
data,
loading,
width,
height,
onSelect,
noItems,
currentMarketId,
}: {
data: MarketMaybeWithDataAndCandles[];
loading: boolean;
width: number;
height: number;
noItems: string;
onSelect?: (marketId: string) => void;
currentMarketId?: string;
}) => {
const row = ({ index, style }: { index: number; style: CSSProperties }) => {
const market = data[index];
return (
<MarketSelectorItem
market={market}
currentMarketId={currentMarketId}
style={style}
onSelect={onSelect}
/>
);
};
if (!data || loading) {
return (
<div style={{ width, height }}>
<Skeleton />
<Skeleton />
</div>
);
}
if (!data.length) {
return (
<div style={{ width, height }} data-testid="no-items">
<div className="mb-2 px-4">
<div className="text-sm bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg px-4 py-2">
{noItems}
</div>
</div>
</div>
);
}
return (
<FixedSizeList
className="virtualized-list"
itemCount={data.length}
itemSize={130}
width={width}
height={height}
>
{row}
</FixedSizeList>
);
};
const Skeleton = () => {
return (
<div className="mb-2 px-2">
<div className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg p-4">
<div className="w-full h-3 bg-vega-light-200 dark:bg-vega-dark-200 mb-2" />
<div className="w-2/3 h-3 bg-vega-light-200 dark:bg-vega-dark-200" />
</div>
</div>
);
};
+3 -1
View File
@@ -9,7 +9,8 @@ import {
import { AsyncRenderer, ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
import { marketProvider, marketDataProvider } from '@vegaprotocol/market-list';
import { useGlobalStore, usePageTitleStore } from '../../stores';
import { TradeGrid, TradePanels } from './trade-grid';
import { TradeGrid } from './trade-grid';
import { TradePanels } from './trade-panels';
import { useNavigate, useParams } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
@@ -100,6 +101,7 @@ export const MarketPage = () => {
/>
);
}, [largeScreen, data, onSelect, navigate]);
if (!data && marketId) {
return (
<Splash>
@@ -0,0 +1,50 @@
import classNames from 'classnames';
// Make sure these match the available __typename properties on product
export const Product = {
Spot: 'Spot',
Future: 'Future',
Perpetual: 'Perpetual',
} as const;
export type ProductType = keyof typeof Product;
const ProductTypeMapping: {
[key in ProductType]: string;
} = {
[Product.Spot]: 'Spot',
[Product.Future]: 'Futures',
[Product.Perpetual]: 'Perpetuals',
};
export const ProductSelector = ({
product,
onSelect,
}: {
product: ProductType;
onSelect: (product: ProductType) => void;
}) => {
return (
<div className="flex gap-3 mb-3">
{Object.keys(Product).map((t) => {
const classes = classNames('py-1 border-b-2', {
'border-vega-yellow text-black dark:text-white': t === product,
'border-transparent text-vega-light-300 dark:text-vega-dark-300':
t !== product,
});
return (
<button
key={t}
onClick={() => {
onSelect(t as ProductType);
}}
className={classes}
data-testid={`product-${t}`}
>
{ProductTypeMapping[t as ProductType]}
</button>
);
})}
</div>
);
};
@@ -0,0 +1,76 @@
import { t } from '@vegaprotocol/i18n';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuItemIndicator,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
export const Sort = {
None: 'None',
Gained: 'Gained',
Lost: 'Lost',
New: 'New',
} as const;
export type SortType = keyof typeof Sort;
export const SortTypeMapping: {
[key in SortType]: string;
} = {
[Sort.None]: 'None',
[Sort.Gained]: 'Top gaining',
[Sort.Lost]: 'Top losing',
[Sort.New]: 'New markets',
};
export const SortDropdown = ({
currentSort,
onSelect,
onReset,
}: {
currentSort: SortType;
onSelect: (sort: SortType) => void;
onReset: () => void;
}) => {
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger data-testid="sort-trigger">
<VegaIcon name={VegaIconNames.TREND_UP} />
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
<DropdownMenuRadioGroup
value={currentSort}
onValueChange={(value) => onSelect(value as SortType)}
>
<DropdownMenuItem onClick={onReset}>{t('Reset')}</DropdownMenuItem>
<DropdownMenuSeparator />
{Object.keys(Sort)
.filter((s) => s !== Sort.None)
.map((key) => {
return (
<DropdownMenuRadioItem
inset
key={key}
value={key}
data-testid={`sort-item-${key}`}
>
{SortTypeMapping[key as SortType]}
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
);
})}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};
+66 -195
View File
@@ -1,120 +1,37 @@
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
import { MarketInfoAccordionContainer } from '@vegaprotocol/market-info';
import { OrderbookContainer } from '@vegaprotocol/market-depth';
import { OrderListContainer, Filter } from '@vegaprotocol/orders';
import type { OrderListContainerProps } from '@vegaprotocol/orders';
import { FillsContainer } from '@vegaprotocol/fills';
import { PositionsContainer } from '@vegaprotocol/positions';
import { TradesContainer } from '@vegaprotocol/trades';
import { memo, useState } from 'react';
import type { ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { LayoutPriority } from 'allotment';
import classNames from 'classnames';
import AutoSizer from 'react-virtualized-auto-sizer';
import { memo, useState } from 'react';
import type { ReactNode, ComponentProps } from 'react';
import { DepthChartContainer } from '@vegaprotocol/market-depth';
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
import { OracleBanner } from '../../components/banner';
import {
Tab,
LocalStoragePersistTabs as Tabs,
Splash,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { AccountsContainer } from '../../components/accounts-container';
import type { Market } from '@vegaprotocol/market-list';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { TradeMarketHeader } from './trade-market-header';
import { NO_MARKET } from './constants';
import { LiquidityContainer } from '../liquidity/liquidity';
import { useNavigate } from 'react-router-dom';
import type { PinnedAsset } from '@vegaprotocol/accounts';
import { t } from '@vegaprotocol/i18n';
import { OracleBanner } from '@vegaprotocol/market-info';
import type { Market } from '@vegaprotocol/market-list';
import { Filter } from '@vegaprotocol/orders';
import {
usePaneLayout,
useScreenDimensions,
} from '@vegaprotocol/react-helpers';
import {
Tab,
LocalStoragePersistTabs as Tabs,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import {
useMarketClickHandler,
useMarketLiquidityClickHandler,
} from '../../lib/hooks/use-market-click-handler';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { HeaderTitle } from '../../components/header';
import {
ResizableGrid,
ResizableGridPanel,
} from '../../components/resizable-grid';
type MarketDependantView =
| typeof CandlesChartContainer
| typeof DepthChartContainer
| typeof DealTicketContainer
| typeof MarketInfoAccordionContainer
| typeof OrderbookContainer
| typeof TradesContainer;
type MarketDependantViewProps = ComponentProps<MarketDependantView>;
const requiresMarket = (View: MarketDependantView) => {
const WrappedComponent = (props: MarketDependantViewProps) =>
props.marketId ? <View {...props} /> : <Splash>{NO_MARKET}</Splash>;
WrappedComponent.displayName = `RequiresMarket(${View.name})`;
return WrappedComponent;
};
const TradingViews = {
candles: {
label: 'Candles',
component: requiresMarket(CandlesChartContainer),
},
depth: {
label: 'Depth',
component: requiresMarket(DepthChartContainer),
},
liquidity: {
label: 'Liquidity',
component: requiresMarket(LiquidityContainer),
},
ticket: {
label: 'Ticket',
component: requiresMarket(DealTicketContainer),
},
info: {
label: 'Info',
component: requiresMarket(MarketInfoAccordionContainer),
},
orderbook: {
label: 'Orderbook',
component: requiresMarket(OrderbookContainer),
},
trades: {
label: 'Trades',
component: requiresMarket(TradesContainer),
},
positions: { label: 'Positions', component: PositionsContainer },
activeOrders: {
label: 'Active',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Open} />
),
},
closedOrders: {
label: 'Closed',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Closed} />
),
},
rejectedOrders: {
label: 'Rejected',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Rejected} />
),
},
orders: {
label: 'All',
component: OrderListContainer,
},
collateral: { label: 'Collateral', component: AccountsContainer },
fills: { label: 'Fills', component: FillsContainer },
};
type TradingView = keyof typeof TradingViews;
import { TradingViews } from './trade-views';
import { MarketSelector } from './market-selector';
import { HeaderStats } from './header-stats';
interface TradeGridProps {
market: Market | null;
@@ -410,18 +327,57 @@ const MainGrid = memo(
);
MainGrid.displayName = 'MainGrid';
export const TradeGrid = ({
market,
onSelect,
pinnedAsset,
}: TradeGridProps) => {
export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
const [sidebarOpen, setSidebarOpen] = useState(true);
const wrapperClasses = classNames(
'h-full grid',
'grid-rows-[min-content_min-content_1fr]',
'grid-cols-[320px_1fr]'
);
const paneWrapperClasses = classNames('min-h-0', {
'col-span-2 col-start-1': !sidebarOpen,
});
return (
<div className="h-full grid grid-rows-[min-content_1fr]">
<div>
<TradeMarketHeader market={market} onSelect={onSelect} />
<div className={wrapperClasses}>
<div className="border-b border-r border-default">
<div className="flex gap-2 justify-between items-center px-4 py-2">
<HeaderTitle
primaryContent={market?.tradableInstrument.instrument.code}
secondaryContent={market?.tradableInstrument.instrument.name}
/>
<button
onClick={() => setSidebarOpen((x) => !x)}
className="p-2"
data-testid="sidebar-toggle"
>
<span
className={classNames('block', {
'rotate-90 translate-x-1': !sidebarOpen,
'-rotate-90 -translate-x-1': sidebarOpen,
})}
>
<VegaIcon name={VegaIconNames.CHEVRON_UP} />
</span>
</button>
</div>
</div>
<div className="border-b border-default min-w-0">
<HeaderStats market={market} />
</div>
<div className="col-span-2 bg-vega-green">
<OracleBanner marketId={market?.id || ''} />
</div>
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
{sidebarOpen && (
<div className="border-r border-default min-h-0">
<div className="h-full pb-8">
<MarketSelector currentMarketId={market?.id} />
</div>
</div>
)}
<div className={paneWrapperClasses}>
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
</div>
</div>
);
};
@@ -439,88 +395,3 @@ const TradeGridChild = ({ children }: TradeGridChildProps) => {
</section>
);
};
interface TradePanelsProps {
market: Market | null;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}
export const TradePanels = ({
market,
onSelect,
onClickCollateral,
pinnedAsset,
}: TradePanelsProps) => {
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const [view, setView] = useState<TradingView>('candles');
const renderView = () => {
const Component = memo<{
marketId: string;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}>(TradingViews[view].component);
if (!Component) {
throw new Error(`No component for view: ${view}`);
}
if (!market) return <Splash>{NO_MARKET}</Splash>;
return (
<Component
marketId={market?.id}
onSelect={onSelect}
onClickCollateral={onClickCollateral}
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
/>
);
};
return (
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
<div>
<TradeMarketHeader market={market} onSelect={onSelect} />
<OracleBanner marketId={market?.id || ''} />
</div>
<div className="h-full">
<AutoSizer>
{({ width, height }) => (
<div style={{ width, height }} className="overflow-auto">
{renderView()}
</div>
)}
</AutoSizer>
</div>
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
{Object.keys(TradingViews).map((key) => {
const isActive = view === key;
const className = classNames('p-4 min-w-[100px] capitalize', {
'text-black dark:text-vega-yellow': isActive,
'bg-neutral-200 dark:bg-neutral-800': isActive,
});
return (
<button
data-testid={key}
onClick={() => setView(key as TradingView)}
className={className}
key={key}
>
{TradingViews[key as keyof typeof TradingViews].label}
</button>
);
})}
</div>
</div>
);
};
@@ -0,0 +1,146 @@
import type { PinnedAsset } from '@vegaprotocol/accounts';
import type { Market } from '@vegaprotocol/market-list';
import { OracleBanner } from '@vegaprotocol/market-info';
import {
useMarketClickHandler,
useMarketLiquidityClickHandler,
} from '../../lib/hooks/use-market-click-handler';
import type { TradingView } from './trade-views';
import { TradingViews } from './trade-views';
import { memo, useState } from 'react';
import {
Icon,
Splash,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { NO_MARKET } from './constants';
import AutoSizer from 'react-virtualized-auto-sizer';
import classNames from 'classnames';
import { HeaderStats } from './header-stats';
import * as DialogPrimitives from '@radix-ui/react-dialog';
import { HeaderTitle } from '../../components/header';
import { MarketSelector } from './market-selector';
interface TradePanelsProps {
market: Market | null;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}
export const TradePanels = ({
market,
onSelect,
onClickCollateral,
pinnedAsset,
}: TradePanelsProps) => {
const [drawerOpen, setDrawerOpen] = useState(false);
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const [view, setView] = useState<TradingView>('candles');
const renderView = () => {
const Component = memo<{
marketId: string;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}>(TradingViews[view].component);
if (!Component) {
throw new Error(`No component for view: ${view}`);
}
if (!market) return <Splash>{NO_MARKET}</Splash>;
return (
<Component
marketId={market?.id}
onSelect={onSelect}
onClickCollateral={onClickCollateral}
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
/>
);
};
return (
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
<div className="border-b border-default min-w-0">
<div className="flex gap-4 items-center px-4 py-2">
<HeaderTitle
primaryContent={market?.tradableInstrument.instrument.code}
secondaryContent={market?.tradableInstrument.instrument.name}
/>
<button onClick={() => setDrawerOpen((x) => !x)} className="p-2">
<span
className={classNames('block', {
'rotate-90 translate-x-1': !drawerOpen,
'-rotate-90 -translate-x-1': drawerOpen,
})}
>
<VegaIcon name={VegaIconNames.CHEVRON_UP} />
</span>
</button>
</div>
<HeaderStats market={market} />
</div>
<div>
<OracleBanner marketId={market?.id || ''} />
</div>
<div className="h-full">
<AutoSizer>
{({ width, height }) => (
<div style={{ width, height }} className="overflow-auto">
{renderView()}
</div>
)}
</AutoSizer>
</div>
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
{Object.keys(TradingViews).map((key) => {
const isActive = view === key;
const className = classNames('p-4 min-w-[100px] capitalize', {
'text-black dark:text-vega-yellow': isActive,
'bg-neutral-200 dark:bg-neutral-800': isActive,
});
return (
<button
data-testid={key}
onClick={() => setView(key as TradingView)}
className={className}
key={key}
>
{TradingViews[key as keyof typeof TradingViews].label}
</button>
);
})}
</div>
<DialogPrimitives.Root open={drawerOpen} onOpenChange={setDrawerOpen}>
<DialogPrimitives.Portal>
<DialogPrimitives.Overlay />
<DialogPrimitives.Content
className={classNames(
'fixed h-full max-w-[500px] w-[90vw] z-10 top-0 left-0 transition-transform',
'bg-white dark:bg-black',
'border-r border-default'
)}
>
<DialogPrimitives.Close className="absolute top-0 right-0 p-2">
<Icon name="cross" />
</DialogPrimitives.Close>
{drawerOpen && (
<MarketSelector onSelect={() => setDrawerOpen(false)} />
)}
</DialogPrimitives.Content>
</DialogPrimitives.Portal>
</DialogPrimitives.Root>
</div>
);
};
@@ -0,0 +1,90 @@
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
import { MarketInfoAccordionContainer } from '@vegaprotocol/market-info';
import { OrderbookContainer } from '@vegaprotocol/market-depth';
import { OrderListContainer, Filter } from '@vegaprotocol/orders';
import type { OrderListContainerProps } from '@vegaprotocol/orders';
import { FillsContainer } from '@vegaprotocol/fills';
import { PositionsContainer } from '@vegaprotocol/positions';
import { TradesContainer } from '@vegaprotocol/trades';
import type { ComponentProps } from 'react';
import { DepthChartContainer } from '@vegaprotocol/market-depth';
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { AccountsContainer } from '../../components/accounts-container';
import { NO_MARKET } from './constants';
import { LiquidityContainer } from '../liquidity/liquidity';
type MarketDependantView =
| typeof CandlesChartContainer
| typeof DepthChartContainer
| typeof DealTicketContainer
| typeof MarketInfoAccordionContainer
| typeof OrderbookContainer
| typeof TradesContainer;
type MarketDependantViewProps = ComponentProps<MarketDependantView>;
const requiresMarket = (View: MarketDependantView) => {
const WrappedComponent = (props: MarketDependantViewProps) =>
props.marketId ? <View {...props} /> : <Splash>{NO_MARKET}</Splash>;
WrappedComponent.displayName = `RequiresMarket(${View.name})`;
return WrappedComponent;
};
export type TradingView = keyof typeof TradingViews;
export const TradingViews = {
candles: {
label: 'Candles',
component: requiresMarket(CandlesChartContainer),
},
depth: {
label: 'Depth',
component: requiresMarket(DepthChartContainer),
},
liquidity: {
label: 'Liquidity',
component: requiresMarket(LiquidityContainer),
},
ticket: {
label: 'Ticket',
component: requiresMarket(DealTicketContainer),
},
info: {
label: 'Info',
component: requiresMarket(MarketInfoAccordionContainer),
},
orderbook: {
label: 'Orderbook',
component: requiresMarket(OrderbookContainer),
},
trades: {
label: 'Trades',
component: requiresMarket(TradesContainer),
},
positions: { label: 'Positions', component: PositionsContainer },
activeOrders: {
label: 'Active',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Open} />
),
},
closedOrders: {
label: 'Closed',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Closed} />
),
},
rejectedOrders: {
label: 'Rejected',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Rejected} />
),
},
orders: {
label: 'All',
component: OrderListContainer,
},
collateral: { label: 'Collateral', component: AccountsContainer },
fills: { label: 'Fills', component: FillsContainer },
};
@@ -0,0 +1,473 @@
import merge from 'lodash/merge';
import { renderHook } from '@testing-library/react';
import { useMarketSelectorList } from './use-market-selector-list';
import { Product } from './product-selector';
import { Sort } from './sort-dropdown';
import { createMarketFragment } from '@vegaprotocol/mock';
import { MarketState } from '@vegaprotocol/types';
import { useMarketList } from '@vegaprotocol/market-list';
import type { Filter } from './market-selector';
import { subDays } from 'date-fns';
jest.mock('@vegaprotocol/market-list');
const mockUseMarketList = useMarketList as jest.Mock;
describe('useMarketSelectorList', () => {
const setup = (initialArgs?: Partial<Filter>) => {
const defaultArgs: Filter = {
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: [],
};
return renderHook((args) => useMarketSelectorList(args), {
initialProps: merge(defaultArgs, initialArgs),
});
};
it('returns all markets active and suspended markets', () => {
const markets = [
createMarketFragment({ id: 'market-0' }),
createMarketFragment({
id: 'market-1',
state: MarketState.STATE_SUSPENDED,
}),
createMarketFragment({
id: 'market-2',
state: MarketState.STATE_CLOSED,
}),
createMarketFragment({
id: 'market-3',
state: MarketState.STATE_PENDING,
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result } = setup();
const expectedFilteredMarkets = markets.filter((m) =>
[MarketState.STATE_ACTIVE, MarketState.STATE_SUSPENDED].includes(m.state)
);
expect(result.current).toEqual({
data: markets,
markets: expectedFilteredMarkets,
loading: false,
error: undefined,
});
});
it('filters by product', () => {
const markets = [
createMarketFragment({
id: 'market-0',
tradableInstrument: {
instrument: {
product: {
__typename: 'Future',
},
},
},
}),
createMarketFragment({
id: 'market-1',
tradableInstrument: {
instrument: {
product: {
__typename: 'Spot' as 'Future', // spot isn't in schema yet
},
},
},
}),
createMarketFragment({
id: 'market-2',
tradableInstrument: {
instrument: {
product: {
__typename: 'Perpetual' as 'Future', // spot isn't in schema yet
},
},
},
}),
];
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.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[1]]);
rerender({
searchTerm: '',
product: Product.Perpetual as 'Future',
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[2]]);
});
it('filters by asset', () => {
const markets = [
createMarketFragment({
id: 'market-0',
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-0',
},
},
},
},
}),
createMarketFragment({
id: 'market-1',
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-0',
},
},
},
},
}),
createMarketFragment({
id: 'market-2',
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-1',
},
},
},
},
}),
createMarketFragment({
id: 'market-3',
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-2',
},
},
},
},
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result, rerender } = setup({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: ['asset-0'],
});
expect(result.current.markets).toEqual([markets[0], markets[1]]);
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: ['asset-0', 'asset-1'],
});
expect(result.current.markets).toEqual([
markets[0],
markets[1],
markets[2],
]);
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: ['asset-0', 'asset-1', 'asset-2'],
});
// all assets selected
expect(result.current.markets).toEqual(markets);
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: ['asset-invalid'],
});
expect(result.current.markets).toEqual([]);
});
it('filters by search term', () => {
const markets = [
createMarketFragment({
id: 'market-0',
tradableInstrument: {
instrument: {
code: 'abc',
name: 'aaa',
},
},
}),
createMarketFragment({
id: 'market-1',
tradableInstrument: {
instrument: {
code: 'def',
name: 'ggg',
},
},
}),
createMarketFragment({
id: 'market-2',
tradableInstrument: {
instrument: {
code: 'defg',
name: 'gggh',
},
},
}),
createMarketFragment({
id: 'market-3',
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.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[0]]);
rerender({
searchTerm: 'def',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[1], markets[2]]);
rerender({
searchTerm: 'defg',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[2]]);
rerender({
searchTerm: 'zzz',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([]);
// by name
rerender({
searchTerm: 'aaa',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[0]]);
rerender({
searchTerm: 'ggg',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([
markets[1],
markets[2],
markets[3],
]);
});
it('sorts albphabetically', () => {
const markets = [
createMarketFragment({
id: 'market-0',
tradableInstrument: {
instrument: {
code: 'd',
},
},
}),
createMarketFragment({
id: 'market-1',
tradableInstrument: {
instrument: {
code: 'b',
},
},
}),
createMarketFragment({
id: 'market-2',
tradableInstrument: {
instrument: {
code: 'a',
},
},
}),
createMarketFragment({
id: 'market-3',
tradableInstrument: {
instrument: {
code: 'c',
},
},
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result } = setup({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([
markets[2],
markets[1],
markets[3],
markets[0],
]);
});
it('sorts by gained', () => {
const markets = [
createMarketFragment({
id: 'market-0',
// @ts-ignore actual fragment doesnt contain candles and is joined later
candles: [
{
close: '100',
},
{
close: '200',
},
],
}),
createMarketFragment({
id: 'market-1',
// @ts-ignore actual fragment doesnt contain candles and is joined later
candles: [
{
close: '100',
},
{
close: '1000',
},
],
}),
createMarketFragment({
id: 'market-2',
// @ts-ignore actual fragment doesnt 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',
marketTimestamps: {
open: subDays(new Date(), 3).toISOString(),
},
}),
createMarketFragment({
id: 'market-1',
marketTimestamps: {
open: subDays(new Date(), 1).toISOString(),
},
}),
createMarketFragment({
id: 'market-2',
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],
]);
});
});
@@ -0,0 +1,82 @@
import { useMemo } from 'react';
import orderBy from 'lodash/orderBy';
import { MarketState } from '@vegaprotocol/types';
import { useMarketList } from '@vegaprotocol/market-list';
import { priceChangePercentage } from '@vegaprotocol/utils';
import type { Filter } from './market-selector';
import { Sort } from './sort-dropdown';
export const useMarketSelectorList = ({
product,
assets,
sort,
searchTerm,
}: Filter) => {
const { data, loading, error } = useMarketList();
const markets = useMemo(() => {
if (!data?.length) return [];
const markets = data
// only active
.filter((m) => {
return [MarketState.STATE_ACTIVE, MarketState.STATE_SUSPENDED].includes(
m.state
);
})
// only selected product type
.filter((m) => {
if (m.tradableInstrument.instrument.product.__typename === product) {
return true;
}
return false;
})
.filter((m) => {
if (assets.length === 0) return true;
return assets.includes(
m.tradableInstrument.instrument.product.settlementAsset.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.None) {
return orderBy(markets, ['tradableInstrument.instrument.code'], ['asc']);
}
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.map((c) => c.close)));
},
],
[dir]
);
}
if (sort === Sort.New) {
return orderBy(
markets,
[(m) => new Date(m.marketTimestamps.open).getTime()],
['desc']
);
}
return markets;
}, [data, product, searchTerm, assets, sort]);
return { markets, data, loading, error };
};
@@ -6,8 +6,8 @@ import { MarketState } from '@vegaprotocol/types';
import { subDays } from 'date-fns';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type { OracleSpecDataConnectionQuery } from '@vegaprotocol/oracles';
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/oracles';
import type { OracleSpecDataConnectionQuery } from '@vegaprotocol/market-info';
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/market-info';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import type {
@@ -2,8 +2,8 @@ import { render, screen } from '@testing-library/react';
import type { Property } from '@vegaprotocol/types';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type { OracleSpecDataConnectionQuery } from '@vegaprotocol/oracles';
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/oracles';
import type { OracleSpecDataConnectionQuery } from '@vegaprotocol/market-info';
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/market-info';
import type { SettlementPriceCellProps } from './settlement-price-cell';
import { SettlementPriceCell } from './settlement-price-cell';
@@ -1,6 +1,6 @@
import { DApp, EXPLORER_ORACLE, useLinks } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { useOracleSpecBindingData } from '@vegaprotocol/oracles';
import { useOracleSpecBindingData } from '@vegaprotocol/market-info';
import { Link } from '@vegaprotocol/ui-toolkit';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
@@ -1,7 +1,7 @@
import { Icon, NavigationLink } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { Links, Routes } from '../../pages/client-router';
import { COG } from '@blueprintjs/icons/src/generated/iconNames';
import { IconNames } from '@blueprintjs/icons';
export const SettingsButton = ({ withMobile }: { withMobile?: boolean }) => {
return (
@@ -9,7 +9,7 @@ export const SettingsButton = ({ withMobile }: { withMobile?: boolean }) => {
{withMobile ? (
t('Settings')
) : (
<Icon name={COG} className="!align-middle" />
<Icon name={IconNames.COG} className="!align-middle" />
)}
</NavigationLink>
);
-1
View File
@@ -1,2 +1 @@
export * from './announcement-banner';
export * from './oracle-banner';
@@ -1,45 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { useMarketOracle } from '@vegaprotocol/market-info';
import ReactMarkdown from 'react-markdown';
import { Intent, NotificationBanner } from '@vegaprotocol/ui-toolkit';
const oracleStatuses = {
UNKNOWN: t(
"This public key's proofs have not been verified yet, or no proofs have been provided yet."
),
GOOD: t("This public key's proofs have been verified."),
SUSPICIOUS: t(
'This public key is suspected to be acting in bad faith, pending investigation.'
),
MALICIOUS: t('This public key has been observed acting in bad faith.'),
RETIRED: t('This public key is no longer in use.'),
COMPROMISED: t(
'This public key is no longer in the control of its original owners.'
),
};
export const OracleBanner = ({ marketId }: { marketId: string }) => {
const oracle = useMarketOracle(marketId);
if (!oracle || oracle.status === 'GOOD') {
return null;
}
return (
<NotificationBanner intent={Intent.Danger}>
<div>
Oracle status for this market is{' '}
<span data-testId="oracle-status">{oracle.status}</span>.{' '}
{oracleStatuses[oracle.status]}
</div>
{oracle.status_reason ? (
<ReactMarkdown
className="react-markdown-container"
skipHtml={true}
disallowedElements={['img']}
linkTarget="_blank"
>
{oracle.status_reason}
</ReactMarkdown>
) : null}
</NotificationBanner>
);
};
+2 -2
View File
@@ -43,7 +43,7 @@ export const HeaderStat = ({
testId?: string;
}) => {
const itemClass =
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-default text-neutral-500 dark:text-neutral-400';
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-default first:border-none text-neutral-500 dark:text-neutral-400';
const itemHeading = 'text-black dark:text-white';
return (
@@ -72,7 +72,7 @@ export const HeaderTitle = ({
secondaryContent: ReactNode;
}) => {
return (
<div className="text-left">
<div className="text-left" data-testid="header-title">
<div className="text-sm md:text-md lg:text-lg whitespace-nowrap leading-4">
{primaryContent}
</div>
@@ -19,11 +19,10 @@ import { useCheckLiquidityStatus } from '@vegaprotocol/liquidity';
import { AuctionTrigger, MarketTradingMode } from '@vegaprotocol/types';
import {
addDecimalsFormatNumber,
createDocsLinks,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useEnvironment } from '@vegaprotocol/environment';
import { DocsLinks } from '@vegaprotocol/environment';
interface Props {
marketId?: string;
@@ -47,8 +46,6 @@ export const MarketLiquiditySupplied = ({
params.market_liquidity_targetstake_triggering_ratio
);
const { VEGA_DOCS_URL } = useEnvironment();
const variables = useMemo(
() => ({
marketId: marketId || '',
@@ -131,11 +128,8 @@ export const MarketLiquiditySupplied = ({
<Link href={`/#/liquidity/${marketId}`} data-testid="view-liquidity-link">
{t('View liquidity provision table')}
</Link>
{VEGA_DOCS_URL && (
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).LIQUIDITY}
className="mt-2"
>
{DocsLinks && (
<ExternalLink href={DocsLinks.LIQUIDITY} className="mt-2">
{t('Learn about providing liquidity')}
</ExternalLink>
)}
@@ -0,0 +1,42 @@
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { Navbar } from './navbar';
describe('Navbar', () => {
const pubKey = 'pubKey';
it('should be properly rendered', () => {
render(
<MockedProvider>
<MemoryRouter>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
>
<Navbar theme="dark" />
</VegaWalletContext.Provider>
</MemoryRouter>
</MockedProvider>
);
expect(screen.getByTestId('Markets')).toBeInTheDocument();
expect(screen.getByTestId('Trading')).toBeInTheDocument();
expect(screen.getByTestId('Portfolio')).toBeInTheDocument();
});
it('Markets page route should not match empty market page', () => {
render(
<MockedProvider>
<MemoryRouter initialEntries={['/markets/all']}>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
>
<Navbar theme="dark" />
</VegaWalletContext.Provider>
</MemoryRouter>
</MockedProvider>
);
expect(screen.getByTestId('Markets')).toHaveClass('active');
expect(screen.getByTestId('Trading')).not.toHaveClass('active');
});
});
+5 -7
View File
@@ -5,6 +5,7 @@ import {
TOKEN_GOVERNANCE,
useEnvironment,
useLinks,
DocsLinks,
} from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { useGlobalStore } from '../../stores';
@@ -22,7 +23,6 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { Links, Routes } from '../../pages/client-router';
import { createDocsLinks } from '@vegaprotocol/utils';
import { SettingsButton } from '../../client-pages/settings';
import {
ProtocolUpgradeCountdown,
@@ -34,7 +34,7 @@ export const Navbar = ({
}: {
theme: ComponentProps<typeof Navigation>['theme'];
}) => {
const { VEGA_DOCS_URL, GITHUB_FEEDBACK_URL } = useEnvironment();
const { GITHUB_FEEDBACK_URL } = useEnvironment();
const tokenLink = useLinks(DApp.Token);
const { marketId } = useGlobalStore((store) => ({
marketId: store.marketId,
@@ -75,7 +75,7 @@ export const Navbar = ({
</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink data-testid="Trading" to={tradingPath}>
<NavigationLink data-testid="Trading" to={tradingPath} end>
{t('Trading')}
</NavigationLink>
</NavigationItem>
@@ -92,15 +92,13 @@ export const Navbar = ({
{t('Governance')}
</NavExternalLink>
</NavigationItem>
{VEGA_DOCS_URL && GITHUB_FEEDBACK_URL && (
{DocsLinks?.NEW_TO_VEGA && GITHUB_FEEDBACK_URL && (
<NavigationItem>
<NavigationTrigger>{t('Resources')}</NavigationTrigger>
<NavigationContent>
<NavigationList>
<NavigationItem>
<NavExternalLink
href={createDocsLinks(VEGA_DOCS_URL).NEW_TO_VEGA}
>
<NavExternalLink href={DocsLinks.NEW_TO_VEGA}>
{t('Docs')}
</NavExternalLink>
</NavigationItem>
@@ -1,3 +1,2 @@
export * from './select-market-columns';
export * from './select-market-table';
export * from './select-market';
@@ -7,7 +7,7 @@ import {
} from '@vegaprotocol/market-list';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { PriceCell, signedNumberCssClass } from '@vegaprotocol/datagrid';
import { PriceCell } from '@vegaprotocol/datagrid';
import { Link as UILink, Sparkline, Tooltip } from '@vegaprotocol/ui-toolkit';
import isNil from 'lodash/isNil';
import type { CandleClose } from '@vegaprotocol/types';
@@ -341,187 +341,3 @@ export const columns = (
];
return selectMarketColumns;
};
export const columnsPositionMarkets = (
market: MarketMaybeWithDataAndCandles,
onSelect: (id: string, metaKey?: boolean) => void,
inViewRoot?: RefObject<HTMLElement>,
openVolume?: string,
onCellClick?: OnCellClickHandler
) => {
const candlesClose = market.candles
?.map((candle) => candle?.close)
.filter((c: string | undefined): c is CandleClose => !isNil(c));
const candleLow = market.candles && calcCandleLow(market.candles);
const candleHigh = market.candles && calcCandleHigh(market.candles);
const candleVolume = market.candles && calcCandleVolume(market.candles);
const selectMarketColumns: Column[] = [
{
kind: ColumnKind.Market,
value: (
<Link
to={Links[Routes.MARKET](market.id)}
data-testid={`market-link-${market.id}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onSelect(market.id, e.metaKey || e.ctrlKey);
}}
>
<UILink>{market.tradableInstrument.instrument.code}</UILink>
</Link>
),
className: cellClassNames,
onlyOnDetailed: false,
},
{
kind: ColumnKind.ProductType,
value: market.tradableInstrument.instrument.product.__typename,
className: `py-2 first:text-left hidden sm:table-cell`,
onlyOnDetailed: false,
},
{
kind: ColumnKind.LastPrice,
value: (
<MarketMarkPrice
marketId={market.id}
decimalPlaces={market?.decimalPlaces}
inViewRoot={inViewRoot}
initialValue={market.data?.markPrice}
asPriceCell
/>
),
className: cellClassNames,
onlyOnDetailed: false,
},
{
kind: ColumnKind.Change24,
value: (
<Last24hPriceChange
marketId={market.id}
decimalPlaces={market?.decimalPlaces}
inViewRoot={inViewRoot}
initialValue={candlesClose}
/>
),
className: cellClassNames,
onlyOnDetailed: false,
},
{
kind: ColumnKind.Sparkline,
value: candlesClose && (
<Sparkline
width={100}
height={20}
muted={false}
data={candlesClose.map((c: string) => Number(c))}
/>
),
className: `${cellClassNames} hidden lg:table-cell`,
onlyOnDetailed: false,
},
{
kind: ColumnKind.Asset,
value: (
<button
data-dialog-trigger
className="inline underline"
onClick={(e) => {
e.stopPropagation();
if (!onCellClick) return;
onCellClick(
e,
ColumnKind.Asset,
market.tradableInstrument.instrument.product.settlementAsset.id
);
}}
>
{market.tradableInstrument.instrument.product.settlementAsset.symbol}
</button>
),
className: `${cellClassNames} hidden sm:table-cell`,
onlyOnDetailed: false,
},
{
kind: ColumnKind.High24,
value: candleHigh ? (
<PriceCell
value={Number(candleHigh)}
valueFormatted={addDecimalsFormatNumber(
candleHigh.toString(),
market.decimalPlaces,
2
)}
/>
) : (
'-'
),
className: `${cellClassNames} hidden xl:table-cell font-mono`,
onlyOnDetailed: true,
},
{
kind: ColumnKind.Low24,
value: candleLow ? (
<PriceCell
value={Number(candleLow)}
valueFormatted={addDecimalsFormatNumber(
candleLow.toString(),
market.decimalPlaces,
2
)}
/>
) : (
'-'
),
className: `${cellClassNames} hidden xl:table-cell font-mono`,
onlyOnDetailed: true,
},
{
kind: ColumnKind.Volume,
value: (
<Last24hVolume
marketId={market.id}
inViewRoot={inViewRoot}
positionDecimalPlaces={market.positionDecimalPlaces}
initialValue={candleVolume}
formatDecimals={2}
/>
),
className: `${cellClassNames} hidden lg:table-cell font-mono`,
onlyOnDetailed: true,
dataTestId: 'market-volume',
},
{
kind: ColumnKind.TradingMode,
value: (
<MarketTradingMode
marketId={market?.id}
inViewRoot={inViewRoot}
initialTradingMode={market.tradingMode}
initialTrigger={market.data?.trigger}
/>
),
className: `${cellClassNames} hidden lg:table-cell`,
onlyOnDetailed: true,
dataTestId: 'trading-mode-col',
},
{
kind: ColumnKind.Fee,
value: <FeesCell feeFactors={market.fees.factors} />,
className: `${cellClassNames} hidden xl:table-cell font-mono`,
onlyOnDetailed: true,
},
{
kind: ColumnKind.Position,
value: (
<p className={signedNumberCssClass(openVolume || '')}>
{openVolume &&
addDecimalsFormatNumber(openVolume, market.positionDecimalPlaces)}
</p>
),
className: `${cellClassNames} hidden xxl:table-cell font-mono`,
onlyOnDetailed: true,
},
];
return selectMarketColumns;
};
@@ -1,7 +1,6 @@
import { columnHeaders } from './select-market-columns';
import classNames from 'classnames';
import type { Column } from './select-market-columns';
import type { ReactNode } from 'react';
export const SelectMarketTableHeader = ({
detailed = false,
@@ -62,23 +61,3 @@ export const SelectMarketTableRow = ({
</tr>
);
};
export const SelectMarketTableRowSplash = ({
children,
colSpan,
}: {
children: ReactNode;
colSpan: number;
}) => {
return (
<tr className={`relative`}>
<td
className="text-center p-10 pt-14 text-xs"
key="splash"
colSpan={colSpan}
>
{children}
</td>
</tr>
);
};
@@ -1,203 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react';
import * as Schema from '@vegaprotocol/types';
import { SelectAllMarketsTableBody } from './select-market';
import type {
MarketMaybeWithCandles,
MarketMaybeWithData,
MarketData,
} from '@vegaprotocol/market-list';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
type Market = MarketMaybeWithCandles & MarketMaybeWithData;
type PartialMarket = Partial<
Omit<Market, 'data'> & { data: Partial<MarketData> }
>;
const MARKET_A: PartialMarket = {
__typename: 'Market',
id: '1',
decimalPlaces: 2,
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '1',
code: 'ABCDEF',
name: 'ABCDEF 1-Day',
product: {
__typename: 'Future',
quoteName: 'ABCDEF',
settlementAsset: {
__typename: 'Asset',
id: 'asset-ABC',
name: '',
decimals: 2,
symbol: 'ABC',
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: 'oracleId',
},
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: 'oracleId',
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
tradingTerminationProperty: 'trading-termination-property',
settlementDataProperty: 'settlement-data-property',
},
},
metadata: {
__typename: 'InstrumentMetadata',
tags: ['ABC', 'DEF'],
},
},
},
fees: {
__typename: 'Fees',
factors: {
__typename: 'FeeFactors',
infrastructureFee: '0.01',
liquidityFee: '0.01',
makerFee: '0.01',
},
},
data: {
__typename: 'MarketData',
market: {
__typename: 'Market',
id: '1',
},
markPrice: '90',
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING,
marketState: Schema.MarketState.STATE_PENDING,
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
indicativeVolume: '1000',
},
candles: [
{
__typename: 'Candle',
high: '100',
low: '10',
open: '10',
close: '80',
volume: '1000',
periodStart: '2022-11-01T15:49:00Z',
},
{
__typename: 'Candle',
high: '10',
low: '1',
open: '1',
close: '100',
volume: '1000',
periodStart: '2022-11-01T15:50:00Z',
},
],
};
const MARKET_B: PartialMarket = {
__typename: 'Market',
id: '2',
decimalPlaces: 2,
positionDecimalPlaces: 0,
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '2',
code: 'XYZ',
name: 'XYZ 1-Day',
product: {
__typename: 'Future',
quoteName: 'XYZ',
settlementAsset: {
__typename: 'Asset',
id: 'asset-XYZ',
name: 'asset-XYZ',
decimals: 2,
symbol: 'XYZ',
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: 'oracleId',
},
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: 'oracleId',
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
tradingTerminationProperty: 'trading-termination-property',
settlementDataProperty: 'settlement-data-property',
},
},
metadata: {
__typename: 'InstrumentMetadata',
tags: ['XYZ'],
},
},
},
fees: {
__typename: 'Fees',
factors: {
__typename: 'FeeFactors',
infrastructureFee: '0.01',
liquidityFee: '0.01',
makerFee: '0.01',
},
},
data: {
__typename: 'MarketData',
market: {
__typename: 'Market',
id: '2',
},
markPrice: '123.123',
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING,
marketState: Schema.MarketState.STATE_PENDING,
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
indicativeVolume: '2000',
},
candles: [
{
__typename: 'Candle',
high: '100',
low: '10',
open: '10',
close: '80',
volume: '1000',
periodStart: '2022-11-01T15:49:00Z',
},
],
};
describe('SelectMarket', () => {
const table = document.createElement('table');
it('should render the SelectAllMarketsTableBody', () => {
const onSelect = jest.fn();
const onCellClick = jest.fn();
const { container } = render(
<MemoryRouter>
<SelectAllMarketsTableBody
markets={[MARKET_A as Market, MARKET_B as Market]}
onCellClick={onCellClick}
onSelect={onSelect}
/>
</MemoryRouter>,
{ wrapper: MockedProvider, container: document.body.appendChild(table) }
);
expect(screen.getByText('ABCDEF')).toBeTruthy(); // name
expect(screen.getByText('25.00%')).toBeTruthy(); // price change
expect(container).toHaveTextContent(/1,000/); // volume
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]);
expect(onSelect).toHaveBeenCalledWith('1', false);
});
});
@@ -1,214 +0,0 @@
import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import type { RefObject } from 'react';
import { useMarketList } from '@vegaprotocol/market-list';
import { positionsDataProvider } from '@vegaprotocol/positions';
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { ExternalLink, Icon, Loader, Popover } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import {
columnHeaders,
columnHeadersPositionMarkets,
columns,
columnsPositionMarkets,
} from './select-market-columns';
import {
SelectMarketTableHeader,
SelectMarketTableRow,
SelectMarketTableRowSplash,
} from './select-market-table';
import type { ReactNode } from 'react';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/market-list';
import type { PositionFieldsFragment } from '@vegaprotocol/positions';
import type { Column, OnCellClickHandler } from './select-market-columns';
import {
DApp,
TOKEN_NEW_MARKET_PROPOSAL,
useLinks,
} from '@vegaprotocol/environment';
import { HeaderTitle } from '../header';
export const SelectAllMarketsTableBody = ({
markets,
positions,
onSelect,
onCellClick,
inViewRoot,
headers = columnHeaders,
tableColumns = (market) => columns(market, onSelect, onCellClick, inViewRoot),
}: {
markets?: MarketMaybeWithDataAndCandles[] | null;
positions?: PositionFieldsFragment[];
title?: string;
onSelect: (id: string, metaKey?: boolean) => void;
onCellClick: OnCellClickHandler;
headers?: Column[];
tableColumns?: (
market: MarketMaybeWithDataAndCandles,
inViewRoot?: RefObject<HTMLDivElement>,
openVolume?: string
) => Column[];
inViewRoot?: RefObject<HTMLDivElement>;
}) => {
const tokenLink = useLinks(DApp.Token);
if (!markets) return null;
return (
<>
<thead className="bg-neutral-100 dark:bg-neutral-800">
<SelectMarketTableHeader detailed={true} headers={headers} />
</thead>
{/* Border styles required to create space between tbody elements margin/padding don't work */}
<tbody className="border-b-[10px] border-transparent">
{markets.length > 0 ? (
markets?.map((market, i) => (
<SelectMarketTableRow
marketId={market.id}
key={i}
detailed
onSelect={onSelect}
columns={tableColumns(
market,
inViewRoot,
positions &&
positions.find((p) => p.market.id === market.id)?.openVolume
)}
/>
))
) : (
<SelectMarketTableRowSplash colSpan={12}>
{t('No markets ')}
<ExternalLink href={tokenLink(TOKEN_NEW_MARKET_PROPOSAL)}>
{t('Propose a new market')}
</ExternalLink>
</SelectMarketTableRowSplash>
)}
</tbody>
</>
);
};
export const SelectMarketPopover = ({
marketCode,
marketName,
onSelect,
onCellClick,
}: {
marketCode: string;
marketName: string;
onSelect: (id: string, metaKey?: boolean) => void;
onCellClick: OnCellClickHandler;
}) => {
const { pubKey } = useVegaWallet();
const [open, setOpen] = useState(false);
const inViewRoot = useRef<HTMLDivElement>(null);
const {
data,
loading: marketsLoading,
reload: marketListReload,
} = useMarketList();
const {
data: positions,
loading: positionsLoading,
reload,
} = useDataProvider({
dataProvider: positionsDataProvider,
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const onSelectMarket = useCallback(
(marketId: string, metaKey?: boolean) => {
onSelect(marketId, metaKey);
setOpen(false);
},
[onSelect]
);
const iconClass = open ? 'rotate-180' : '';
const markets = useMemo(
() =>
data?.filter((market) =>
positions?.find((node) => node.market.id === market.id)
),
[data, positions]
);
useEffect(() => {
if (open) {
reload();
marketListReload();
}
}, [open, marketListReload, reload]);
return (
<Popover
open={open}
onChange={setOpen}
trigger={
<div className="flex items-center gap-2">
<HeaderTitle
primaryContent={marketCode}
secondaryContent={marketName}
/>
<Icon name="chevron-down" className={iconClass} size={6} />
</div>
}
>
<div
className="w-[90vw] max-h-[80vh] overflow-y-auto"
data-testid="select-market-list"
ref={inViewRoot}
>
{marketsLoading || (pubKey && positionsLoading) ? (
<div className="flex items-center gap-4">
<Loader size="small" />
{t('Loading market data')}
</div>
) : (
<table className="relative text-sm w-full whitespace-nowrap">
{pubKey && (positions?.length ?? 0) && (markets?.length ?? 0) ? (
<>
<TableTitle>{t('My markets')}</TableTitle>
<SelectAllMarketsTableBody
inViewRoot={inViewRoot}
markets={markets}
positions={positions || undefined}
onSelect={onSelectMarket}
onCellClick={onCellClick}
headers={columnHeadersPositionMarkets}
tableColumns={(market, inViewRoot, openVolume) =>
columnsPositionMarkets(
market,
onSelectMarket,
inViewRoot,
openVolume,
onCellClick
)
}
/>
</>
) : null}
<TableTitle>{t('All markets')}</TableTitle>
<SelectAllMarketsTableBody
inViewRoot={inViewRoot}
markets={data}
onSelect={onSelectMarket}
onCellClick={onCellClick}
/>
</table>
)}
</div>
</Popover>
);
};
const TableTitle = ({ children }: { children: ReactNode }) => {
return (
<thead>
<tr>
<th className="font-normal text-left">
<h3 className="text-lg">{children}</h3>
</th>
</tr>
</thead>
);
};
@@ -8,11 +8,10 @@ import {
import { RISK_ACCEPTED_KEY } from '../constants';
import { TelemetryApproval } from './telemetry-approval';
import {
DOCS_VEGA_WALLET,
GET_VEGA_WALLET_URL,
Networks,
useDocsLink,
useEnvironment,
DocsLinks,
ExternalLinks,
} from '@vegaprotocol/environment';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
@@ -53,8 +52,6 @@ export const RiskNoticeDialog = ({ onClose, network }: Props) => {
const TestnetContent = ({ network }: { network: Networks }) => {
const { GITHUB_FEEDBACK_URL } = useEnvironment();
const docsLink = useDocsLink();
return (
<>
<p className="mb-4">
@@ -69,16 +66,16 @@ const TestnetContent = ({ network }: { network: Networks }) => {
[network]
)}
</p>
{GITHUB_FEEDBACK_URL && GET_VEGA_WALLET_URL && docsLink && (
{GITHUB_FEEDBACK_URL && DocsLinks && (
<ul className="list-disc pl-4">
<li className="mb-1">
<Link href={GET_VEGA_WALLET_URL} target="_blank">
<Link href={ExternalLinks.VEGA_WALLET_URL} target="_blank">
<span className="underline">{t('Get a Vega Wallet')}</span>{' '}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</Link>
</li>
<li className="mb-1">
<Link href={docsLink(DOCS_VEGA_WALLET)} target="_blank">
<Link href={DocsLinks.VEGA_WALLET_TOOLS_URL} target="_blank">
<span className="underline">{t('Learn about Vega Wallet')}</span>{' '}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</Link>
@@ -1,13 +1,13 @@
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import {
BLOG,
DApp,
Networks,
TOKEN_NEW_MARKET_PROPOSAL,
TOKEN_PROPOSALS,
useEnvironment,
useLinks,
ExternalLinks,
} from '@vegaprotocol/environment';
import { ProposedMarkets } from './proposed-markets';
@@ -52,7 +52,7 @@ export const WelcomeNoticeDialog = () => {
</ExternalLink>
</li>
<li>
<ExternalLink target="_blank" href={BLOG}>
<ExternalLink target="_blank" href={ExternalLinks.BLOG}>
{t('Read about the mainnet launch')}
</ExternalLink>
</li>
+2 -2
View File
@@ -81,7 +81,7 @@ function AppBody({ Component }: AppProps) {
const gridClasses = classNames(
'h-full relative z-0 grid',
'grid-rows-[repeat(3,min-content),1fr]'
'grid-rows-[repeat(3,min-content),minmax(0,1fr)]'
);
return (
@@ -93,7 +93,7 @@ function AppBody({ Component }: AppProps) {
<Title />
<div className={gridClasses}>
<AnnouncementBanner />
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'system'} />
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
<div data-testid="banners">
<ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
+21
View File
@@ -127,3 +127,24 @@ html [data-theme='light'] {
.ag-theme-balham .ag-row.no-hover:hover {
background: var(--ag-background-color);
}
.virtualized-list {
/* Works on Firefox */
scrollbar-width: thin;
scrollbar-color: #999 #333;
}
/* Works on Chrome, Edge, and Safari */
.virtualized-list::-webkit-scrollbar {
width: 6px;
background-color: #999;
}
.virtualized-list::-webkit-scrollbar-thumb {
width: 6px;
background-color: #333;
}
.virtualized-list::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px rgb(0 0 0 / 30%);
background-color: #999;
}
+3 -3
View File
@@ -316,7 +316,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
onClickDeposit && onClickDeposit(data.asset.id);
}}
>
<span>
<span className="flex gap-2">
<VegaIcon
name={VegaIconNames.DEPOSIT}
size={16}
@@ -331,7 +331,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
onClickWithdraw && onClickWithdraw(data.asset.id)
}
>
<span>
<span className="flex gap-2">
<VegaIcon
name={VegaIconNames.WITHDRAW}
size={16}
@@ -347,7 +347,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
setRow(data);
}}
>
<span>
<span className="flex gap-2">
<VegaIcon
name={VegaIconNames.BREAKDOWN}
size={16}
+2 -2
View File
@@ -12,13 +12,13 @@ export * from '../fills/src/lib/fills.mock';
export * from '../proposals/src/lib/proposals-data-provider/proposals.mock';
export * from '../ledger/src/lib/ledger-entries.mock';
export * from '../market-depth/src/lib/market-depth.mock';
export * from '../market-info/src/components/market-info/market-info.mock';
export * from '../market-info/src/lib/components/market-info/market-info.mock';
export * from '../market-list/src/lib/market-candles.mock';
export * from '../market-list/src/lib/market-data.mock';
export * from '../market-list/src/lib/markets-candles.mock';
export * from '../market-list/src/lib/markets-data.mock';
export * from '../market-list/src/lib/markets.mock';
export * from '../oracles/src/lib/oracle-spec-data-connection.mock';
export * from '../market-info/src/lib/oracle-spec-data-connection.mock';
export * from '../orders/src/lib/components/order-data-provider/orders.mock';
export * from '../positions/src/lib/positions.mock';
export * from '../network-parameters/src/network-params.mock';
@@ -1,6 +1,8 @@
import {
addDecimalsFormatNumber,
formatNumberPercentage,
priceChange,
priceChangePercentage,
} from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { memo, forwardRef } from 'react';
@@ -13,29 +15,6 @@ export interface PriceChangeCellProps {
decimalPlaces?: number;
}
export const priceChangePercentage = (candles: string[]) => {
const change = priceChange(candles);
if (change && candles && candles.length > 0) {
const yesterdayLastPrice = candles[0] && BigInt(candles[0]);
if (yesterdayLastPrice) {
return new BigNumber(change.toString())
.dividedBy(new BigNumber(yesterdayLastPrice.toString()))
.multipliedBy(100)
.toNumber();
}
return 0;
}
return 0;
};
export const priceChange = (candles: string[]) => {
return candles &&
candles[candles.length - 1] !== undefined &&
candles[0] !== undefined
? BigInt(candles[candles.length - 1] ?? 0) - BigInt(candles[0] ?? 0)
: 0;
};
export const PriceChangeCell = memo(
forwardRef<HTMLSpanElement, PriceChangeCellProps>(
({ candles, decimalPlaces }: PriceChangeCellProps, ref) => {
+6
View File
@@ -0,0 +1,6 @@
function ReactMarkdown({ children }) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
}
export default ReactMarkdown;
@@ -4,7 +4,6 @@ export * from './deal-ticket-limit-amount';
export * from './deal-ticket-market-amount';
export * from './deal-ticket';
export * from './expiry-selector';
export * from './market-selector';
export * from './side-selector';
export * from './time-in-force-selector';
export * from './type-selector';
@@ -1,328 +0,0 @@
import type { ReactNode } from 'react';
import React, {
useCallback,
useState,
useEffect,
useRef,
useMemo,
} from 'react';
import * as DialogPrimitives from '@radix-ui/react-dialog';
import classNames from 'classnames';
import {
ButtonLink,
Icon,
Input,
Loader,
Splash,
} from '@vegaprotocol/ui-toolkit';
import {
useScreenDimensions,
useOutsideClick,
} from '@vegaprotocol/react-helpers';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { t } from '@vegaprotocol/i18n';
import { IconNames } from '@blueprintjs/icons';
import * as Schema from '@vegaprotocol/types';
import type { Market } from '@vegaprotocol/market-list';
import { marketsProvider } from '@vegaprotocol/market-list';
interface Props {
market: Market;
setMarket: (marketId: string) => void;
ItemRenderer?: React.FC<{
market: Market;
isMobile?: boolean;
}>;
}
function escapeRegExp(str: string) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
export const MarketSelector = ({ market, setMarket, ItemRenderer }: Props) => {
const { isMobile } = useScreenDimensions();
const contRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const arrowButtonRef = useRef<HTMLButtonElement | null>(null);
const [results, setResults] = useState<Market[]>([]);
const [showPane, setShowPane] = useState(false);
const [lookup, setLookup] = useState(
market.tradableInstrument.instrument.name || ''
);
const [dialogContent, setDialogContent] = useState<React.ReactNode | null>(
null
);
const { data, loading, error } = useDataProvider({
dataProvider: marketsProvider,
variables: undefined,
skipUpdates: true,
});
const outsideClickCb = useCallback(() => {
if (!isMobile) {
setShowPane(false);
}
}, [setShowPane, isMobile]);
useOutsideClick({ refs: [contRef, arrowButtonRef], func: outsideClickCb });
const handleOnChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const {
target: { value },
} = event;
setLookup(value);
setShowPane(true);
},
[setLookup, setShowPane]
);
const handleMarketSelect = useCallback(
(market: {
id: string;
tradableInstrument: { instrument: { name: string } };
}) => {
setLookup(market.tradableInstrument.instrument.name);
setShowPane(false);
setMarket(market.id);
inputRef.current?.focus();
},
[setLookup, setShowPane, setMarket, inputRef]
);
const handleItemKeyDown = useCallback(
(event: React.KeyboardEvent, market: Market, index: number) => {
switch (event.key) {
case 'ArrowDown':
if (index < results.length - 1) {
(contRef.current?.children[index + 1] as HTMLDivElement).focus();
}
break;
case 'ArrowUp':
if (!index) {
inputRef.current?.focus();
inputRef.current?.setSelectionRange(
inputRef.current?.value.length,
inputRef.current?.value.length
);
return;
}
(contRef.current?.children[index - 1] as HTMLDivElement).focus();
break;
case 'Enter':
event.preventDefault();
handleMarketSelect(market);
break;
default:
setShowPane(false);
setLookup(market.tradableInstrument.instrument.name);
}
},
[results, handleMarketSelect]
);
const handleInputKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (event.key === 'ArrowDown') {
(contRef.current?.children[0] as HTMLDivElement)?.focus();
}
},
[contRef]
);
const handleOnBlur = useCallback(() => {
if (!lookup && !showPane) {
setLookup(market.tradableInstrument.instrument.name);
}
}, [market, lookup, showPane, setLookup]);
const openPane = useCallback(() => {
setShowPane(!showPane);
inputRef.current?.focus();
}, [showPane, setShowPane, inputRef]);
const handleDialogOnchange = useCallback(
(isOpen: boolean) => {
setShowPane(isOpen);
if (!isOpen) {
setLookup(lookup || market.tradableInstrument.instrument.name);
inputRef.current?.focus();
}
},
[
setShowPane,
lookup,
setLookup,
market.tradableInstrument.instrument.name,
inputRef,
]
);
const selectorContent = useMemo(() => {
return (
<div className="relative flex flex-col">
<div className="relative w-full min-h-[30px]">
<Input
className="h-[30px] w-[calc(100%-20px)] border-none outline-none"
ref={inputRef}
tabIndex={0}
value={lookup}
placeholder={t('Search')}
onChange={handleOnChange}
onKeyDown={handleInputKeyDown}
onBlur={handleOnBlur}
/>
<span className="absolute self-end top-0 right-0 z-10">
<ButtonLink
onClick={openPane}
ref={arrowButtonRef}
data-testid="arrow-button"
>
<Icon
name={IconNames.ARROW_DOWN}
className={classNames('fill-current transition-transform', {
'rotate-180': showPane,
})}
/>
</ButtonLink>
</span>
</div>
<hr className="mb-2" />
<div
className={classNames(
'md:absolute z-20 flex flex-col top-[30px] md:drop-shadow-md md:border md:border-black md:dark:border-white bg-white dark:bg-black text-black dark:text-white min-w-full md:max-h-[200px] overflow-y-auto',
showPane ? 'block' : 'hidden'
)}
data-testid="market-pane"
>
{loading && (
<div className="p-4">
<Loader size="small" />
</div>
)}
{error && (
<Splash>{t(`Something went wrong: ${error.message}`)}</Splash>
)}
<div ref={contRef} className="w-full">
{results.map((market, i) => (
<div
role="button"
tabIndex={0}
key={market.id}
className="bg-white dark:bg-black cursor-pointer px-4 py-2"
onClick={() => handleMarketSelect(market)}
onKeyDown={(e) => handleItemKeyDown(e, market, i)}
>
{ItemRenderer ? (
<ItemRenderer market={market} />
) : (
market.tradableInstrument.instrument.name
)}
</div>
))}
</div>
</div>
</div>
);
}, [
ItemRenderer,
error,
handleInputKeyDown,
handleItemKeyDown,
handleMarketSelect,
handleOnBlur,
handleOnChange,
loading,
lookup,
openPane,
results,
showPane,
]);
useEffect(() => {
setResults(
data?.filter(
(item) =>
item.state === Schema.MarketState.STATE_ACTIVE &&
item.tradableInstrument.instrument.name.match(
new RegExp(escapeRegExp(lookup), 'i')
)
) || []
);
}, [data, lookup]);
useEffect(() => {
inputRef.current?.focus();
}, [inputRef]);
useEffect(() => {
if (showPane && isMobile) {
setDialogContent(selectorContent);
inputRef.current?.focus();
window.scrollTo(0, 0);
} else {
setDialogContent(null);
}
}, [selectorContent, showPane, isMobile, setDialogContent]);
return (
<>
{!dialogContent && selectorContent}
<MarketDrawer
open={Boolean(dialogContent)}
onChange={handleDialogOnchange}
>
{dialogContent}
</MarketDrawer>
</>
);
};
interface MarketDrawerProps {
children: ReactNode;
open: boolean;
onChange?: (isOpen: boolean) => void;
}
export const MarketDrawer = ({
children,
open,
onChange,
}: MarketDrawerProps) => {
const contentClasses = classNames(
// Positions the modal in the center of screen
'z-20 fixed p-8 inset-x-1/2 dark:text-white w-screen',
// Need to apply background and text colors again as content is rendered in a portal
'dark:bg-black bg-white',
'left-[0px] top-[99px] h-[calc(100%-99px)] overflow-y-auto'
);
return (
<DialogPrimitives.Root open={open} onOpenChange={(x) => onChange?.(x)}>
<DialogPrimitives.Portal>
<DialogPrimitives.Overlay
className="fixed inset-0 bg-black/50 z-10"
data-testid="dialog-overlay"
/>
<DialogPrimitives.Content className={contentClasses}>
<DialogPrimitives.Close
className="p-2 absolute top-2 right-2"
data-testid="dialog-close"
>
<Icon name="cross" />
</DialogPrimitives.Close>
<div className="flex gap-4 max-w-full">
<div data-testid="dialog-content" className="flex-1">
<h1 className="text-xl uppercase mb-4" data-testid="dialog-title">
{t('Select market')}
</h1>
<div>{children}</div>
</div>
</div>
</DialogPrimitives.Content>
</DialogPrimitives.Portal>
</DialogPrimitives.Root>
);
};
@@ -2,8 +2,8 @@ import { useMemo } from 'react';
import { parseISO, isValid, isAfter } from 'date-fns';
import classNames from 'classnames';
import { useProposalOfMarketQuery } from '@vegaprotocol/proposals';
import { useEnvironment } from '@vegaprotocol/environment';
import { createDocsLinks, getDateTimeFormat } from '@vegaprotocol/utils';
import { DocsLinks } from '@vegaprotocol/environment';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
import { ExternalLink, SimpleGrid } from '@vegaprotocol/ui-toolkit';
@@ -23,7 +23,6 @@ export const TradingModeTooltip = ({
skip,
skipGrid,
}: TradingModeTooltipProps) => {
const { VEGA_DOCS_URL } = useEnvironment();
const { data: market } = useMarket(marketId);
const { data: marketData } = useStaticMarketData(marketId, skip);
const { marketTradingMode, trigger } = marketData || {};
@@ -102,9 +101,9 @@ export const TradingModeTooltip = ({
</span>
</>
)}
{VEGA_DOCS_URL && (
{DocsLinks && (
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).AUCTION_TYPE_OPENING}
href={DocsLinks.AUCTION_TYPE_OPENING}
className="ml-1"
>
{t('Find out more')}
@@ -126,12 +125,9 @@ export const TradingModeTooltip = ({
'This market is in auction until it reaches sufficient liquidity.'
)}
</span>
{VEGA_DOCS_URL && (
{DocsLinks && (
<ExternalLink
href={
createDocsLinks(VEGA_DOCS_URL)
.AUCTION_TYPE_LIQUIDITY_MONITORING
}
href={DocsLinks.AUCTION_TYPE_LIQUIDITY_MONITORING}
className="ml-1"
>
{t('Find out more')}
@@ -151,12 +147,9 @@ export const TradingModeTooltip = ({
'This market may have sufficient liquidity but there are not enough priced limit orders in the order book, which are required to deploy liquidity commitment pegged orders.'
)}
</span>
{VEGA_DOCS_URL && (
{DocsLinks && (
<ExternalLink
href={
createDocsLinks(VEGA_DOCS_URL)
.AUCTION_TYPE_LIQUIDITY_MONITORING
}
href={DocsLinks.AUCTION_TYPE_LIQUIDITY_MONITORING}
className="ml-1"
>
{t('Find out more')}
@@ -174,12 +167,9 @@ export const TradingModeTooltip = ({
<span>
{t('This market is in auction due to high price volatility.')}
</span>{' '}
{VEGA_DOCS_URL && (
{DocsLinks && (
<ExternalLink
href={
createDocsLinks(VEGA_DOCS_URL)
.AUCTION_TYPE_PRICE_MONITORING
}
href={DocsLinks.AUCTION_TYPE_PRICE_MONITORING}
className="ml-1"
>
{t('Find out more')}
@@ -1,12 +1,11 @@
import { useBridgeContract, useTokenContract } from '@vegaprotocol/web3';
import { useCallback, useEffect, useState } from 'react';
import BigNumber from 'bignumber.js';
import * as Sentry from '@sentry/react';
import { useGetAllowance } from './use-get-allowance';
import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
import { useGetDepositMaximum } from './use-get-deposit-maximum';
import { useGetDepositedAmount } from './use-get-deposited-amount';
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
import { isAssetTypeERC20, localLoggerFactory } from '@vegaprotocol/utils';
import { useAccountBalance } from '@vegaprotocol/accounts';
import type { Asset } from '@vegaprotocol/assets';
@@ -43,7 +42,9 @@ export const useDepositBalances = (asset: Asset | undefined) => {
const getBalances = useCallback(async () => {
if (!asset) return;
const logger = localLoggerFactory({ application: 'deposits' });
try {
logger.info('get deposit balances', { asset: asset.id });
setState(null);
const [max, deposited, balance, allowance] = await Promise.all([
getDepositMaximum(),
@@ -59,7 +60,7 @@ export const useDepositBalances = (asset: Asset | undefined) => {
allowance: allowance ?? initialState.allowance,
});
} catch (err) {
Sentry.captureException(err);
logger.error('get deposit balances', err);
setState(null);
}
}, [asset, getAllowance, getBalance, getDepositMaximum, getDepositedAmount]);
+8 -4
View File
@@ -1,11 +1,10 @@
import type { Token } from '@vegaprotocol/smart-contracts';
import * as Sentry from '@sentry/react';
import { useWeb3React } from '@web3-react/core';
import { useCallback } from 'react';
import { useEthereumConfig } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
import type { Asset } from '@vegaprotocol/assets';
import { addDecimal } from '@vegaprotocol/utils';
import { addDecimal, localLoggerFactory } from '@vegaprotocol/utils';
export const useGetAllowance = (
contract: Token | null,
@@ -18,15 +17,20 @@ export const useGetAllowance = (
if (!contract || !account || !config || !asset) {
return;
}
const logger = localLoggerFactory({ application: 'deposits' });
try {
logger.info('get allowance', {
account,
contractAddress: config.collateral_bridge_contract.address,
});
const res = await contract.allowance(
account,
config.collateral_bridge_contract.address
);
return new BigNumber(addDecimal(res.toString(), asset.decimals));
} catch (err) {
Sentry.captureException(err);
logger.error('get allowance', err);
return;
}
}, [contract, account, config, asset]);
@@ -1,8 +1,7 @@
import { useCallback } from 'react';
import * as Sentry from '@sentry/react';
import BigNumber from 'bignumber.js';
import type { Asset } from '@vegaprotocol/assets';
import { addDecimal } from '@vegaprotocol/utils';
import { addDecimal, localLoggerFactory } from '@vegaprotocol/utils';
import type { CollateralBridge } from '@vegaprotocol/smart-contracts';
export const useGetDepositMaximum = (
@@ -13,14 +12,16 @@ export const useGetDepositMaximum = (
if (!contract || !asset || asset.source.__typename !== 'ERC20') {
return;
}
const logger = localLoggerFactory({ application: 'deposits' });
try {
logger.info('get deposit maximum', { asset: asset.id });
const res = await contract.get_deposit_maximum(
asset.source.contractAddress
);
const max = new BigNumber(addDecimal(res.toString(), asset.decimals));
return max.isEqualTo(0) ? new BigNumber(Infinity) : max;
} catch (err) {
Sentry.captureException(err);
logger.error('get deposit maximum', err);
return;
}
}, [contract, asset]);
@@ -1,10 +1,9 @@
import { useCallback } from 'react';
import * as Sentry from '@sentry/react';
import { ethers } from 'ethers';
import { useEthereumConfig } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
import type { Asset } from '@vegaprotocol/assets';
import { addDecimal } from '@vegaprotocol/utils';
import { addDecimal, localLoggerFactory } from '@vegaprotocol/utils';
import { useWeb3React } from '@web3-react/core';
export const useGetDepositedAmount = (asset: Asset | undefined) => {
@@ -22,8 +21,9 @@ export const useGetDepositedAmount = (asset: Asset | undefined) => {
) {
return;
}
const logger = localLoggerFactory({ application: 'deposits' });
try {
logger.info('get deposited amount', { asset: asset.id });
const abicoder = new ethers.utils.AbiCoder();
const innerHash = ethers.utils.keccak256(
abicoder.encode(['address', 'uint256'], [account, 4])
@@ -41,7 +41,7 @@ export const useGetDepositedAmount = (asset: Asset | undefined) => {
const value = new BigNumber(res, 16).toString();
return new BigNumber(addDecimal(value, asset.decimals));
} catch (err) {
Sentry.captureException(err);
logger.error('get deposited amount', err);
return;
}
}, [provider, asset, config, account]);
+37 -23
View File
@@ -4,6 +4,8 @@ import { Networks } from '../types';
import { useEnvironment } from './use-environment';
import { stripFullStops } from '@vegaprotocol/utils';
const VEGA_DOCS_URL = process.env['NX_VEGA_DOCS_URL'] || '';
type Net = Exclude<Networks, 'CUSTOM'>;
export enum DApp {
Explorer = 'Explorer',
@@ -50,6 +52,25 @@ const Links: { [k in DApp]: DAppLinks } = {
[DApp.Token]: TokenLinks,
};
export const DocsLinks = VEGA_DOCS_URL
? {
NEW_TO_VEGA: `${VEGA_DOCS_URL}/concepts/new-to-vega`,
AUCTION_TYPE_OPENING: `${VEGA_DOCS_URL}/concepts/trading-on-vega/trading-modes#auction-type-opening`,
AUCTION_TYPE_LIQUIDITY_MONITORING: `${VEGA_DOCS_URL}/concepts/trading-on-vega/trading-modes#auction-type-liquidity-monitoring`,
AUCTION_TYPE_PRICE_MONITORING: `${VEGA_DOCS_URL}/concepts/trading-on-vega/trading-modes#auction-type-price-monitoring`,
AUCTION_TYPE_CLOSING: `${VEGA_DOCS_URL}/concepts/trading-on-vega/trading-modes#auction-type-closing`,
STAKING_GUIDE: `${VEGA_DOCS_URL}/concepts/vega-chain/#staking-on-vega`,
REWARDS_GUIDE: `${VEGA_DOCS_URL}/concepts/trading-on-vega/fees-rewards#trading-rewards`,
VEGA_WALLET_CONCEPTS_URL: `${VEGA_DOCS_URL}/concepts/vega-wallet`,
VEGA_WALLET_TOOLS_URL: `${VEGA_DOCS_URL}/tools/vega-wallet`,
PROPOSALS_GUIDE: `${VEGA_DOCS_URL}/tutorials/proposals`,
NODE_OPERATORS: `${VEGA_DOCS_URL}/node-operators`,
LOSS_SOCIALIZATION: `${VEGA_DOCS_URL}/concepts/trading-on-vega/market-protections#loss-socialisation`,
POSITION_RESOLUTION: `${VEGA_DOCS_URL}/concepts/trading-on-vega/market-protections#position-resolution`,
LIQUIDITY: `${VEGA_DOCS_URL}/concepts/liquidity/provision`,
}
: undefined;
export const useLinks = (dapp: DApp, network?: Net) => {
const { VEGA_ENV, VEGA_EXPLORER_URL, VEGA_TOKEN_URL } = useEnvironment();
const fallback = {
@@ -84,19 +105,6 @@ export const useEtherscanLink = () => {
return link;
};
export const useDocsLink = () => {
const { VEGA_DOCS_URL } = useEnvironment();
const baseUrl = trim(VEGA_DOCS_URL, '/');
const link = useCallback(
(url?: string) => `${baseUrl}/${trim(url, '/') || ''}`,
[baseUrl]
);
return link;
};
// Vega blog
export const BLOG = 'https://blog.vega.xyz/';
// Governance pages
export const TOKEN_NEW_MARKET_PROPOSAL = '/proposals/propose/new-market';
export const TOKEN_NEW_NETWORK_PARAM_PROPOSAL =
@@ -130,14 +138,20 @@ export const EXPLORER_ORACLE = '/oracles/:id';
export const ETHERSCAN_ADDRESS = '/address/:hash';
export const ETHERSCAN_TX = '/tx/:hash';
// Console pages
export const CONSOLE_MARKET = '/markets/:marketId';
export const CONSOLE_MARKETS = '/markets/all';
export const CONSOLE_PORTFOLIO = '/portfolio';
export const CONSOLE_LIQUIDITY = 'liquidity/:marketId';
export const ExternalLinks = {
FEEDBACK: 'https://github.com/vegaprotocol/feedback/discussions',
GITHUB: 'https://github.com/vegaprotocol/token-frontend',
DISCORD: 'https://vega.xyz/discord',
GOVERNANCE_PAGE: 'https://vega.xyz/governance',
VALIDATOR_FORUM: 'https://community.vega.xyz/c/mainnet-validator-candidates',
MARGIN_CREDIT_RISK:
'https://vega.xyz/papers/margins-and-credit-risk.pdf#page=7',
VEGA_WALLET_URL: 'https://vega.xyz/wallet',
VEGA_WALLET_HOSTED_URL: 'https://vega-hosted-wallet.on.fleek.co/',
BLOG: 'https://blog.vega.xyz/',
};
// Vega XYZ
export const GET_VEGA_WALLET_URL = 'https://vega.xyz/wallet/';
// Docs
export const DOCS_VEGA_WALLET = '/tools/vega-wallet';
export const TokenStaticLinks = {
PROPOSAL_PAGE: ':tokenUrl/proposals/:proposalId',
UPDATE_PROPOSAL_PAGE: ':tokenUrl/proposals/propose/update-market',
};
+6
View File
@@ -0,0 +1,6 @@
function ReactMarkdown({ children }) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
}
export default ReactMarkdown;
+31
View File
@@ -39,6 +39,37 @@
"passWithNoTests": true
}
},
"storybook": {
"executor": "@nrwl/storybook:storybook",
"options": {
"uiFramework": "@storybook/react",
"port": 4400,
"config": {
"configFolder": "libs/market-info/.storybook"
}
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"build-storybook": {
"executor": "@nrwl/storybook:build",
"outputs": ["{options.outputPath}"],
"options": {
"uiFramework": "@storybook/react",
"outputPath": "dist/storybook/market-info",
"config": {
"configFolder": "libs/market-info/.storybook"
}
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"build-spec": {
"executor": "@nrwl/workspace:run-commands",
"outputs": [],
-1
View File
@@ -1 +0,0 @@
export * from './use-market-oracle';
+1 -2
View File
@@ -1,2 +1 @@
export * from './components';
export * from './hooks';
export * from './lib';

Some files were not shown because too many files have changed in this diff Show More