Compare commits

...
83 changed files with 2773 additions and 1115 deletions
+6 -2
View File
@@ -12,7 +12,8 @@ import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-web
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
import { RouterProvider } from 'react-router-dom';
import { router } from './routes/router-config';
import { useRouterConfig } from './routes/router-config';
import { createBrowserRouter } from 'react-router-dom';
import { t } from '@vegaprotocol/i18n';
import { Suspense } from 'react';
@@ -38,7 +39,10 @@ function App() {
}
>
<Suspense fallback={splashLoading}>
<RouterProvider router={router} fallbackElement={splashLoading} />
<RouterProvider
router={createBrowserRouter(useRouterConfig())}
fallbackElement={splashLoading}
/>
</Suspense>
</NodeGuard>
<NodeSwitcherDialog
@@ -14,7 +14,7 @@ import { Routes } from '../../routes/route-names';
import { NetworkSwitcher } from '@vegaprotocol/environment';
import type { Navigable } from '../../routes/router-config';
import { isNavigable } from '../../routes/router-config';
import { routerConfig } from '../../routes/router-config';
import { useRouterConfig } from '../../routes/router-config';
import { useMemo } from 'react';
import compact from 'lodash/compact';
import { Search } from '../search';
@@ -26,6 +26,7 @@ const routeToNavigationItem = (r: Navigable) => (
);
export const Header = () => {
const routerConfig = useRouterConfig();
const isHome = Boolean(useMatch(Routes.HOME));
const pages = routerConfig[0].children || [];
const mainItems = compact(
+292 -289
View File
@@ -18,7 +18,6 @@ import { t } from '@vegaprotocol/i18n';
import { Routes } from './route-names';
import { NetworkParameters } from './network-parameters';
import type { Params, RouteObject } from 'react-router-dom';
import { createBrowserRouter } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { MarketPage, MarketsPage } from './markets';
import type { ReactNode } from 'react';
@@ -29,7 +28,7 @@ import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
import { remove0x } from '@vegaprotocol/utils';
import { PartyAccountsByAsset } from './parties/id/accounts';
import { Disclaimer } from './pages/disclaimer';
import { FLAGS } from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
import RestrictedPage from './restricted';
export type Navigable = {
@@ -60,311 +59,315 @@ type Route = RouteItem & {
children?: RouteItem[];
};
const partiesRoutes: Route[] = FLAGS.EXPLORER_PARTIES
? [
{
path: Routes.PARTIES,
element: <Party />,
handle: {
name: t('Parties'),
text: t('Parties'),
breadcrumb: () => <Link to={Routes.PARTIES}>{t('Parties')}</Link>,
},
children: [
{
index: true,
element: <Parties />,
},
{
path: ':party',
element: <Party />,
export const useRouterConfig = () => {
const featureFlags = useFeatureFlags((state) => state.flags);
children: [
{
index: true,
element: <PartySingle />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.PARTIES, params.party)}>
{truncateMiddle(params.party as string)}
</Link>
),
const partiesRoutes: Route[] = featureFlags.EXPLORER_PARTIES
? [
{
path: Routes.PARTIES,
element: <Party />,
handle: {
name: t('Parties'),
text: t('Parties'),
breadcrumb: () => <Link to={Routes.PARTIES}>{t('Parties')}</Link>,
},
children: [
{
index: true,
element: <Parties />,
},
{
path: ':party',
element: <Party />,
children: [
{
index: true,
element: <PartySingle />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.PARTIES, params.party)}>
{truncateMiddle(params.party as string)}
</Link>
),
},
},
},
{
path: 'assets',
element: <Party />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.PARTIES, params.party)}>
{truncateMiddle(params.party as string)}
</Link>
),
},
children: [
{
index: true,
element: <PartyAccountsByAsset />,
handle: {
breadcrumb: () => {
return t('Assets');
{
path: 'assets',
element: <Party />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.PARTIES, params.party)}>
{truncateMiddle(params.party as string)}
</Link>
),
},
children: [
{
index: true,
element: <PartyAccountsByAsset />,
handle: {
breadcrumb: () => {
return t('Assets');
},
},
},
},
],
],
},
],
},
],
},
]
: [];
const assetsRoutes: Route[] = featureFlags.EXPLORER_ASSETS
? [
{
path: Routes.ASSETS,
handle: {
name: t('Assets'),
text: t('Assets'),
breadcrumb: () => <Link to={Routes.ASSETS}>{t('Assets')}</Link>,
},
children: [
{
index: true,
element: <AssetsPage />,
},
{
path: ':assetId',
element: <AssetPage />,
handle: {
breadcrumb: (params: Params<string>) => (
<AssetLink assetId={params.assetId as string} />
),
},
],
},
],
},
]
: [];
const assetsRoutes: Route[] = FLAGS.EXPLORER_ASSETS
? [
{
path: Routes.ASSETS,
handle: {
name: t('Assets'),
text: t('Assets'),
breadcrumb: () => <Link to={Routes.ASSETS}>{t('Assets')}</Link>,
},
],
},
children: [
{
index: true,
element: <AssetsPage />,
]
: [];
const genesisRoutes: Route[] = featureFlags.EXPLORER_GENESIS
? [
{
path: Routes.GENESIS,
handle: {
name: t('Genesis'),
text: t('Genesis Parameters'),
breadcrumb: () => (
<Link to={Routes.GENESIS}>{t('Genesis Parameters')}</Link>
),
},
element: <Genesis />,
},
]
: [];
const governanceRoutes: Route[] = featureFlags.EXPLORER_GOVERNANCE
? [
{
path: Routes.GOVERNANCE,
handle: {
name: t('Governance proposals'),
text: t('Governance Proposals'),
breadcrumb: () => (
<Link to={Routes.GOVERNANCE}>{t('Governance Proposals')}</Link>
),
},
element: <Proposals />,
},
]
: [];
const marketsRoutes: Route[] = featureFlags.EXPLORER_MARKETS
? [
{
path: Routes.MARKETS,
handle: {
name: t('Markets'),
text: t('Markets'),
breadcrumb: () => <Link to={Routes.MARKETS}>{t('Markets')}</Link>,
},
children: [
{
index: true,
element: <MarketsPage />,
},
{
path: ':marketId',
element: <MarketPage />,
handle: {
breadcrumb: (params: Params<string>) => (
<MarketLink id={params.marketId as string} />
),
},
},
],
},
]
: [];
const networkParametersRoutes: Route[] =
featureFlags.EXPLORER_NETWORK_PARAMETERS
? [
{
path: ':assetId',
element: <AssetPage />,
path: Routes.NETWORK_PARAMETERS,
handle: {
breadcrumb: (params: Params<string>) => (
<AssetLink assetId={params.assetId as string} />
name: t('NetworkParameters'),
text: t('Network Parameters'),
breadcrumb: () => (
<Link to={Routes.NETWORK_PARAMETERS}>
{t('Network Parameters')}
</Link>
),
},
element: <NetworkParameters />,
},
],
},
]
: [];
]
: [];
const genesisRoutes: Route[] = FLAGS.EXPLORER_GENESIS
? [
{
path: Routes.GENESIS,
handle: {
name: t('Genesis'),
text: t('Genesis Parameters'),
breadcrumb: () => (
<Link to={Routes.GENESIS}>{t('Genesis Parameters')}</Link>
),
},
element: <Genesis />,
},
]
: [];
const governanceRoutes: Route[] = FLAGS.EXPLORER_GOVERNANCE
? [
{
path: Routes.GOVERNANCE,
handle: {
name: t('Governance proposals'),
text: t('Governance Proposals'),
breadcrumb: () => (
<Link to={Routes.GOVERNANCE}>{t('Governance Proposals')}</Link>
),
},
element: <Proposals />,
},
]
: [];
const marketsRoutes: Route[] = FLAGS.EXPLORER_MARKETS
? [
{
path: Routes.MARKETS,
handle: {
name: t('Markets'),
text: t('Markets'),
breadcrumb: () => <Link to={Routes.MARKETS}>{t('Markets')}</Link>,
},
children: [
{
index: true,
element: <MarketsPage />,
const validators: Route[] = featureFlags.EXPLORER_VALIDATORS
? [
{
path: Routes.VALIDATORS,
handle: {
name: t('Validators'),
text: t('Validators'),
breadcrumb: () => (
<Link to={Routes.VALIDATORS}>{t('Validators')}</Link>
),
},
{
path: ':marketId',
element: <MarketPage />,
handle: {
breadcrumb: (params: Params<string>) => (
<MarketLink id={params.marketId as string} />
),
element: <ValidatorsPage />,
},
]
: [];
const linkTo = (...segments: (string | undefined)[]) =>
compact(segments).join('/');
const routerConfig: Route[] = [
{
path: Routes.HOME,
element: <Layout />,
handle: {
name: t('Home'),
text: t('Home'),
breadcrumb: () => <Link to={Routes.HOME}>{t('Home')}</Link>,
},
errorElement: <ErrorBoundary />,
children: [
{
index: true,
element: <Home />,
},
{
path: Routes.TX,
handle: {
name: t('Txs'),
text: t('Transactions'),
breadcrumb: () => <Link to={Routes.TX}>{t('Transactions')}</Link>,
},
children: [
{
path: ':txHash',
element: <Tx />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.TX, params.txHash)}>
{truncateMiddle(remove0x(params.txHash as string))}
</Link>
),
},
},
{
index: true,
element: <TxsList />,
},
],
},
{
path: Routes.BLOCKS,
handle: {
name: t('Blocks'),
text: t('Blocks'),
breadcrumb: () => <Link to={Routes.BLOCKS}>{t('Blocks')}</Link>,
},
],
},
]
: [];
const networkParametersRoutes: Route[] = FLAGS.EXPLORER_NETWORK_PARAMETERS
? [
{
path: Routes.NETWORK_PARAMETERS,
handle: {
name: t('NetworkParameters'),
text: t('Network Parameters'),
breadcrumb: () => (
<Link to={Routes.NETWORK_PARAMETERS}>
{t('Network Parameters')}
</Link>
),
element: <BlockPage />,
children: [
{
index: true,
element: <Blocks />,
},
{
path: ':block',
element: <Block />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.BLOCKS, params.block)}>
{params.block}
</Link>
),
},
},
],
},
element: <NetworkParameters />,
},
]
: [];
const validators: Route[] = FLAGS.EXPLORER_VALIDATORS
? [
{
path: Routes.VALIDATORS,
handle: {
name: t('Validators'),
text: t('Validators'),
breadcrumb: () => (
<Link to={Routes.VALIDATORS}>{t('Validators')}</Link>
),
{
path: Routes.ORACLES,
handle: {
name: t('Oracles'),
text: t('Oracles'),
breadcrumb: () => <Link to={Routes.ORACLES}>{t('Oracles')}</Link>,
},
element: <OraclePage />,
children: [
{
index: true,
element: <Oracles />,
},
{
path: ':id',
element: <Oracle />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.ORACLES, params.id)}>
{truncateMiddle(params.id as string)}
</Link>
),
},
},
],
},
element: <ValidatorsPage />,
},
]
: [];
const linkTo = (...segments: (string | undefined)[]) =>
compact(segments).join('/');
export const routerConfig: Route[] = [
{
path: Routes.HOME,
element: <Layout />,
handle: {
name: t('Home'),
text: t('Home'),
breadcrumb: () => <Link to={Routes.HOME}>{t('Home')}</Link>,
{
path: Routes.DISCLAIMER,
element: <Disclaimer />,
handle: {
name: t('Disclaimer'),
text: t('Disclaimer'),
breadcrumb: () => (
<Link to={Routes.DISCLAIMER}>{t('Disclaimer')}</Link>
),
},
},
...partiesRoutes,
...assetsRoutes,
...genesisRoutes,
...governanceRoutes,
...marketsRoutes,
...networkParametersRoutes,
...validators,
],
},
errorElement: <ErrorBoundary />,
children: [
{
index: true,
element: <Home />,
{
path: Routes.RESTRICTED,
element: <RestrictedPage />,
handle: {
name: t('Restricted'),
text: t('Restricted'),
},
{
path: Routes.TX,
handle: {
name: t('Txs'),
text: t('Transactions'),
breadcrumb: () => <Link to={Routes.TX}>{t('Transactions')}</Link>,
},
children: [
{
path: ':txHash',
element: <Tx />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.TX, params.txHash)}>
{truncateMiddle(remove0x(params.txHash as string))}
</Link>
),
},
},
{
index: true,
element: <TxsList />,
},
],
},
{
path: Routes.BLOCKS,
handle: {
name: t('Blocks'),
text: t('Blocks'),
breadcrumb: () => <Link to={Routes.BLOCKS}>{t('Blocks')}</Link>,
},
element: <BlockPage />,
children: [
{
index: true,
element: <Blocks />,
},
{
path: ':block',
element: <Block />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.BLOCKS, params.block)}>
{params.block}
</Link>
),
},
},
],
},
{
path: Routes.ORACLES,
handle: {
name: t('Oracles'),
text: t('Oracles'),
breadcrumb: () => <Link to={Routes.ORACLES}>{t('Oracles')}</Link>,
},
element: <OraclePage />,
children: [
{
index: true,
element: <Oracles />,
},
{
path: ':id',
element: <Oracle />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.ORACLES, params.id)}>
{truncateMiddle(params.id as string)}
</Link>
),
},
},
],
},
{
path: Routes.DISCLAIMER,
element: <Disclaimer />,
handle: {
name: t('Disclaimer'),
text: t('Disclaimer'),
breadcrumb: () => (
<Link to={Routes.DISCLAIMER}>{t('Disclaimer')}</Link>
),
},
},
...partiesRoutes,
...assetsRoutes,
...genesisRoutes,
...governanceRoutes,
...marketsRoutes,
...networkParametersRoutes,
...validators,
],
},
{
path: Routes.RESTRICTED,
element: <RestrictedPage />,
handle: {
name: t('Restricted'),
text: t('Restricted'),
},
},
];
export const router = createBrowserRouter(routerConfig);
];
return routerConfig;
};
+17 -9
View File
@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/react';
import { toBigNum } from '@vegaprotocol/utils';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useEagerConnect } from '@vegaprotocol/wallet';
import { FLAGS, useEnvironment } from '@vegaprotocol/environment';
import { useFeatureFlags, useEnvironment } from '@vegaprotocol/environment';
import { useWeb3React } from '@web3-react/core';
import React, { Suspense } from 'react';
import { useTranslation } from 'react-i18next';
@@ -15,21 +15,23 @@ import {
} from './contexts/app-state/app-state-context';
import { useContracts } from './contexts/contracts/contracts-context';
import { useRefreshAssociatedBalances } from './hooks/use-refresh-associated-balances';
import { Connectors } from './lib/vega-connectors';
import { useConnectors } from './lib/vega-connectors';
import { useSearchParams } from 'react-router-dom';
const useVegaWalletEagerConnect = () => {
const vegaConnecting = useEagerConnect(Connectors);
const connectors = useConnectors();
const vegaConnecting = useEagerConnect(connectors);
const { pubKey, connect } = useVegaWallet();
const [searchParams] = useSearchParams();
const [query] = React.useState(searchParams.get('address'));
if (query && !pubKey) {
connect(Connectors['view']);
connect(connectors.view);
}
return vegaConnecting;
};
export const AppLoader = ({ children }: { children: React.ReactElement }) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const { t } = useTranslation();
const { account } = useWeb3React();
const { VEGA_URL } = useEnvironment();
@@ -79,10 +81,16 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
}
};
if (!FLAGS.GOVERNANCE_NETWORK_DOWN) {
if (!featureFlags.GOVERNANCE_NETWORK_DOWN) {
run();
}
}, [token, appDispatch, staking, vesting]);
}, [
token,
appDispatch,
staking,
vesting,
featureFlags.GOVERNANCE_NETWORK_DOWN,
]);
React.useEffect(() => {
if (account && pubKey) {
@@ -147,16 +155,16 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
};
// Only begin polling if network limits flag is set, as this is a new API not yet on mainnet 7/3/22
if (FLAGS.GOVERNANCE_NETWORK_LIMITS) {
if (featureFlags.GOVERNANCE_NETWORK_LIMITS) {
getNetworkLimits();
}
return () => {
stopPoll();
};
}, [appDispatch, VEGA_URL, t]);
}, [appDispatch, VEGA_URL, t, featureFlags.GOVERNANCE_NETWORK_LIMITS]);
if (FLAGS.GOVERNANCE_NETWORK_DOWN) {
if (featureFlags.GOVERNANCE_NETWORK_DOWN) {
return (
<Splash>
<SplashError />
@@ -7,16 +7,16 @@ import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
import { Connectors } from '../../lib/vega-connectors';
import { useConnectors } from '../../lib/vega-connectors';
import { RiskMessage } from './risk-message';
export const VegaWalletDialogs = () => {
const { appState, appDispatch } = useAppState();
const connectors = useConnectors();
return (
<>
<VegaConnectDialog
connectors={Connectors}
connectors={connectors}
riskMessage={<RiskMessage />}
/>
@@ -30,7 +30,7 @@ export const VegaWalletDialogs = () => {
}
/>
<ViewAsDialog connector={Connectors.view} />
<ViewAsDialog connector={connectors.view} />
</>
);
};
+14 -9
View File
@@ -1,4 +1,5 @@
import { FLAGS } from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { useMemo } from 'react';
import {
JsonRpcConnector,
ViewConnector,
@@ -13,13 +14,17 @@ export const jsonRpc = new JsonRpcConnector();
export const injected = new InjectedConnector();
export const view = new ViewConnector(urlParams.get('address'));
export const snap = FLAGS.METAMASK_SNAPS
? new SnapConnector(DEFAULT_SNAP_ID)
: undefined;
export const snap = new SnapConnector(DEFAULT_SNAP_ID);
export const Connectors = {
injected,
jsonRpc,
view,
snap,
export const useConnectors = () => {
const featureFlags = useFeatureFlags((state) => state.flags);
return useMemo(
() => ({
injected,
jsonRpc,
view,
snap: featureFlags.METAMASK_SNAPS ? snap : undefined,
}),
[featureFlags.METAMASK_SNAPS]
);
};
+5 -4
View File
@@ -12,7 +12,7 @@ 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, FLAGS } from '@vegaprotocol/environment';
import { ExternalLinks, useFeatureFlags } from '@vegaprotocol/environment';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
@@ -175,6 +175,7 @@ export const ValidatorDetailsLink = ({
};
const GovernanceHome = ({ name }: RouteChildProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
useDocumentTitle(name);
const { t } = useTranslation();
const {
@@ -186,9 +187,9 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
variables: {
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
includeNewMarketProductFields: !!featureFlags.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!featureFlags.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!featureFlags.REFERRALS,
},
});
@@ -19,7 +19,7 @@ import {
mockWalletContext,
createUserVoteQueryMock,
} from '../../test-helpers/mocks';
import { FLAGS } from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { BrowserRouter } from 'react-router-dom';
import { VoteState } from '../vote-details/use-user-vote';
import { useNewTransferProposalDetails } from '@vegaprotocol/proposals';
@@ -62,8 +62,7 @@ describe('Proposal header', () => {
jest.clearAllMocks();
});
it('Renders New market proposal', () => {
const mockedFlags = jest.mocked(FLAGS);
mockedFlags.SUCCESSOR_MARKETS = true;
useFeatureFlags.setState({ flags: { SUCCESSOR_MARKETS: true } });
renderComponent(
generateProposal({
rationale: {
@@ -12,7 +12,7 @@ import {
useNewTransferProposalDetails,
useSuccessorMarketProposalDetails,
} from '@vegaprotocol/proposals';
import { FLAGS } from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
import Routes from '../../../routes';
import { Link } from 'react-router-dom';
import type { VoteState } from '../vote-details/use-user-vote';
@@ -28,6 +28,7 @@ export const ProposalHeader = ({
isListItem?: boolean;
voteState?: VoteState | null;
}) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const { t } = useTranslation();
const change = proposal?.terms.change;
@@ -54,13 +55,14 @@ export const ProposalHeader = ({
switch (change?.__typename) {
case 'NewMarket': {
proposalType =
FLAGS.PRODUCT_PERPETUALS && change?.instrument?.product?.__typename
featureFlags.PRODUCT_PERPETUALS &&
change?.instrument?.product?.__typename
? `NewMarket${change?.instrument?.product?.__typename}`
: 'NewMarket';
fallbackTitle = t('NewMarketProposal');
details = (
<>
{FLAGS.SUCCESSOR_MARKETS && (
{featureFlags.SUCCESSOR_MARKETS && (
<SuccessorCode proposalId={proposal?.id} />
)}
<span>
@@ -82,13 +84,13 @@ export const ProposalHeader = ({
}
case 'UpdateMarketState': {
proposalType =
FLAGS.UPDATE_MARKET_STATE && change?.updateType
featureFlags.UPDATE_MARKET_STATE && change?.updateType
? t(change.updateType)
: 'UpdateMarketState';
fallbackTitle = t('UpdateMarketStateProposal');
details = (
<span>
{FLAGS.UPDATE_MARKET_STATE &&
{featureFlags.UPDATE_MARKET_STATE &&
change?.market?.id &&
change.updateType ? (
<>
@@ -177,14 +179,14 @@ export const ProposalHeader = ({
case 'NewTransfer':
proposalType = 'NewTransfer';
fallbackTitle = t('NewTransferProposal');
details = FLAGS.GOVERNANCE_TRANSFERS ? (
details = featureFlags.GOVERNANCE_TRANSFERS ? (
<NewTransferSummary proposalId={proposal?.id} />
) : null;
break;
case 'CancelTransfer':
proposalType = 'CancelTransfer';
fallbackTitle = t('CancelTransferProposal');
details = FLAGS.GOVERNANCE_TRANSFERS ? (
details = featureFlags.GOVERNANCE_TRANSFERS ? (
<CancelTransferSummary proposalId={proposal?.id} />
) : null;
break;
@@ -26,7 +26,7 @@ import {
ProposalCancelTransferDetails,
ProposalTransferDetails,
} from '../proposal-transfer';
import { FLAGS } from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
export interface ProposalProps {
@@ -53,6 +53,7 @@ export const Proposal = ({
originalMarketProposalRestData,
mostRecentlyEnactedAssociatedMarketProposal,
}: ProposalProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const { t } = useTranslation();
const { submit, Dialog, finalizedVote, transaction } = useVoteSubmit();
const { voteState, voteDatetime } = useUserVote(proposal?.id, finalizedVote);
@@ -132,7 +133,7 @@ export const Proposal = ({
}
// Show governance transfer details only if the GOVERNANCE_TRANSFERS flag is on.
const governanceTransferDetails = FLAGS.GOVERNANCE_TRANSFERS && (
const governanceTransferDetails = featureFlags.GOVERNANCE_TRANSFERS && (
<>
{proposal.terms.change.__typename === 'NewTransfer' && (
/** Governance New Transfer Details */
@@ -15,10 +15,11 @@ import {
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { useParentMarketIdQuery } from '@vegaprotocol/markets';
import { FLAGS } from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
export const ProposalContainer = () => {
const featureFlags = useFeatureFlags((state) => state.flags);
const [
mostRecentlyEnactedAssociatedMarketProposal,
setMostRecentlyEnactedAssociatedMarketProposal,
@@ -59,9 +60,9 @@ export const ProposalContainer = () => {
errorPolicy: 'ignore',
variables: {
proposalId: params.proposalId || '',
includeNewMarketProductField: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketState: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralProgram: !!FLAGS.REFERRALS,
includeNewMarketProductField: !!featureFlags.PRODUCT_PERPETUALS,
includeUpdateMarketState: !!featureFlags.UPDATE_MARKET_STATE,
includeUpdateReferralProgram: !!featureFlags.REFERRALS,
},
skip: !params.proposalId,
});
@@ -120,7 +121,7 @@ export const ProposalContainer = () => {
variables: {
marketId: marketData?.id || '',
},
skip: !FLAGS.SUCCESSOR_MARKETS || !isSuccessor || !marketData?.id,
skip: !featureFlags.SUCCESSOR_MARKETS || !isSuccessor || !marketData?.id,
});
const {
@@ -133,7 +134,7 @@ export const ProposalContainer = () => {
variables: {
marketId: parentMarketId?.market?.parentMarketID || '',
skip:
!FLAGS.SUCCESSOR_MARKETS ||
!featureFlags.SUCCESSOR_MARKETS ||
!isSuccessor ||
!parentMarketId?.market?.parentMarketID,
},
@@ -17,7 +17,7 @@ import {
} from './__generated__/Proposals';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { useProtocolUpgradeProposalsQuery } from '@vegaprotocol/proposals';
import { FLAGS } from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
export function getNotRejectedProposals(data?: ProposalFieldsFragment[]) {
return flow([
@@ -43,15 +43,16 @@ export function getNotRejectedProtocolUpgradeProposals<
}
export const ProposalsContainer = () => {
const featureFlags = useFeatureFlags((state) => state.flags);
const { t } = useTranslation();
const { data, loading, error } = useProposalsQuery({
pollInterval: 5000,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
variables: {
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
includeNewMarketProductFields: !!featureFlags.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!featureFlags.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!featureFlags.REFERRALS,
},
});
@@ -10,7 +10,7 @@ import { removePaginationWrapper } from '@vegaprotocol/utils';
import flow from 'lodash/flow';
import orderBy from 'lodash/orderBy';
import { ProposalState } from '@vegaprotocol/types';
import { FLAGS } from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
const orderByDate = (arr: ProposalFieldsFragment[]) =>
orderBy(
@@ -33,15 +33,16 @@ export function getRejectedProposals(data?: ProposalFieldsFragment[] | null) {
}
export const RejectedProposalsContainer = () => {
const featureFlags = useFeatureFlags((state) => state.flags);
const { t } = useTranslation();
const { data, loading, error } = useProposalsQuery({
pollInterval: 5000,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
variables: {
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
includeNewMarketProductFields: !!featureFlags.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!featureFlags.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!featureFlags.REFERRALS,
},
});
+4 -1
View File
@@ -24,4 +24,7 @@ NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
NX_REFERRALS=true
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
+3
View File
@@ -28,3 +28,6 @@ NX_REFERRALS=true
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
@@ -18,7 +18,7 @@ import {
MarketSuccessorProposalBanner,
MarketTerminationBanner,
} from '../../components/market-banner';
import { FLAGS } from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary';
@@ -35,6 +35,7 @@ const MainGrid = memo(
marketId: string;
pinnedAsset?: PinnedAsset;
}) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const t = useT();
const { data: market } = useMarket(marketId);
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'top' });
@@ -61,10 +62,10 @@ const MainGrid = memo(
id="chart"
overflowHidden
name={t('Chart')}
menu={<TradingViews.candles.menu />}
menu={<TradingViews.chart.menu />}
>
<ErrorBoundary feature="chart">
<TradingViews.candles.component marketId={marketId} />
<TradingViews.chart.component marketId={marketId} />
</ErrorBoundary>
</Tab>
<Tab id="depth" name={t('Depth')}>
@@ -165,7 +166,7 @@ const MainGrid = memo(
<TradingViews.orders.component />
</ErrorBoundary>
</Tab>
{FLAGS.STOP_ORDERS ? (
{featureFlags.STOP_ORDERS ? (
<Tab id="stop-orders" name={t('Stop orders')}>
<ErrorBoundary feature="stop-orders">
<TradingViews.stopOrders.component />
@@ -196,6 +197,7 @@ const MainGrid = memo(
MainGrid.displayName = 'MainGrid';
export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const wrapperClasses = classNames(
'h-full grid',
'grid-rows-[min-content_1fr]'
@@ -204,7 +206,7 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
return (
<div className={wrapperClasses}>
<div>
{FLAGS.SUCCESSOR_MARKETS && (
{featureFlags.SUCCESSOR_MARKETS && (
<>
<MarketSuccessorBanner market={market} />
<MarketSuccessorProposalBanner marketId={market?.id} />
@@ -4,7 +4,6 @@ import { OracleBanner } from '@vegaprotocol/markets';
import { useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import classNames from 'classnames';
import { FLAGS } from '@vegaprotocol/environment';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useT } from '../../lib/use-t';
import {
@@ -15,6 +14,7 @@ import {
import { ErrorBoundary } from '../../components/error-boundary';
import { type TradingView } from './trade-views';
import { TradingViews } from './trade-views';
import { useFeatureFlags } from '@vegaprotocol/environment';
interface TradePanelsProps {
market: Market | null;
@@ -22,7 +22,8 @@ interface TradePanelsProps {
}
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
const [view, setView] = useState<TradingView>('candles');
const featureFlags = useFeatureFlags((state) => state.flags);
const [view, setView] = useState<TradingView>('chart');
const renderView = () => {
const Component = TradingViews[view].component;
@@ -49,7 +50,7 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
const Menu = viewCfg.menu;
return (
<div className="flex gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
<div className="flex items-center justify-end gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
<Menu />
</div>
);
@@ -61,7 +62,7 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
return (
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
<div>
{FLAGS.SUCCESSOR_MARKETS && (
{featureFlags.SUCCESSOR_MARKETS && (
<>
<MarketSuccessorBanner market={market} />
<MarketSuccessorProposalBanner marketId={market?.id} />
@@ -148,7 +149,7 @@ const useViewLabel = (view: TradingView) => {
const t = useT();
const labels = {
candles: t('Candles'),
chart: t('Chart'),
depth: t('Depth'),
liquidity: t('Liquidity'),
funding: t('Funding'),
@@ -1,8 +1,4 @@
import { DepthChartContainer } from '@vegaprotocol/market-depth';
import {
CandlesChartContainer,
CandlesMenu,
} from '@vegaprotocol/candles-chart';
import { Filter, OpenOrdersMenu } from '@vegaprotocol/orders';
import { TradesContainer } from '../../components/trades-container';
import { OrderbookContainer } from '../../components/orderbook-container';
@@ -16,13 +12,14 @@ import { OrdersContainer } from '../../components/orders-container';
import { StopOrdersContainer } from '../../components/stop-orders-container';
import { AccountsMenu } from '../../components/accounts-menu';
import { PositionsMenu } from '../../components/positions-menu';
import { ChartContainer, ChartMenu } from '../../components/chart-container';
export type TradingView = keyof typeof TradingViews;
export const TradingViews = {
candles: {
component: CandlesChartContainer,
menu: CandlesMenu,
chart: {
component: ChartContainer,
menu: ChartMenu,
},
depth: {
component: DepthChartContainer,
@@ -0,0 +1,66 @@
import { render, screen } from '@testing-library/react';
import { ChartContainer } from './chart-container';
import { useChartSettingsStore } from './use-chart-settings';
import { useEnvironment } from '@vegaprotocol/environment';
jest.mock('@vegaprotocol/candles-chart', () => ({
...jest.requireActual('@vegaprotocol/candles-chart'),
CandlesChartContainer: ({ marketId }: { marketId: string }) => (
<div data-testid="pennant">{marketId}</div>
),
}));
jest.mock('@vegaprotocol/trading-view', () => ({
...jest.requireActual('@vegaprotocol/trading-view'),
TradingViewContainer: ({ marketId }: { marketId: string }) => (
<div data-testid="tradingview">{marketId}</div>
),
}));
describe('ChartContainer', () => {
it('renders pennant if no library path is set', () => {
useChartSettingsStore.setState({
chartlib: 'tradingview',
});
useEnvironment.setState({
CHARTING_LIBRARY_PATH: undefined,
CHARTING_LIBRARY_HASH: undefined,
});
const marketId = 'market-id';
render(<ChartContainer marketId={marketId} />);
expect(screen.getByTestId('pennant')).toHaveTextContent(marketId);
});
it('renders trading view if library path is set', () => {
useChartSettingsStore.setState({
chartlib: 'tradingview',
});
useEnvironment.setState({
CHARTING_LIBRARY_PATH: 'dummy-path',
CHARTING_LIBRARY_HASH: 'hash',
});
const marketId = 'market-id';
render(<ChartContainer marketId={marketId} />);
expect(screen.getByTestId('tradingview')).toHaveTextContent(marketId);
});
it('renders pennant chart if stored in settings', () => {
useChartSettingsStore.setState({
chartlib: 'pennant',
});
const marketId = 'market-id';
render(<ChartContainer marketId={marketId} />);
expect(screen.getByTestId('pennant')).toHaveTextContent(marketId);
});
});
@@ -0,0 +1,120 @@
import invert from 'lodash/invert';
import { type Interval } from '@vegaprotocol/types';
import {
TradingViewContainer,
ALLOWED_TRADINGVIEW_HOSTNAMES,
TRADINGVIEW_INTERVAL_MAP,
} from '@vegaprotocol/trading-view';
import {
CandlesChartContainer,
PENNANT_INTERVAL_MAP,
} from '@vegaprotocol/candles-chart';
import { useEnvironment } from '@vegaprotocol/environment';
import { useChartSettings, STUDY_SIZE } from './use-chart-settings';
/**
* Renders either the pennant chart or the tradingview chart
*/
export const ChartContainer = ({ marketId }: { marketId: string }) => {
const { CHARTING_LIBRARY_PATH, CHARTING_LIBRARY_HASH } = useEnvironment();
const {
chartlib,
interval,
chartType,
overlays,
studies,
studySizes,
tradingViewStudies,
setInterval,
setStudies,
setStudySizes,
setOverlays,
setTradingViewStudies,
} = useChartSettings();
const pennantChart = (
<CandlesChartContainer
marketId={marketId}
interval={toPennantInterval(interval)}
chartType={chartType}
overlays={overlays}
studies={studies}
studySizes={studySizes}
setStudySizes={setStudySizes}
setStudies={setStudies}
setOverlays={setOverlays}
defaultStudySize={STUDY_SIZE}
/>
);
if (!ALLOWED_TRADINGVIEW_HOSTNAMES.includes(window.location.hostname)) {
return pennantChart;
}
if (!CHARTING_LIBRARY_PATH || !CHARTING_LIBRARY_HASH) {
return pennantChart;
}
switch (chartlib) {
case 'tradingview': {
return (
<TradingViewContainer
libraryPath={CHARTING_LIBRARY_PATH}
libraryHash={CHARTING_LIBRARY_HASH}
marketId={marketId}
interval={toTradingViewResolution(interval)}
studies={tradingViewStudies}
onIntervalChange={(newInterval) => {
setInterval(fromTradingViewResolution(newInterval));
}}
onAutoSaveNeeded={(data: { studies: string[] }) => {
setTradingViewStudies(data.studies);
}}
/>
);
}
case 'pennant': {
return pennantChart;
}
default: {
throw new Error('invalid chart lib');
}
}
};
const toTradingViewResolution = (interval: Interval) => {
const resolution = TRADINGVIEW_INTERVAL_MAP[interval];
if (!resolution) {
throw new Error(
`failed to convert interval: ${interval} to valid resolution`
);
}
return resolution;
};
const fromTradingViewResolution = (resolution: string) => {
const interval = invert(TRADINGVIEW_INTERVAL_MAP)[resolution];
if (!interval) {
throw new Error(
`failed to convert resolution: ${resolution} to valid interval`
);
}
return interval as Interval;
};
const toPennantInterval = (interval: Interval) => {
const pennantInterval = PENNANT_INTERVAL_MAP[interval];
if (!pennantInterval) {
throw new Error(
`failed to convert interval: ${interval} to valid pennant interval`
);
}
return pennantInterval;
};
@@ -0,0 +1,140 @@
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ChartMenu } from './chart-menu';
import {
useChartSettingsStore,
DEFAULT_CHART_SETTINGS,
} from './use-chart-settings';
import { Overlay, Study, overlayLabels, studyLabels } from 'pennant';
import { useEnvironment } from '@vegaprotocol/environment';
describe('ChartMenu', () => {
it('doesnt show trading view option if library path undefined', () => {
useEnvironment.setState({ CHARTING_LIBRARY_PATH: undefined });
render(<ChartMenu />);
expect(
screen.queryByRole('button', { name: 'TradingView' })
).not.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: 'Vega chart' })
).not.toBeInTheDocument();
});
it('can switch between charts if library path', async () => {
useEnvironment.setState({ CHARTING_LIBRARY_PATH: 'dummy' });
render(<ChartMenu />);
await userEvent.click(screen.getByRole('button', { name: 'TradingView' }));
expect(useChartSettingsStore.getState().chartlib).toEqual('tradingview');
await userEvent.click(screen.getByRole('button', { name: 'Vega chart' }));
expect(useChartSettingsStore.getState().chartlib).toEqual('pennant');
});
describe('tradingview', () => {
beforeEach(() => {
useEnvironment.setState({ CHARTING_LIBRARY_PATH: 'dummy-path' });
// clear store each time to avoid conditional testing of defaults
useChartSettingsStore.setState({
chartlib: 'tradingview',
});
});
it('only shows chartlib switch and attribution', () => {
render(<ChartMenu />);
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(1);
expect(buttons[0]).toHaveTextContent('Vega chart');
expect(screen.getByText('Chart by')).toBeInTheDocument();
});
});
describe('pennant', () => {
const openDropdown = async () => {
await userEvent.click(
screen.getByRole('button', {
name: 'Indicators',
})
);
};
beforeEach(() => {
// clear store each time to avoid conditional testing of defaults
useChartSettingsStore.setState({
chartlib: 'pennant',
overlays: [],
studies: [],
});
});
it.each(Object.values(Overlay))('can set %s overlay', async (overlay) => {
render(<ChartMenu />);
await openDropdown();
const menu = within(await screen.findByRole('menu'));
await userEvent.click(menu.getByText(overlayLabels[overlay as Overlay]));
// re-open the dropdown
await openDropdown();
expect(
screen.getByText(overlayLabels[overlay as Overlay])
).toHaveAttribute('data-state', 'checked');
});
it.each(Object.values(Study))('can set %s study', async (study) => {
render(<ChartMenu />);
await openDropdown();
const menu = within(await screen.findByRole('menu'));
await userEvent.click(menu.getByText(studyLabels[study as Study]));
// re-open the dropdown
await openDropdown();
expect(screen.getByText(studyLabels[study as Study])).toHaveAttribute(
'data-state',
'checked'
);
});
it('should render with the correct default studies and overlays', async () => {
useChartSettingsStore.setState({
...DEFAULT_CHART_SETTINGS,
chartlib: 'pennant',
});
render(<ChartMenu />);
await userEvent.click(
screen.getByRole('button', {
name: 'Indicators',
})
);
const menu = within(await screen.findByRole('menu'));
expect(menu.getByText(studyLabels.volume)).toHaveAttribute(
'data-state',
'checked'
);
expect(menu.getByText(studyLabels.macd)).toHaveAttribute(
'data-state',
'checked'
);
expect(menu.getByText(overlayLabels.movingAverage)).toHaveAttribute(
'data-state',
'checked'
);
});
});
});
@@ -1,14 +1,12 @@
import 'pennant/dist/style.css';
import {
ChartType,
Interval,
Overlay,
Study,
chartTypeLabels,
intervalLabels,
overlayLabels,
studyLabels,
} from 'pennant';
import { Trans } from 'react-i18next';
import {
TradingButton,
TradingDropdown,
@@ -20,10 +18,21 @@ import {
TradingDropdownTrigger,
Icon,
} from '@vegaprotocol/ui-toolkit';
import { type IconName } from '@blueprintjs/icons';
import { IconNames } from '@blueprintjs/icons';
import { useCandlesChartSettings } from './use-candles-chart-settings';
import { useT } from './use-t';
import { Interval } from '@vegaprotocol/types';
import { useEnvironment } from '@vegaprotocol/environment';
import { ALLOWED_TRADINGVIEW_HOSTNAMES } from '@vegaprotocol/trading-view';
import { IconNames, type IconName } from '@blueprintjs/icons';
import { useChartSettings } from './use-chart-settings';
import { useT } from '../../lib/use-t';
const INTERVALS = [
Interval.INTERVAL_I1M,
Interval.INTERVAL_I5M,
Interval.INTERVAL_I15M,
Interval.INTERVAL_I1H,
Interval.INTERVAL_I6H,
Interval.INTERVAL_I1D,
];
const chartTypeIcon = new Map<ChartType, IconName>([
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
@@ -32,30 +41,46 @@ const chartTypeIcon = new Map<ChartType, IconName>([
[ChartType.OHLC, IconNames.WATERFALL_CHART],
]);
export const CandlesMenu = () => {
export const ChartMenu = () => {
const { CHARTING_LIBRARY_PATH } = useEnvironment();
const {
chartlib,
interval,
chartType,
studies,
overlays,
setChartlib,
setInterval,
setType,
setStudies,
setOverlays,
} = useCandlesChartSettings();
} = useChartSettings();
const t = useT();
const triggerClasses = 'text-xs';
const contentAlign = 'end';
const triggerClasses = 'text-xs';
const triggerButtonProps = { size: 'extra-small' } as const;
return (
const isPennant = chartlib === 'pennant';
const commonMenuItems = (
<TradingButton
onClick={() => {
setChartlib(isPennant ? 'tradingview' : 'pennant');
}}
size="extra-small"
>
{isPennant ? 'TradingView' : t('Vega chart')}
</TradingButton>
);
const pennantMenuItems = (
<>
<TradingDropdown
trigger={
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
{t('Interval: {{interval}}', {
interval: intervalLabels[interval],
interval: t(interval),
})}
</TradingButton>
</TradingDropdownTrigger>
@@ -68,13 +93,13 @@ export const CandlesMenu = () => {
setInterval(value as Interval);
}}
>
{Object.values(Interval).map((timeInterval) => (
{INTERVALS.map((timeInterval) => (
<TradingDropdownRadioItem
key={timeInterval}
inset
value={timeInterval}
>
{intervalLabels[timeInterval]}
{t(timeInterval)}
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
))}
@@ -158,4 +183,50 @@ export const CandlesMenu = () => {
</TradingDropdown>
</>
);
const tradingViewMenuItems = (
<p className="text-xs mr-2 whitespace-nowrap">
<Trans
i18nKey="Chart by <0>TradingView</0>"
components={[
// eslint-disable-next-line
<a
className="underline"
target="_blank"
href="https://www.tradingview.com"
/>,
]}
/>
</p>
);
if (!ALLOWED_TRADINGVIEW_HOSTNAMES.includes(window.location.hostname)) {
return pennantMenuItems;
}
if (!CHARTING_LIBRARY_PATH) {
return pennantMenuItems;
}
switch (chartlib) {
case 'tradingview': {
return (
<>
{tradingViewMenuItems}
{commonMenuItems}
</>
);
}
case 'pennant': {
return (
<>
{pennantMenuItems}
{commonMenuItems}
</>
);
}
default: {
throw new Error('invalid chart lib');
}
}
};
@@ -0,0 +1,2 @@
export { ChartContainer } from './chart-container';
export { ChartMenu } from './chart-menu';
@@ -1,18 +1,23 @@
import { getValidItem, getValidSubset } from '@vegaprotocol/react-helpers';
import { ChartType, Interval, Study } from 'pennant';
import { Overlay } from 'pennant';
import { ChartType, Overlay, Study } from 'pennant';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { Interval } from '@vegaprotocol/types';
import { getValidItem, getValidSubset } from '@vegaprotocol/react-helpers';
type StudySizes = { [S in Study]?: number };
export type Chartlib = 'pennant' | 'tradingview';
interface StoredSettings {
chartlib: Chartlib;
// For interval we use the enum from @vegaprotocol/types, this is to make mapping between different
// chart types easier and more consistent
interval: Interval;
type: ChartType;
overlays: Overlay[];
studies: Study[];
studySizes: StudySizes;
tradingViewStudies: string[];
}
export const STUDY_SIZE = 90;
@@ -25,20 +30,24 @@ const STUDY_ORDER: Study[] = [
];
export const DEFAULT_CHART_SETTINGS = {
interval: Interval.I15M,
chartlib: 'pennant' as const,
interval: Interval.INTERVAL_I15M,
type: ChartType.CANDLE,
overlays: [Overlay.MOVING_AVERAGE],
studies: [Study.MACD, Study.VOLUME],
studySizes: {},
tradingViewStudies: ['Volume'],
};
export const useCandlesChartSettingsStore = create<
export const useChartSettingsStore = create<
StoredSettings & {
setType: (type: ChartType) => void;
setInterval: (interval: Interval) => void;
setOverlays: (overlays?: Overlay[]) => void;
setStudies: (studies?: Study[]) => void;
setStudySizes: (sizes: number[]) => void;
setChartlib: (lib: Chartlib) => void;
setTradingViewStudies: (studies: string[]) => void;
}
>()(
persist(
@@ -81,6 +90,16 @@ export const useCandlesChartSettingsStore = create<
});
});
},
setChartlib: (lib) => {
set((state) => {
state.chartlib = lib;
});
},
setTradingViewStudies: (studies: string[]) => {
set((state) => {
state.tradingViewStudies = studies;
});
},
})),
{
name: 'vega_candles_chart_store',
@@ -88,13 +107,13 @@ export const useCandlesChartSettingsStore = create<
)
);
export const useCandlesChartSettings = () => {
const settings = useCandlesChartSettingsStore();
export const useChartSettings = () => {
const settings = useChartSettingsStore();
const interval: Interval = getValidItem(
settings.interval,
Object.values(Interval),
Interval.I15M
Interval.INTERVAL_I15M
);
const chartType: ChartType = getValidItem(
@@ -122,15 +141,19 @@ export const useCandlesChartSettings = () => {
});
return {
chartlib: settings.chartlib,
interval,
chartType,
overlays,
studies,
studySizes,
tradingViewStudies: settings.tradingViewStudies,
setInterval: settings.setInterval,
setType: settings.setType,
setStudies: settings.setStudies,
setOverlays: settings.setOverlays,
setStudySizes: settings.setStudySizes,
setChartlib: settings.setChartlib,
setTradingViewStudies: settings.setTradingViewStudies,
};
};
@@ -5,7 +5,7 @@ import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { Navbar } from './navbar';
import { useGlobalStore } from '../../stores';
import { ENV, FLAGS } from '@vegaprotocol/environment';
import { ENV, useFeatureFlags } from '@vegaprotocol/environment';
jest.mock('@vegaprotocol/proposals', () => ({
ProtocolUpgradeCountdown: () => null,
@@ -48,8 +48,7 @@ describe('Navbar', () => {
beforeAll(() => {
useGlobalStore.setState({ marketId });
const mockedFLAGS = jest.mocked(FLAGS);
mockedFLAGS.REFERRALS = true;
useFeatureFlags.setState({ flags: { REFERRALS: true } });
const mockedENV = jest.mocked(ENV);
mockedENV.VEGA_TOKEN_URL = 'governance';
});
+3 -2
View File
@@ -6,7 +6,7 @@ import {
Networks,
DApp,
useLinks,
FLAGS,
useFeatureFlags,
useEnvNameMapping,
} from '@vegaprotocol/environment';
import { useGlobalStore } from '../../stores';
@@ -157,6 +157,7 @@ export const Navbar = ({
* of the navigation
*/
const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const t = useT();
const envNameMapping = useEnvNameMapping();
const { VEGA_ENV, VEGA_NETWORKS, GITHUB_FEEDBACK_URL } = useEnvironment();
@@ -201,7 +202,7 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
{t('Portfolio')}
</NavbarLink>
</NavbarItem>
{FLAGS.REFERRALS && (
{featureFlags.REFERRALS && (
<NavbarItem>
<NavbarLink end={false} to={Links.REFERRALS()} onClick={onClick}>
{t('Referrals')}
+13 -1
View File
@@ -10,6 +10,18 @@ import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
import { useState, type ReactNode } from 'react';
import classNames from 'classnames';
import { useT } from '../../lib/use-t';
import { useFeatureFlags, type FeatureFlags } from '@vegaprotocol/environment';
export const FeatureFlagSwitch = ({ flag }: { flag: keyof FeatureFlags }) => {
const flags = useFeatureFlags((state) => state.flags);
const setFeatureFlag = useFeatureFlags((state) => state.setFeatureFlag);
return (
<Switch
onCheckedChange={(checked) => setFeatureFlag(flag, !!checked)}
checked={flags[flag]}
/>
);
};
export const Settings = () => {
const t = useT();
@@ -120,7 +132,7 @@ const SettingsGroup = ({
})}
>
<div className={classNames({ 'w-3/4': inline, 'mb-2': !inline })}>
<label className="text-sm">{label}</label>
<div className="text-sm">{label}</div>
{helpText && <p className="text-xs text-muted">{helpText}</p>}
</div>
{children}
@@ -3,7 +3,7 @@ import { matchPath, useLocation } from 'react-router-dom';
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '@vegaprotocol/environment';
import { VegaConnectDialog } from '@vegaprotocol/wallet';
import { Connectors } from '../../lib/vega-connectors';
import { useConnectors } from '../../lib/vega-connectors';
import { useT } from '../../lib/use-t';
import { Routes } from '../../lib/links';
import { RiskMessage } from './risk-message';
@@ -20,6 +20,7 @@ export const WelcomeDialog = () => {
const { pathname } = useLocation();
const t = useT();
const { VEGA_ENV } = useEnvironment();
const connectors = useConnectors();
const dismissed = useOnboardingStore((store) => store.dismissed);
const dialogOpen = useOnboardingStore((store) => store.dialogOpen);
const dismiss = useOnboardingStore((store) => store.dismiss);
@@ -43,7 +44,7 @@ export const WelcomeDialog = () => {
const content = walletDialogOpen ? (
<VegaConnectDialog
connectors={Connectors}
connectors={connectors}
riskMessage={<RiskMessage />}
onClose={() => setWalletDialogOpen(false)}
contentOnly
+10 -1
View File
@@ -4,9 +4,18 @@ export default {
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
'^.+\\.[tj]sx?$': [
'babel-jest',
{
presets: ['@nx/next/babel'],
// required for pennant to work in jest, due to having untranspiled exports
plugins: [['@babel/plugin-proposal-private-methods']],
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/apps/trading',
setupFilesAfterEnv: ['./setup-tests.ts'],
// dont ignore pennant from transpilation
transformIgnorePatterns: ['<rootDir>/node_modules/pennant'],
};
@@ -1,9 +1,9 @@
import { routerConfig } from '../../pages/client-router';
import { useRouterConfig } from '../../pages/client-router';
import { matchRoutes, useLocation } from 'react-router-dom';
export const useGetCurrentRouteId = () => {
const location = useLocation();
const matches = matchRoutes(routerConfig, location);
const matches = matchRoutes(useRouterConfig(), location);
const lastRoute = matches ? matches[matches.length - 1] : undefined;
if (lastRoute) {
const id = lastRoute.route.id;
+1
View File
@@ -75,6 +75,7 @@ i18n
'positions',
'trades',
'trading',
'trading-view',
'ui-toolkit',
'utils',
'wallet',
+14 -9
View File
@@ -1,4 +1,5 @@
import { FLAGS } from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { useMemo } from 'react';
import {
JsonRpcConnector,
ViewConnector,
@@ -18,13 +19,17 @@ if (typeof window !== 'undefined') {
view = new ViewConnector();
}
export const snap = FLAGS.METAMASK_SNAPS
? new SnapConnector(DEFAULT_SNAP_ID)
: undefined;
export const snap = new SnapConnector(DEFAULT_SNAP_ID);
export const Connectors = {
injected,
jsonRpc,
view,
snap,
export const useConnectors = () => {
const featureFlags = useFeatureFlags((state) => state.flags);
return useMemo(
() => ({
injected,
jsonRpc,
view,
snap: featureFlags.METAMASK_SNAPS ? snap : undefined,
}),
[featureFlags.METAMASK_SNAPS]
);
};
+139 -136
View File
@@ -22,7 +22,7 @@ import { ApplyCodeFormContainer } from '../client-pages/referrals/apply-code-for
import { CreateCodeContainer } from '../client-pages/referrals/create-code-form';
import { NotFound as ReferralNotFound } from '../client-pages/referrals/error-boundary';
import { compact } from 'lodash';
import { FLAGS } from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { LiquidityHeader } from '../components/liquidity-header';
import { MarketHeader } from '../components/market-header';
import { PortfolioSidebar } from '../client-pages/portfolio/portfolio-sidebar';
@@ -44,144 +44,147 @@ const NotFound = () => {
);
};
export const routerConfig: RouteObject[] = compact([
{
index: true,
element: <Home />,
id: AppRoutes.HOME,
},
{
path: 'disclaimer',
element: <LayoutCentered />,
id: AppRoutes.DISCLAIMER,
children: [{ index: true, element: <Disclaimer /> }],
},
// Referrals routing (the pages should be available if the feature flag is on)
FLAGS.REFERRALS
? {
path: AppRoutes.REFERRALS,
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [
{
element: (
<LayoutWithSky>
<Referrals />
</LayoutWithSky>
),
children: [
{
index: true,
element: <ReferralStatistics />,
},
{
path: AppRoutes.REFERRALS_CREATE_CODE,
element: <CreateCodeContainer />,
},
{
path: AppRoutes.REFERRALS_APPLY_CODE,
element: <ApplyCodeFormContainer />,
},
],
},
{
path: '*',
element: <ReferralNotFound />,
},
],
}
: undefined,
{
path: 'fees/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [
{
index: true,
element: <Fees />,
},
],
},
{
path: 'rewards/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [
{
index: true,
element: <Rewards />,
},
],
},
{
path: 'markets/*',
element: (
<LayoutWithSidebar
header={<MarketHeader />}
sidebar={<MarketsSidebar />}
/>
),
children: [
{
index: true,
element: <MarketsPage />,
id: AppRoutes.MARKETS,
},
{
path: 'all',
element: <Navigate to="/markets" />,
},
{
path: ':marketId',
element: <MarketPage />,
id: AppRoutes.MARKET,
},
],
},
{
path: 'portfolio/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [
{
index: true,
element: <Portfolio />,
id: AppRoutes.PORTFOLIO,
},
{
path: 'assets',
element: <Assets />,
id: AppRoutes.ASSETS,
children: [
{ index: true, element: <Navigate to="deposit" /> },
{ path: 'deposit', element: <Deposit />, id: AppRoutes.DEPOSIT },
{ path: 'withdraw', element: <Withdraw />, id: AppRoutes.WITHDRAW },
{ path: 'transfer', element: <Transfer />, id: AppRoutes.TRANSFER },
],
},
],
},
export const useRouterConfig = (): RouteObject[] => {
const featureFlags = useFeatureFlags((state) => state.flags);
return compact([
{
index: true,
element: <Home />,
id: AppRoutes.HOME,
},
{
path: 'disclaimer',
element: <LayoutCentered />,
id: AppRoutes.DISCLAIMER,
children: [{ index: true, element: <Disclaimer /> }],
},
// Referrals routing (the pages should be available if the feature flag is on)
featureFlags.REFERRALS
? {
path: AppRoutes.REFERRALS,
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [
{
element: (
<LayoutWithSky>
<Referrals />
</LayoutWithSky>
),
children: [
{
index: true,
element: <ReferralStatistics />,
},
{
path: AppRoutes.REFERRALS_CREATE_CODE,
element: <CreateCodeContainer />,
},
{
path: AppRoutes.REFERRALS_APPLY_CODE,
element: <ApplyCodeFormContainer />,
},
],
},
{
path: '*',
element: <ReferralNotFound />,
},
],
}
: undefined,
{
path: 'fees/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [
{
index: true,
element: <Fees />,
},
],
},
{
path: 'rewards/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [
{
index: true,
element: <Rewards />,
},
],
},
{
path: 'markets/*',
element: (
<LayoutWithSidebar
header={<MarketHeader />}
sidebar={<MarketsSidebar />}
/>
),
children: [
{
index: true,
element: <MarketsPage />,
id: AppRoutes.MARKETS,
},
{
path: 'all',
element: <Navigate to="/markets" />,
},
{
path: ':marketId',
element: <MarketPage />,
id: AppRoutes.MARKET,
},
],
},
{
path: 'portfolio/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [
{
index: true,
element: <Portfolio />,
id: AppRoutes.PORTFOLIO,
},
{
path: 'assets',
element: <Assets />,
id: AppRoutes.ASSETS,
children: [
{ index: true, element: <Navigate to="deposit" /> },
{ path: 'deposit', element: <Deposit />, id: AppRoutes.DEPOSIT },
{ path: 'withdraw', element: <Withdraw />, id: AppRoutes.WITHDRAW },
{ path: 'transfer', element: <Transfer />, id: AppRoutes.TRANSFER },
],
},
],
},
{
path: 'liquidity/*',
element: (
<LayoutWithSidebar
header={<LiquidityHeader />}
sidebar={<LiquiditySidebar />}
/>
),
id: AppRoutes.LIQUIDITY,
children: [
{
path: ':marketId',
element: <Liquidity />,
},
],
},
{
path: '*',
element: <NotFound />,
},
]);
{
path: 'liquidity/*',
element: (
<LayoutWithSidebar
header={<LiquidityHeader />}
sidebar={<LiquiditySidebar />}
/>
),
id: AppRoutes.LIQUIDITY,
children: [
{
path: ':marketId',
element: <Liquidity />,
},
],
},
{
path: '*',
element: <NotFound />,
},
]);
};
export const ClientRouter = () => {
const routes = useRoutes(routerConfig);
const routes = useRoutes(useRouterConfig());
return (
<Suspense
fallback={
+4 -3
View File
@@ -3,7 +3,7 @@ import {
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { VegaConnectDialog, ViewAsDialog } from '@vegaprotocol/wallet';
import { Connectors } from '../lib/vega-connectors';
import { useConnectors } from '../lib/vega-connectors';
import {
Web3ConnectUncontrolledDialog,
WithdrawalApprovalDialogContainer,
@@ -13,13 +13,14 @@ import { RiskMessage } from '../components/welcome-dialog';
const DialogsContainer = () => {
const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore();
const connectors = useConnectors();
return (
<>
<VegaConnectDialog
connectors={Connectors}
connectors={connectors}
riskMessage={<RiskMessage />}
/>
<ViewAsDialog connector={Connectors.view} />
<ViewAsDialog connector={connectors.view} />
<AssetDetailsDialog
assetId={id}
trigger={trigger || null}
+4 -3
View File
@@ -7,13 +7,14 @@ import {
useVegaWallet,
} from '@vegaprotocol/wallet';
import { useGlobalStore } from '../stores';
import { Connectors } from '../lib/vega-connectors';
import { useConnectors } from '../lib/vega-connectors';
import { useTelemetryApproval } from '../lib/hooks/use-telemetry-approval';
export const MaybeConnectEagerly = () => {
const { VEGA_ENV, SENTRY_DSN } = useEnvironment();
const update = useGlobalStore((store) => store.update);
const eagerConnecting = useVegaEagerConnect(Connectors);
const connectors = useConnectors();
const eagerConnecting = useVegaEagerConnect(connectors);
const [isTelemetryApproved] = useTelemetryApproval();
useEthereumEagerConnect(
isTelemetryApproved ? { dsn: SENTRY_DSN, env: VEGA_ENV } : {}
@@ -23,7 +24,7 @@ export const MaybeConnectEagerly = () => {
const [searchParams] = useSearchParams();
const [query] = useState(searchParams.get('address'));
if (query && !pubKey) {
connect(Connectors['view']);
connect(connectors.view);
}
useEffect(() => {
update({ eagerConnecting });
+26 -17
View File
@@ -1,43 +1,52 @@
import 'pennant/dist/style.css';
import { CandlestickChart } from 'pennant';
import {
CandlestickChart,
type Overlay,
type ChartType,
type Interval,
type Study,
} from 'pennant';
import { VegaDataSource } from './data-source';
import { useApolloClient } from '@apollo/client';
import { useMemo } from 'react';
import debounce from 'lodash/debounce';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useVegaWallet } from '@vegaprotocol/wallet';
import {
STUDY_SIZE,
useCandlesChartSettings,
} from './use-candles-chart-settings';
import { useT } from './use-t';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
export type CandlesChartContainerProps = {
marketId: string;
interval: Interval;
chartType: ChartType;
overlays: Overlay[];
studies: Study[];
studySizes: number[];
defaultStudySize: number;
setStudies: (studies?: Study[]) => void;
setStudySizes: (sizes: number[]) => void;
setOverlays: (overlays?: Overlay[]) => void;
};
const CANDLES_TO_WIDTH_FACTOR = 0.2;
export const CandlesChartContainer = ({
marketId,
interval,
chartType,
overlays,
studies,
studySizes,
defaultStudySize,
setStudies,
setStudySizes,
setOverlays,
}: CandlesChartContainerProps) => {
const client = useApolloClient();
const { pubKey } = useVegaWallet();
const { theme } = useThemeSwitcher();
const t = useT();
const {
interval,
chartType,
overlays,
studies,
studySizes,
setStudies,
setStudySizes,
setOverlays,
} = useCandlesChartSettings();
const handlePaneChange = useMemo(
() =>
debounce((sizes: number[]) => {
@@ -69,7 +78,7 @@ export const CandlesChartContainer = ({
</span>
),
initialNumCandlesToDisplay: candlesCount,
studySize: STUDY_SIZE,
studySize: defaultStudySize,
studySizes,
}}
interval={interval}
@@ -1,85 +0,0 @@
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CandlesMenu } from './candles-menu';
import {
useCandlesChartSettingsStore,
DEFAULT_CHART_SETTINGS,
} from './use-candles-chart-settings';
import { Overlay, Study, overlayLabels, studyLabels } from 'pennant';
describe('CandlesMenu', () => {
const openDropdown = async () => {
await userEvent.click(
screen.getByRole('button', {
name: 'Indicators',
})
);
};
beforeEach(() => {
// clear store each time to avoid conditional testing of defaults
useCandlesChartSettingsStore.setState({ overlays: [], studies: [] });
});
it.each(Object.values(Overlay))('can set %s overlay', async (overlay) => {
render(<CandlesMenu />);
await openDropdown();
const menu = within(await screen.findByRole('menu'));
await userEvent.click(menu.getByText(overlayLabels[overlay as Overlay]));
// re-open the dropdown
await openDropdown();
expect(screen.getByText(overlayLabels[overlay as Overlay])).toHaveAttribute(
'data-state',
'checked'
);
});
it.each(Object.values(Study))('can set %s study', async (study) => {
render(<CandlesMenu />);
await openDropdown();
const menu = within(await screen.findByRole('menu'));
await userEvent.click(menu.getByText(studyLabels[study as Study]));
// re-open the dropdown
await openDropdown();
expect(screen.getByText(studyLabels[study as Study])).toHaveAttribute(
'data-state',
'checked'
);
});
it('should render with the correct default studies and overlays', async () => {
useCandlesChartSettingsStore.setState(DEFAULT_CHART_SETTINGS);
render(<CandlesMenu />);
await userEvent.click(
screen.getByRole('button', {
name: 'Indicators',
})
);
const menu = within(await screen.findByRole('menu'));
expect(menu.getByText(studyLabels.volume)).toHaveAttribute(
'data-state',
'checked'
);
expect(menu.getByText(studyLabels.macd)).toHaveAttribute(
'data-state',
'checked'
);
expect(menu.getByText(overlayLabels.movingAverage)).toHaveAttribute(
'data-state',
'checked'
);
});
});
+12
View File
@@ -0,0 +1,12 @@
import { Interval as PennantInterval } from 'pennant';
import { Interval } from '@vegaprotocol/types';
export const PENNANT_INTERVAL_MAP = {
[Interval.INTERVAL_BLOCK]: undefined, // TODO: handle block tick
[Interval.INTERVAL_I1M]: PennantInterval.I1M,
[Interval.INTERVAL_I5M]: PennantInterval.I5M,
[Interval.INTERVAL_I15M]: PennantInterval.I15M,
[Interval.INTERVAL_I1H]: PennantInterval.I1H,
[Interval.INTERVAL_I6H]: PennantInterval.I6H,
[Interval.INTERVAL_I1D]: PennantInterval.I1D,
} as const;
+5 -1
View File
@@ -56,6 +56,7 @@ const defaultConfig = {
*/
export class VegaDataSource implements DataSource {
client: ApolloClient<object>;
from?: Date;
marketId: string;
partyId: null | string;
_decimalPlaces = 0;
@@ -158,6 +159,7 @@ export class VegaDataSource implements DataSource {
*/
async query(interval: PennantInterval, from: string) {
try {
this.from = new Date(from);
const { data } = await this.client.query<
CandlesQuery,
CandlesQueryVariables
@@ -215,7 +217,9 @@ export class VegaDataSource implements DataSource {
this.decimalPlaces,
this.positionDecimalPlaces
);
if (!this.from || candle.date < this.from) {
return;
}
onSubscriptionData(candle);
}
});
+1 -1
View File
@@ -1,5 +1,5 @@
export * from './__generated__/Candles';
export * from './__generated__/Chart';
export * from './candles-chart';
export * from './candles-menu';
export { PENNANT_INTERVAL_MAP } from './constants';
export * from './data-source';
@@ -11,7 +11,7 @@ import {
} from '@vegaprotocol/markets';
import { AsyncRendererInline } from '@vegaprotocol/ui-toolkit';
import { DealTicket } from './deal-ticket';
import { FLAGS } from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { useT } from '../../use-t';
interface DealTicketContainerProps {
@@ -24,6 +24,7 @@ export const DealTicketContainer = ({
marketId,
...props
}: DealTicketContainerProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const t = useT();
const showStopOrder = useDealTicketFormValues((state) =>
isStopOrderType(state.formValues[marketId]?.type)
@@ -50,7 +51,7 @@ export const DealTicketContainer = ({
reload={reload}
>
{market && marketData ? (
FLAGS.STOP_ORDERS && showStopOrder ? (
featureFlags.STOP_ORDERS && showStopOrder ? (
<StopOrder
market={market}
marketPrice={marketPrice}
@@ -11,7 +11,7 @@ import {
DealTicketType,
useDealTicketFormValues,
} from '../../hooks/use-form-values';
import type { FeatureFlags } from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { formatForInput } from '@vegaprotocol/utils';
jest.mock('zustand');
@@ -19,17 +19,6 @@ jest.mock('./deal-ticket-fee-details', () => ({
DealTicketFeeDetails: () => <div data-testid="deal-ticket-fee-details" />,
}));
jest.mock('@vegaprotocol/environment', () => {
const actual = jest.requireActual('@vegaprotocol/environment');
return {
...actual,
FLAGS: {
...actual.FLAGS,
STOP_ORDERS: true,
} as FeatureFlags,
};
});
const marketPrice = '200';
const market = generateMarket();
const submit = jest.fn();
@@ -94,6 +83,7 @@ jest.mock('@vegaprotocol/data-provider', () => ({
describe('StopOrder', () => {
beforeEach(() => {
localStorage.clear();
useFeatureFlags.setState({ flags: { STOP_ORDERS: true } });
});
afterEach(() => {
@@ -18,7 +18,7 @@ import { MarketModeValidationType } from '../../constants';
import { DealTicketType } from '../../hooks/use-form-values';
import * as RadioGroup from '@radix-ui/react-radio-group';
import classNames from 'classnames';
import { FLAGS } from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { Trans } from 'react-i18next';
import { useT, ns } from '../../use-t';
@@ -49,6 +49,7 @@ export const TypeToggle = ({
value,
onValueChange,
}: Pick<TypeSelectorProps, 'onValueChange' | 'value'>) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const t = useT();
const options = useOptions();
const toggles = useToggles();
@@ -57,8 +58,8 @@ export const TypeToggle = ({
<RadioGroup.Root
name="order-type"
className={classNames('mb-2 grid h-8 leading-8 font-alpha text-xs', {
'grid-cols-3': FLAGS.STOP_ORDERS,
'grid-cols-2': !FLAGS.STOP_ORDERS,
'grid-cols-3': featureFlags.STOP_ORDERS,
'grid-cols-2': !featureFlags.STOP_ORDERS,
})}
value={value}
onValueChange={onValueChange}
@@ -80,7 +81,7 @@ export const TypeToggle = ({
</button>
</RadioGroup.Item>
))}
{FLAGS.STOP_ORDERS && (
{featureFlags.STOP_ORDERS && (
<TradingDropdown
trigger={
<TradingDropdownTrigger
@@ -3,7 +3,13 @@ import { act } from 'react-dom/test-utils';
import type { ClientOptions } from '@vegaprotocol/apollo-client';
import { createClient } from '@vegaprotocol/apollo-client';
import { Networks } from '../types';
import { STORAGE_KEY, useEnvironment } from './use-environment';
import {
STORAGE_KEY,
useEnvironment,
featureFlagsLocalStorageKey,
getUserEnabledFeatureFlags,
setUserEnabledFeatureFlag,
} from './use-environment';
const noop = () => {
/* no op*/
@@ -459,3 +465,30 @@ describe('useEnvironment', () => {
expect(localStorage.getItem(STORAGE_KEY)).toBe(newUrl);
});
});
describe('getUserEnabledFeatureFlags', () => {
beforeEach(() => {
localStorage.clear();
});
it('reads enabled flags from local storage', () => {
localStorage.setItem(
featureFlagsLocalStorageKey,
'STOP_ORDERS,STOP_ORDERS,REFERRALS,BLAH'
);
const userEnabledFlags = getUserEnabledFeatureFlags(true, ['STOP_ORDERS']);
expect(userEnabledFlags).toEqual(['STOP_ORDERS']);
});
});
describe('setUserEnabledFeatureFlag', () => {
beforeEach(() => {
localStorage.clear();
});
it('saves enabled flags to local storage', () => {
setUserEnabledFeatureFlag('STOP_ORDERS', true);
expect(localStorage.getItem(featureFlagsLocalStorageKey)).toEqual(
'STOP_ORDERS'
);
expect(getUserEnabledFeatureFlags()).toEqual(['STOP_ORDERS']);
});
});
+470 -400
View File
@@ -40,12 +40,478 @@ const VERSION = 1;
export const STORAGE_KEY = `vega_url_${VERSION}`;
const SUBSCRIPTION_TIMEOUT = 3000;
export const ENV = compileEnvVars();
export const FLAGS = compileFeatureFlags();
/**
* Fetch and validate a vega node configuration
*/
const fetchConfig = async (url?: string) => {
if (!url) return [];
const res = await fetch(url);
const content = await res.text();
const parsed = tomlParse(content);
const tomlResults = tomlConfigSchema.parse(parsed);
const {
API: {
GraphQL: { Hosts },
},
} = tomlResults;
return Hosts;
};
/**
* Find a suitable node by running a test query and test
* subscription, against a list of clients, first to resolve wins
*/
const findNode = async (clients: ClientCollection): Promise<string | null> => {
const tests = Object.entries(clients).map((args) => testNode(...args));
try {
const url = await Promise.any(tests);
return url;
} catch {
// All tests rejected, no suitable node found
return null;
}
};
/**
* Test a node for suitability for connection
*/
const testNode = async (
url: string,
client: Client
): Promise<string | null> => {
const results = await Promise.all([
// these promises will only resolve with true/false
testQuery(client),
testSubscription(client),
]);
if (results[0] && results[1]) {
return url;
}
const message = `Tests failed for node: ${url}`;
console.warn(message);
// throwing here will mean this tests is ignored and a different
// node that hopefully does resolve will fulfill the Promise.any
throw new Error(message);
};
/**
* Run a test query on a client
*/
const testQuery = async (client: Client) => {
try {
const result = await client.query<NodeCheckQuery>({
query: NodeCheckDocument,
});
if (!result || result.error) {
return false;
}
return true;
} catch (err) {
return false;
}
};
/**
* Run a test subscription on a client. A subscription
* that takes longer than SUBSCRIPTION_TIMEOUT ms to respond
* is deemed a failure
*/
const testSubscription = (client: Client) => {
return new Promise((resolve) => {
const sub = client
.subscribe<NodeCheckTimeUpdateSubscription>({
query: NodeCheckTimeUpdateDocument,
errorPolicy: 'all',
})
.subscribe({
next: () => {
resolve(true);
sub.unsubscribe();
},
error: () => {
resolve(false);
sub.unsubscribe();
},
});
setTimeout(() => {
resolve(false);
sub.unsubscribe();
}, SUBSCRIPTION_TIMEOUT);
});
};
export const userControllableFeatureFlags: (keyof FeatureFlags)[] = [];
/**
* Retrieve env vars, parsing where needed some type casting is needed
* here to appease the environment store interface
*/
const compileEnvVars = () => {
const VEGA_ENV = windowOrDefault(
'VEGA_ENV',
process.env['NX_VEGA_ENV']
) as Networks;
const env: Environment = {
VEGA_URL: windowOrDefault('VEGA_URL', process.env['NX_VEGA_URL']),
VEGA_ENV,
VEGA_CONFIG_URL: windowOrDefault(
'VEGA_CONFIG_URL',
process.env['NX_VEGA_CONFIG_URL'] as string
),
VEGA_NETWORKS: parseNetworks(
windowOrDefault('VEGA_NETWORKS', process.env['NX_VEGA_NETWORKS'])
),
VEGA_WALLET_URL: windowOrDefault(
'VEGA_WALLET_URL',
process.env['NX_VEGA_WALLET_URL'] as string
),
HOSTED_WALLET_URL: windowOrDefault(
'HOSTED_WALLET_URL',
process.env['NX_HOSTED_WALLET_URL']
),
ETHERSCAN_URL: getEtherscanUrl(
VEGA_ENV,
windowOrDefault('ETHERSCAN_URL', process.env['NX_ETHERSCAN_URL'])
),
ETHEREUM_PROVIDER_URL: getEthereumProviderUrl(
VEGA_ENV,
windowOrDefault(
'ETHEREUM_PROVIDER_URL',
process.env['NX_ETHEREUM_PROVIDER_URL']
)
),
ETH_LOCAL_PROVIDER_URL: windowOrDefault(
'ETH_LOCAL_PROVIDER_URL',
process.env['NX_ETH_LOCAL_PROVIDER_URL']
),
ETH_WALLET_MNEMONIC: windowOrDefault(
'ETH_WALLET_MNEMONIC',
process.env['NX_ETH_WALLET_MNEMONIC']
),
ORACLE_PROOFS_URL: windowOrDefault(
'ORACLE_PROOFS_URL',
process.env['NX_ORACLE_PROOFS_URL']
),
VEGA_DOCS_URL: windowOrDefault(
'VEGA_DOCS_URL',
process.env['NX_VEGA_DOCS_URL']
),
VEGA_CONSOLE_URL: windowOrDefault(
'VEGA_CONSOLE_URL',
process.env['NX_VEGA_CONSOLE_URL']
),
VEGA_EXPLORER_URL: windowOrDefault(
'VEGA_EXPLORER_URL',
process.env['NX_VEGA_EXPLORER_URL']
),
VEGA_TOKEN_URL: windowOrDefault(
'VEGA_TOKEN_URL',
process.env['NX_VEGA_TOKEN_URL']
),
GITHUB_FEEDBACK_URL: windowOrDefault(
'GITHUB_FEEDBACK_URL',
process.env['NX_GITHUB_FEEDBACK_URL']
),
GIT_BRANCH: windowOrDefault(
'GIT_COMMIT_BRANCH',
process.env['GIT_COMMIT_BRANCH']
),
GIT_COMMIT_HASH: windowOrDefault(
'GIT_COMMIT_HASH',
process.env['GIT_COMMIT_HASH']
),
GIT_ORIGIN_URL: windowOrDefault(
'GIT_ORIGIN_URL',
process.env['GIT_ORIGIN_URL']
),
ANNOUNCEMENTS_CONFIG_URL: windowOrDefault(
'ANNOUNCEMENTS_CONFIG_URL',
process.env['NX_ANNOUNCEMENTS_CONFIG_URL']
),
VEGA_INCIDENT_URL: windowOrDefault(
'VEGA_INCIDENT_URL',
process.env['NX_VEGA_INCIDENT_URL']
),
APP_VERSION: windowOrDefault('APP_VERSION', process.env['NX_APP_VERSION']),
SENTRY_DSN: windowOrDefault('SENTRY_DSN', process.env['NX_SENTRY_DSN']),
TENDERMINT_URL: windowOrDefault(
'NX_TENDERMINT_URL',
process.env['NX_TENDERMINT_URL']
),
TENDERMINT_WEBSOCKET_URL: windowOrDefault(
'NX_TENDERMINT_WEBSOCKET_URL',
process.env['NX_TENDERMINT_WEBSOCKET_URL']
),
CHROME_EXTENSION_URL: windowOrDefault(
'NX_CHROME_EXTENSION_URL',
process.env['NX_CHROME_EXTENSION_URL']
),
MOZILLA_EXTENSION_URL: windowOrDefault(
'NX_MOZILLA_EXTENSION_URL',
process.env['NX_MOZILLA_EXTENSION_URL']
),
CHARTING_LIBRARY_PATH: windowOrDefault(
'NX_CHARTING_LIBRARY_PATH',
process.env['NX_CHARTING_LIBRARY_PATH']
),
CHARTING_LIBRARY_HASH: windowOrDefault(
'NX_CHARTING_LIBRARY_HASH',
process.env['NX_CHARTING_LIBRARY_HASH']
),
};
return env;
};
export const featureFlagsLocalStorageKey = 'vega_feature_flags';
let userEnabledFeatureFlags: (keyof FeatureFlags)[] | undefined = undefined;
export const setUserEnabledFeatureFlag = (
flag: keyof FeatureFlags,
enabled = false
) => {
const enabledFlags = getUserEnabledFeatureFlags();
if (enabled && !enabledFlags.includes(flag)) {
enabledFlags.push(flag);
}
if (!enabled && enabledFlags.includes(flag)) {
enabledFlags.splice(enabledFlags.indexOf(flag), 1);
}
userEnabledFeatureFlags = enabledFlags;
if (typeof window !== 'undefined') {
window.localStorage.setItem(
featureFlagsLocalStorageKey,
enabledFlags.join(',')
);
}
};
export const getUserEnabledFeatureFlags = (
refresh = false,
allowedFlags = userControllableFeatureFlags
): (keyof FeatureFlags)[] => {
if (typeof window === 'undefined') {
return [];
}
if (typeof userEnabledFeatureFlags !== 'undefined' && !refresh) {
return userEnabledFeatureFlags;
}
const enabledFlags = window.localStorage.getItem(featureFlagsLocalStorageKey);
userEnabledFeatureFlags = enabledFlags
? uniq(
(enabledFlags.split(',') as (keyof FeatureFlags)[]).filter((flag) =>
allowedFlags.includes(flag)
)
)
: [];
return userEnabledFeatureFlags;
};
const TRUTHY = ['1', 'true'];
export const compileFeatureFlags = (refresh = false): FeatureFlags => {
const COSMIC_ELEVATOR_FLAGS: CosmicElevatorFlags = {
ICEBERG_ORDERS: TRUTHY.includes(
windowOrDefault(
'NX_ICEBERG_ORDERS',
process.env['NX_ICEBERG_ORDERS']
) as string
),
STOP_ORDERS: TRUTHY.includes(
windowOrDefault('NX_STOP_ORDERS', process.env['NX_STOP_ORDERS']) as string
),
SUCCESSOR_MARKETS: TRUTHY.includes(
windowOrDefault(
'NX_SUCCESSOR_MARKETS',
process.env['NX_SUCCESSOR_MARKETS']
) as string
),
PRODUCT_PERPETUALS: TRUTHY.includes(
windowOrDefault(
'NX_PRODUCT_PERPETUALS',
process.env['NX_PRODUCT_PERPETUALS']
) as string
),
METAMASK_SNAPS: TRUTHY.includes(
windowOrDefault(
'NX_METAMASK_SNAPS',
process.env['NX_METAMASK_SNAPS']
) as string
),
REFERRALS: TRUTHY.includes(
windowOrDefault('NX_REFERRALS', process.env['NX_REFERRALS']) as string
),
DISABLE_CLOSE_POSITION: TRUTHY.includes(
windowOrDefault(
'NX_DISABLE_CLOSE_POSITION',
process.env['NX_DISABLE_CLOSE_POSITION']
) as string
),
UPDATE_MARKET_STATE: TRUTHY.includes(
windowOrDefault(
'NX_UPDATE_MARKET_STATE',
process.env['NX_UPDATE_MARKET_STATE']
) as string
),
GOVERNANCE_TRANSFERS: TRUTHY.includes(
windowOrDefault(
'NX_GOVERNANCE_TRANSFERS',
process.env['NX_GOVERNANCE_TRANSFERS']
) as string
),
VOLUME_DISCOUNTS: TRUTHY.includes(
windowOrDefault(
'NX_VOLUME_DISCOUNTS',
process.env['NX_VOLUME_DISCOUNTS']
) as string
),
};
const EXPLORER_FLAGS = {
EXPLORER_ASSETS: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_ASSETS',
process.env['NX_EXPLORER_ASSETS']
) as string
),
EXPLORER_GENESIS: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_GENESIS',
process.env['NX_EXPLORER_GENESIS']
) as string
),
EXPLORER_GOVERNANCE: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_GOVERNANCE',
process.env['NX_EXPLORER_GOVERNANCE']
) as string
),
EXPLORER_MARKETS: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_MARKETS',
process.env['NX_EXPLORER_MARKETS']
) as string
),
EXPLORER_ORACLES: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_ORACLES',
process.env['NX_EXPLORER_ORACLES']
) as string
),
EXPLORER_TXS_LIST: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_TXS_LIST',
process.env['NX_EXPLORER_TXS_LIST']
) as string
),
EXPLORER_NETWORK_PARAMETERS: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_NETWORK_PARAMETERS',
process.env['NX_EXPLORER_NETWORK_PARAMETERS']
) as string
),
EXPLORER_PARTIES: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_PARTIES',
process.env['NX_EXPLORER_PARTIES']
) as string
),
EXPLORER_VALIDATORS: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_VALIDATORS',
process.env['NX_EXPLORER_VALIDATORS']
) as string
),
};
const GOVERNANCE_FLAGS = {
GOVERNANCE_NETWORK_DOWN: TRUTHY.includes(
windowOrDefault(
'NX_NETWORK_DOWN',
process.env['NX_NETWORK_DOWN']
) as string
),
GOVERNANCE_NETWORK_LIMITS: TRUTHY.includes(
windowOrDefault(
'NX_GOVERNANCE_NETWORK_LIMITS',
process.env['NX_GOVERNANCE_NETWORK_LIMITS']
) as string
),
};
const flags = {
...COSMIC_ELEVATOR_FLAGS,
...EXPLORER_FLAGS,
...GOVERNANCE_FLAGS,
};
getUserEnabledFeatureFlags(refresh).forEach((flag) => (flags[flag] = true));
return flags;
};
const parseNetworks = (value?: string) => {
if (value) {
try {
return JSON.parse(value);
} catch (e) {
return {};
}
}
return {};
};
/**
* Provides a fallback ethereum provider url for test purposes in some apps
*/
const getEthereumProviderUrl = (
network: Networks | undefined,
envvar: string | undefined
) => {
if (envvar) return envvar;
return network === Networks.MAINNET
? 'https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'
: 'https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8';
};
/**
* Provide a fallback etherscan url for test purposes in some apps
*/
const getEtherscanUrl = (
network: Networks | undefined,
envvar: string | undefined
) => {
if (envvar) return envvar;
return network === Networks.MAINNET
? 'https://etherscan.io'
: 'https://sepolia.etherscan.io';
};
const windowOrDefault = (key: string, defaultValue?: string) => {
if (typeof window !== 'undefined') {
// @ts-ignore avoid conflict in env
if (window._env_ && window._env_[key]) {
// @ts-ignore presence has been check above
return window._env_[key];
}
}
return defaultValue || undefined;
};
export const useFeatureFlags = create<{
flags: FeatureFlags;
setFeatureFlag: (flag: keyof FeatureFlags, enabled: boolean) => void;
}>()((set, get) => ({
flags: compileFeatureFlags(),
setFeatureFlag: (flag: keyof FeatureFlags, enabled: boolean) => {
if (userControllableFeatureFlags.includes(flag)) {
setUserEnabledFeatureFlag(flag, enabled);
set({ flags: { ...get().flags, [flag]: enabled } });
}
},
}));
export const useEnvironment = create<EnvStore>()((set, get) => ({
...compileEnvVars(),
...compileFeatureFlags(),
nodes: [],
status: 'default',
error: null,
@@ -166,400 +632,4 @@ export const useInitializeEnv = () => {
}, [status, initialize]);
};
/**
* Fetch and validate a vega node configuration
*/
const fetchConfig = async (url?: string) => {
if (!url) return [];
const res = await fetch(url);
const content = await res.text();
const parsed = tomlParse(content);
const tomlResults = tomlConfigSchema.parse(parsed);
const {
API: {
GraphQL: { Hosts },
},
} = tomlResults;
return Hosts;
};
/**
* Find a suitable node by running a test query and test
* subscription, against a list of clients, first to resolve wins
*/
const findNode = async (clients: ClientCollection): Promise<string | null> => {
const tests = Object.entries(clients).map((args) => testNode(...args));
try {
const url = await Promise.any(tests);
return url;
} catch {
// All tests rejected, no suitable node found
return null;
}
};
/**
* Test a node for suitability for connection
*/
const testNode = async (
url: string,
client: Client
): Promise<string | null> => {
const results = await Promise.all([
// these promises will only resolve with true/false
testQuery(client),
testSubscription(client),
]);
if (results[0] && results[1]) {
return url;
}
const message = `Tests failed for node: ${url}`;
console.warn(message);
// throwing here will mean this tests is ignored and a different
// node that hopefully does resolve will fulfill the Promise.any
throw new Error(message);
};
/**
* Run a test query on a client
*/
const testQuery = async (client: Client) => {
try {
const result = await client.query<NodeCheckQuery>({
query: NodeCheckDocument,
});
if (!result || result.error) {
return false;
}
return true;
} catch (err) {
return false;
}
};
/**
* Run a test subscription on a client. A subscription
* that takes longer than SUBSCRIPTION_TIMEOUT ms to respond
* is deemed a failure
*/
const testSubscription = (client: Client) => {
return new Promise((resolve) => {
const sub = client
.subscribe<NodeCheckTimeUpdateSubscription>({
query: NodeCheckTimeUpdateDocument,
errorPolicy: 'all',
})
.subscribe({
next: () => {
resolve(true);
sub.unsubscribe();
},
error: () => {
resolve(false);
sub.unsubscribe();
},
});
setTimeout(() => {
resolve(false);
sub.unsubscribe();
}, SUBSCRIPTION_TIMEOUT);
});
};
/**
* Retrieve env vars, parsing where needed some type casting is needed
* here to appease the environment store interface
*/
function compileEnvVars() {
const VEGA_ENV = windowOrDefault(
'VEGA_ENV',
process.env['NX_VEGA_ENV']
) as Networks;
const env: Environment = {
VEGA_URL: windowOrDefault('VEGA_URL', process.env['NX_VEGA_URL']),
VEGA_ENV,
VEGA_CONFIG_URL: windowOrDefault(
'VEGA_CONFIG_URL',
process.env['NX_VEGA_CONFIG_URL'] as string
),
VEGA_NETWORKS: parseNetworks(
windowOrDefault('VEGA_NETWORKS', process.env['NX_VEGA_NETWORKS'])
),
VEGA_WALLET_URL: windowOrDefault(
'VEGA_WALLET_URL',
process.env['NX_VEGA_WALLET_URL'] as string
),
HOSTED_WALLET_URL: windowOrDefault(
'HOSTED_WALLET_URL',
process.env['NX_HOSTED_WALLET_URL']
),
ETHERSCAN_URL: getEtherscanUrl(
VEGA_ENV,
windowOrDefault('ETHERSCAN_URL', process.env['NX_ETHERSCAN_URL'])
),
ETHEREUM_PROVIDER_URL: getEthereumProviderUrl(
VEGA_ENV,
windowOrDefault(
'ETHEREUM_PROVIDER_URL',
process.env['NX_ETHEREUM_PROVIDER_URL']
)
),
ETH_LOCAL_PROVIDER_URL: windowOrDefault(
'ETH_LOCAL_PROVIDER_URL',
process.env['NX_ETH_LOCAL_PROVIDER_URL']
),
ETH_WALLET_MNEMONIC: windowOrDefault(
'ETH_WALLET_MNEMONIC',
process.env['NX_ETH_WALLET_MNEMONIC']
),
ORACLE_PROOFS_URL: windowOrDefault(
'ORACLE_PROOFS_URL',
process.env['NX_ORACLE_PROOFS_URL']
),
VEGA_DOCS_URL: windowOrDefault(
'VEGA_DOCS_URL',
process.env['NX_VEGA_DOCS_URL']
),
VEGA_CONSOLE_URL: windowOrDefault(
'VEGA_CONSOLE_URL',
process.env['NX_VEGA_CONSOLE_URL']
),
VEGA_EXPLORER_URL: windowOrDefault(
'VEGA_EXPLORER_URL',
process.env['NX_VEGA_EXPLORER_URL']
),
VEGA_TOKEN_URL: windowOrDefault(
'VEGA_TOKEN_URL',
process.env['NX_VEGA_TOKEN_URL']
),
GITHUB_FEEDBACK_URL: windowOrDefault(
'GITHUB_FEEDBACK_URL',
process.env['NX_GITHUB_FEEDBACK_URL']
),
GIT_BRANCH: windowOrDefault(
'GIT_COMMIT_BRANCH',
process.env['GIT_COMMIT_BRANCH']
),
GIT_COMMIT_HASH: windowOrDefault(
'GIT_COMMIT_HASH',
process.env['GIT_COMMIT_HASH']
),
GIT_ORIGIN_URL: windowOrDefault(
'GIT_ORIGIN_URL',
process.env['GIT_ORIGIN_URL']
),
ANNOUNCEMENTS_CONFIG_URL: windowOrDefault(
'ANNOUNCEMENTS_CONFIG_URL',
process.env['NX_ANNOUNCEMENTS_CONFIG_URL']
),
VEGA_INCIDENT_URL: windowOrDefault(
'VEGA_INCIDENT_URL',
process.env['NX_VEGA_INCIDENT_URL']
),
APP_VERSION: windowOrDefault('APP_VERSION', process.env['NX_APP_VERSION']),
SENTRY_DSN: windowOrDefault('SENTRY_DSN', process.env['NX_SENTRY_DSN']),
TENDERMINT_URL: windowOrDefault(
'NX_TENDERMINT_URL',
process.env['NX_TENDERMINT_URL']
),
TENDERMINT_WEBSOCKET_URL: windowOrDefault(
'NX_TENDERMINT_WEBSOCKET_URL',
process.env['NX_TENDERMINT_WEBSOCKET_URL']
),
CHROME_EXTENSION_URL: windowOrDefault(
'NX_CHROME_EXTENSION_URL',
process.env['NX_CHROME_EXTENSION_URL']
),
MOZILLA_EXTENSION_URL: windowOrDefault(
'NX_MOZILLA_EXTENSION_URL',
process.env['NX_MOZILLA_EXTENSION_URL']
),
};
return env;
}
function compileFeatureFlags(): FeatureFlags {
const TRUTHY = ['1', 'true'];
const COSMIC_ELEVATOR_FLAGS: CosmicElevatorFlags = {
ICEBERG_ORDERS: TRUTHY.includes(
windowOrDefault(
'NX_ICEBERG_ORDERS',
process.env['NX_ICEBERG_ORDERS']
) as string
),
STOP_ORDERS: TRUTHY.includes(
windowOrDefault('NX_STOP_ORDERS', process.env['NX_STOP_ORDERS']) as string
),
SUCCESSOR_MARKETS: TRUTHY.includes(
windowOrDefault(
'NX_SUCCESSOR_MARKETS',
process.env['NX_SUCCESSOR_MARKETS']
) as string
),
PRODUCT_PERPETUALS: TRUTHY.includes(
windowOrDefault(
'NX_PRODUCT_PERPETUALS',
process.env['NX_PRODUCT_PERPETUALS']
) as string
),
METAMASK_SNAPS: TRUTHY.includes(
windowOrDefault(
'NX_METAMASK_SNAPS',
process.env['NX_METAMASK_SNAPS']
) as string
),
REFERRALS: TRUTHY.includes(
windowOrDefault('NX_REFERRALS', process.env['NX_REFERRALS']) as string
),
DISABLE_CLOSE_POSITION: TRUTHY.includes(
windowOrDefault(
'NX_DISABLE_CLOSE_POSITION',
process.env['NX_DISABLE_CLOSE_POSITION']
) as string
),
UPDATE_MARKET_STATE: TRUTHY.includes(
windowOrDefault(
'NX_UPDATE_MARKET_STATE',
process.env['NX_UPDATE_MARKET_STATE']
) as string
),
GOVERNANCE_TRANSFERS: TRUTHY.includes(
windowOrDefault(
'NX_GOVERNANCE_TRANSFERS',
process.env['NX_GOVERNANCE_TRANSFERS']
) as string
),
VOLUME_DISCOUNTS: TRUTHY.includes(
windowOrDefault(
'NX_VOLUME_DISCOUNTS',
process.env['NX_VOLUME_DISCOUNTS']
) as string
),
};
const EXPLORER_FLAGS = {
EXPLORER_ASSETS: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_ASSETS',
process.env['NX_EXPLORER_ASSETS']
) as string
),
EXPLORER_GENESIS: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_GENESIS',
process.env['NX_EXPLORER_GENESIS']
) as string
),
EXPLORER_GOVERNANCE: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_GOVERNANCE',
process.env['NX_EXPLORER_GOVERNANCE']
) as string
),
EXPLORER_MARKETS: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_MARKETS',
process.env['NX_EXPLORER_MARKETS']
) as string
),
EXPLORER_ORACLES: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_ORACLES',
process.env['NX_EXPLORER_ORACLES']
) as string
),
EXPLORER_TXS_LIST: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_TXS_LIST',
process.env['NX_EXPLORER_TXS_LIST']
) as string
),
EXPLORER_NETWORK_PARAMETERS: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_NETWORK_PARAMETERS',
process.env['NX_EXPLORER_NETWORK_PARAMETERS']
) as string
),
EXPLORER_PARTIES: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_PARTIES',
process.env['NX_EXPLORER_PARTIES']
) as string
),
EXPLORER_VALIDATORS: TRUTHY.includes(
windowOrDefault(
'NX_EXPLORER_VALIDATORS',
process.env['NX_EXPLORER_VALIDATORS']
) as string
),
};
const GOVERNANCE_FLAGS = {
GOVERNANCE_NETWORK_DOWN: TRUTHY.includes(
windowOrDefault(
'NX_NETWORK_DOWN',
process.env['NX_NETWORK_DOWN']
) as string
),
GOVERNANCE_NETWORK_LIMITS: TRUTHY.includes(
windowOrDefault(
'NX_GOVERNANCE_NETWORK_LIMITS',
process.env['NX_GOVERNANCE_NETWORK_LIMITS']
) as string
),
};
return {
...COSMIC_ELEVATOR_FLAGS,
...EXPLORER_FLAGS,
...GOVERNANCE_FLAGS,
};
}
function parseNetworks(value?: string) {
if (value) {
try {
return JSON.parse(value);
} catch (e) {
return {};
}
}
return {};
}
/**
* Provides a fallback ethereum provider url for test purposes in some apps
*/
function getEthereumProviderUrl(
network: Networks | undefined,
envvar: string | undefined
) {
if (envvar) return envvar;
return network === Networks.MAINNET
? 'https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8'
: 'https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8';
}
/**
* Provide a fallback etherscan url for test purposes in some apps
*/
function getEtherscanUrl(
network: Networks | undefined,
envvar: string | undefined
) {
if (envvar) return envvar;
return network === Networks.MAINNET
? 'https://etherscan.io'
: 'https://sepolia.etherscan.io';
}
export function windowOrDefault(key: string, defaultValue?: string) {
if (typeof window !== 'undefined') {
// @ts-ignore avoid conflict in env
if (window._env_ && window._env_[key]) {
// @ts-ignore presence has been check above
return window._env_[key];
}
}
return defaultValue || undefined;
}
export const ENV = compileEnvVars();
@@ -60,6 +60,8 @@ export const envSchema = z
TENDERMINT_WEBSOCKET_URL: z.optional(z.string()),
CHROME_EXTENSION_URL: z.optional(z.string()),
MOZILLA_EXTENSION_URL: z.optional(z.string()),
CHARTING_LIBRARY_PATH: z.optional(z.string()),
CHARTING_LIBRARY_HASH: z.optional(z.string()),
})
.refine(
(data) => {
+2
View File
@@ -11,6 +11,7 @@ import en_fills from './locales/en/fills.json';
import en_funding_payments from './locales/en/funding-payments.json';
import en_governance from './locales/en/governance.json';
import en_trading from './locales/en/trading.json';
import en_trading_view from './locales/en/trading-view.json';
import en_markets from './locales/en/markets.json';
import en_web3 from './locales/en/web3.json';
import en_proposals from './locales/en/proposals.json';
@@ -32,6 +33,7 @@ export const locales = {
'funding-payments': en_funding_payments,
governance: en_governance,
trading: en_trading,
trading_view: en_trading_view,
markets: en_markets,
web3: en_web3,
positions: en_positions,
@@ -1,5 +1,3 @@
{
"Indicators": "Indicators",
"Interval: {{interval}}": "Interval: {{interval}}",
"No open orders": "No open orders"
}
@@ -0,0 +1,4 @@
{
"Failed to initialize Trading view": "Failed to initialize Trading view",
"Loading Trading View": "Loading Trading View"
}
+13 -1
View File
@@ -26,9 +26,10 @@
"Best offer": "Best offer",
"Browse": "Browse",
"By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer</0>": "By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer</0>",
"Candles": "Candles",
"Chart": "Chart",
"Change (24h)": "Change (24h)",
"Chart": "Chart",
"Chart by <0>TradingView</0>": "Chart by <0>TradingView</0>",
"checkOutProposalsAndVote": "Check out the terms of the proposals and vote:",
"checkOutProposalsAndVote_one": "Check out the terms of the proposal and vote:",
"checkOutProposalsAndVote_other": "Check out the terms of the proposals and vote:",
@@ -120,7 +121,15 @@
"Improve vega console": "Improve vega console",
"Inactive": "Inactive",
"Index Price": "Index Price",
"Indicators": "Indicators",
"Infrastructure": "Infrastructure",
"Interval: {{interval}}": "Interval: {{interval}}",
"INTERVAL_I1M": "1m",
"INTERVAL_I5M": "5m",
"INTERVAL_I15M": "15m",
"INTERVAL_I1H": "1H",
"INTERVAL_I6H": "6H",
"INTERVAL_I1D": "1D",
"Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.": "Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.",
"Learn about providing liquidity": "Learn about providing liquidity",
"Learn more": "Learn more",
@@ -192,6 +201,7 @@
"pastEpochs": "Past {{count}} epochs",
"pastEpochs_one": "Past {{count}} epoch",
"pastEpochs_other": "Past {{count}} epochs",
"Pennant": "Pennant",
"Perpetuals": "Perpetuals",
"Please choose another market from the <0>market list</0>": "Please choose another market from the <0>market list</0>",
"Please connect Vega wallet": "Please connect Vega wallet",
@@ -291,6 +301,7 @@
"Trader": "Trader",
"Trades": "Trades",
"Trading": "Trading",
"TradingView": "TradingView",
"Trading has been terminated as a result of the product definition": "Trading has been terminated as a result of the product definition",
"Trading mode": "Trading mode",
"Trading on Market {{name}} may stop on {{date}}. There is open proposal to close this market.": "Trading on Market {{name}} may stop on {{date}}. There is open proposal to close this market.",
@@ -302,6 +313,7 @@
"totalCommission_other": "Total commission (last {{count}} epochs)",
"Unknown": "Unknown",
"Unknown settlement date": "Unknown settlement date",
"Vega chart": "Vega chart",
"Vega Reward pot": "Vega Reward pot",
"Vega Wallet <0>full featured<0>": "Vega Wallet <0>full featured<0>",
"Vesting": "Vesting",
+1 -1
View File
@@ -198,7 +198,7 @@ export const Orderbook = ({
);
return (
<div
className="overflow-hidden grid"
className="overflow-hidden grid relative"
data-testid="orderbook-grid-element"
style={{
width,
@@ -1,5 +1,5 @@
import {
FLAGS,
useFeatureFlags,
TokenStaticLinks,
useEnvironment,
} from '@vegaprotocol/environment';
@@ -90,6 +90,7 @@ export const MarketInfoAccordion = ({
market,
onSelect,
}: MarketInfoAccordionProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const t = useT();
const { VEGA_TOKEN_URL } = useEnvironment();
const headerClassName = 'uppercase text-lg';
@@ -352,7 +353,7 @@ export const MarketInfoAccordion = ({
</>
}
/>
{FLAGS.SUCCESSOR_MARKETS && (
{featureFlags.SUCCESSOR_MARKETS && (
<AccordionItem
itemId="succession-line"
title={t('Succession line')}
@@ -50,7 +50,7 @@ import {
import {
DApp,
EtherscanLink,
FLAGS,
useFeatureFlags,
TOKEN_PROPOSAL,
useEnvironment,
useLinks,
@@ -189,12 +189,13 @@ export const KeyDetailsInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const t = useT();
const { data: parentMarketIdData } = useParentMarketIdQuery({
variables: {
marketId: market.id,
},
skip: !FLAGS.SUCCESSOR_MARKETS,
skip: !featureFlags.SUCCESSOR_MARKETS,
});
const { data: successorProposalDetails } =
@@ -202,7 +203,7 @@ export const KeyDetailsInfoPanel = ({
variables: {
proposalId: market.proposal?.id || '',
},
skip: !FLAGS.SUCCESSOR_MARKETS || !market.proposal?.id,
skip: !featureFlags.SUCCESSOR_MARKETS || !market.proposal?.id,
});
// The following queries are needed as the parent market could also have been a successor market.
@@ -245,7 +246,7 @@ export const KeyDetailsInfoPanel = ({
</KeyValueTable>
<MarketInfoTable
data={
FLAGS.SUCCESSOR_MARKETS
featureFlags.SUCCESSOR_MARKETS
? {
name: market.tradableInstrument.instrument.name,
parentMarketID:
+3 -2
View File
@@ -11,7 +11,7 @@ import { useCallback } from 'react';
import * as Schema from '@vegaprotocol/types';
import { useVegaTransactionStore } from '@vegaprotocol/web3';
import { HALFMAXGOINT64 } from '@vegaprotocol/utils';
import { FLAGS } from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
interface PositionsManagerProps {
partyIds: string[];
@@ -28,10 +28,11 @@ export const PositionsManager = ({
gridProps,
showClosed = false,
}: PositionsManagerProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const t = useT();
const { pubKeys, pubKey } = useVegaWallet();
const create = useVegaTransactionStore((store) => store.create);
const disableClosePositionsButton = FLAGS.DISABLE_CLOSE_POSITION;
const disableClosePositionsButton = featureFlags.DISABLE_CLOSE_POSITION;
const onClose = useCallback(
({ marketId, openVolume }: { marketId: string; openVolume: string }) =>
+2 -1
View File
@@ -1,3 +1,4 @@
export * from './use-copy-timeout';
export * from './use-fetch';
export * from './use-local-storage';
export * from './use-mutation-observer';
@@ -11,4 +12,4 @@ export * from './use-theme-switcher';
export * from './use-storybook-theme-observer';
export * from './use-yesterday';
export * from './use-previous';
export * from './use-copy-timeout';
export { useScript } from './use-script';
@@ -0,0 +1,18 @@
import { renderHook, waitFor } from '@testing-library/react';
import { useScript } from './use-script';
describe('useScript', () => {
it('appends a script to the body', async () => {
const url = 'http://localhost:8080/foo.js';
const { result } = renderHook(() => useScript(url, 'integrity-hash'));
expect(result.current).toBe('loading');
await waitFor(() => {
const script = document.body.getElementsByTagName('script')[0];
expect(script).toBeInTheDocument();
expect(script).toHaveAttribute('src', url);
});
});
});
@@ -0,0 +1,57 @@
import { useEffect, useState } from 'react';
type State = 'idle' | 'loading' | 'ready' | 'error';
// keep record of loaded script state, so if the component re-mounts but the script
// was already appended its not re-appended and will have the correct state
type Url = string;
const cache: Record<Url, State> = {};
export const useScript = (url: string, integrity: string) => {
// track state of the script as it loads or possibly fails
const [state, setState] = useState<State>(url ? 'loading' : 'idle');
useEffect(() => {
if (!url) {
setState('idle');
return;
}
// Use the integrity hash of the script as an identifier
let script = document.getElementById(integrity) as HTMLScriptElement | null;
if (script) {
// script already on the page
setState(cache[url]);
} else {
// script not found, create and append script
script = document.createElement('script');
script.id = integrity;
script.src = url;
script.async = true;
script.crossOrigin = 'anonymous'; // make sure sri is respected with cross origin request
script.integrity = `sha256-${integrity}`;
document.body.appendChild(script);
}
// Setup/teardown listeners to notify component when script has loaded
const _setState = (event: Event) => {
const result = event.type === 'load' ? 'ready' : 'error';
setState(result);
cache[url] = result;
};
script.addEventListener('load', _setState);
script.addEventListener('error', _setState);
return () => {
if (script) {
script.removeEventListener('load', _setState);
script.removeEventListener('error', _setState);
}
};
}, [url, integrity]);
return state;
};
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+18
View File
@@ -0,0 +1,18 @@
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
+7
View File
@@ -0,0 +1,7 @@
# trading-view
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test trading-view` to execute the unit tests via [Jest](https://jestjs.io).
+12
View File
@@ -0,0 +1,12 @@
/* eslint-disable */
export default {
displayName: 'trading-view',
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/trading-view',
setupFilesAfterEnv: ['./src/setup-tests.ts'],
};
+4
View File
@@ -0,0 +1,4 @@
{
"name": "trading-view",
"version": "0.0.1"
}
+43
View File
@@ -0,0 +1,43 @@
{
"name": "trading-view",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/trading-view/src",
"projectType": "library",
"tags": [],
"targets": {
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/trading-view/**/*.{ts,tsx,js,jsx}"]
}
},
"build": {
"executor": "@nx/rollup:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/trading-view",
"tsConfig": "libs/trading-view/tsconfig.lib.json",
"project": "libs/trading-view/package.json",
"entryFile": "libs/trading-view/src/index.ts",
"external": ["react", "react-dom", "react/jsx-runtime"],
"rollupConfig": "@nx/react/plugins/bundle-rollup",
"compiler": "babel",
"assets": [
{
"glob": "libs/trading-view/README.md",
"input": ".",
"output": "."
}
]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/trading-view/jest.config.ts"
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
export { TradingViewContainer } from './lib/trading-view-container';
export {
ALLOWED_TRADINGVIEW_HOSTNAMES,
TRADINGVIEW_INTERVAL_MAP,
} from './lib/constants';
+40
View File
@@ -0,0 +1,40 @@
fragment Bar on Candle {
periodStart
lastUpdateInPeriod
high
low
open
close
volume
}
query GetBars(
$marketId: ID!
$interval: Interval!
$since: String!
$to: String
) {
market(id: $marketId) {
id
decimalPlaces
positionDecimalPlaces
candlesConnection(
interval: $interval
since: $since
to: $to
pagination: { last: 5000 }
) {
edges {
node {
...Bar
}
}
}
}
}
subscription LastBar($marketId: ID!, $interval: Interval!) {
candles(marketId: $marketId, interval: $interval) {
...Bar
}
}
+24
View File
@@ -0,0 +1,24 @@
query Symbol($marketId: ID!) {
market(id: $marketId) {
id
decimalPlaces
positionDecimalPlaces
tradableInstrument {
instrument {
code
name
metadata {
tags
}
product {
... on Future {
__typename
}
... on Perpetual {
__typename
}
}
}
}
}
}
+119
View File
@@ -0,0 +1,119 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type BarFragment = { __typename?: 'Candle', periodStart: any, lastUpdateInPeriod: any, high: string, low: string, open: string, close: string, volume: string };
export type GetBarsQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
interval: Types.Interval;
since: Types.Scalars['String'];
to?: Types.InputMaybe<Types.Scalars['String']>;
}>;
export type GetBarsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, candlesConnection?: { __typename?: 'CandleDataConnection', edges?: Array<{ __typename?: 'CandleEdge', node: { __typename?: 'Candle', periodStart: any, lastUpdateInPeriod: any, high: string, low: string, open: string, close: string, volume: string } } | null> | null } | null } | null };
export type LastBarSubscriptionVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
interval: Types.Interval;
}>;
export type LastBarSubscription = { __typename?: 'Subscription', candles: { __typename?: 'Candle', periodStart: any, lastUpdateInPeriod: any, high: string, low: string, open: string, close: string, volume: string } };
export const BarFragmentDoc = gql`
fragment Bar on Candle {
periodStart
lastUpdateInPeriod
high
low
open
close
volume
}
`;
export const GetBarsDocument = gql`
query GetBars($marketId: ID!, $interval: Interval!, $since: String!, $to: String) {
market(id: $marketId) {
id
decimalPlaces
positionDecimalPlaces
candlesConnection(
interval: $interval
since: $since
to: $to
pagination: {last: 5000}
) {
edges {
node {
...Bar
}
}
}
}
}
${BarFragmentDoc}`;
/**
* __useGetBarsQuery__
*
* To run a query within a React component, call `useGetBarsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetBarsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetBarsQuery({
* variables: {
* marketId: // value for 'marketId'
* interval: // value for 'interval'
* since: // value for 'since'
* to: // value for 'to'
* },
* });
*/
export function useGetBarsQuery(baseOptions: Apollo.QueryHookOptions<GetBarsQuery, GetBarsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetBarsQuery, GetBarsQueryVariables>(GetBarsDocument, options);
}
export function useGetBarsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetBarsQuery, GetBarsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetBarsQuery, GetBarsQueryVariables>(GetBarsDocument, options);
}
export type GetBarsQueryHookResult = ReturnType<typeof useGetBarsQuery>;
export type GetBarsLazyQueryHookResult = ReturnType<typeof useGetBarsLazyQuery>;
export type GetBarsQueryResult = Apollo.QueryResult<GetBarsQuery, GetBarsQueryVariables>;
export const LastBarDocument = gql`
subscription LastBar($marketId: ID!, $interval: Interval!) {
candles(marketId: $marketId, interval: $interval) {
...Bar
}
}
${BarFragmentDoc}`;
/**
* __useLastBarSubscription__
*
* To run a query within a React component, call `useLastBarSubscription` and pass it any options that fit your needs.
* When your component renders, `useLastBarSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useLastBarSubscription({
* variables: {
* marketId: // value for 'marketId'
* interval: // value for 'interval'
* },
* });
*/
export function useLastBarSubscription(baseOptions: Apollo.SubscriptionHookOptions<LastBarSubscription, LastBarSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<LastBarSubscription, LastBarSubscriptionVariables>(LastBarDocument, options);
}
export type LastBarSubscriptionHookResult = ReturnType<typeof useLastBarSubscription>;
export type LastBarSubscriptionResult = Apollo.SubscriptionResult<LastBarSubscription>;
+67
View File
@@ -0,0 +1,67 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type SymbolQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type SymbolQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', code: string, name: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename: 'Future' } | { __typename: 'Perpetual' } | { __typename?: 'Spot' } } } } | null };
export const SymbolDocument = gql`
query Symbol($marketId: ID!) {
market(id: $marketId) {
id
decimalPlaces
positionDecimalPlaces
tradableInstrument {
instrument {
code
name
metadata {
tags
}
product {
... on Future {
__typename
}
... on Perpetual {
__typename
}
}
}
}
}
}
`;
/**
* __useSymbolQuery__
*
* To run a query within a React component, call `useSymbolQuery` and pass it any options that fit your needs.
* When your component renders, `useSymbolQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useSymbolQuery({
* variables: {
* marketId: // value for 'marketId'
* },
* });
*/
export function useSymbolQuery(baseOptions: Apollo.QueryHookOptions<SymbolQuery, SymbolQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<SymbolQuery, SymbolQueryVariables>(SymbolDocument, options);
}
export function useSymbolLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SymbolQuery, SymbolQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<SymbolQuery, SymbolQueryVariables>(SymbolDocument, options);
}
export type SymbolQueryHookResult = ReturnType<typeof useSymbolQuery>;
export type SymbolLazyQueryHookResult = ReturnType<typeof useSymbolLazyQuery>;
export type SymbolQueryResult = Apollo.QueryResult<SymbolQuery, SymbolQueryVariables>;
+24
View File
@@ -0,0 +1,24 @@
import { Interval } from '@vegaprotocol/types';
export const ALLOWED_TRADINGVIEW_HOSTNAMES = [
'localhost',
'vegafairground.eth.limo',
'vegafairground.eth',
'vegaprotocol.eth',
'vegaprotocol.eth.limo',
];
export const CHARTING_LIBRARY_FILE = 'charting_library.standalone.js';
export const TRADINGVIEW_INTERVAL_MAP = {
[Interval.INTERVAL_BLOCK]: undefined, // TODO: handle block tick
[Interval.INTERVAL_I1M]: '1',
[Interval.INTERVAL_I5M]: '5',
[Interval.INTERVAL_I15M]: '15',
[Interval.INTERVAL_I1H]: '60',
[Interval.INTERVAL_I6H]: '360',
[Interval.INTERVAL_I1D]: '1D',
} as const;
export type ResolutionRecord = typeof TRADINGVIEW_INTERVAL_MAP;
export type ResolutionString = ResolutionRecord[keyof ResolutionRecord];
@@ -0,0 +1,56 @@
import { render, screen } from '@testing-library/react';
import { TradingViewContainer } from './trading-view-container';
import * as useScriptModule from '@vegaprotocol/react-helpers';
import { CHARTING_LIBRARY_FILE } from './constants';
jest.mock('./trading-view', () => ({
TradingView: ({ marketId }: { marketId: string }) => (
<div data-testid="trading-view">{marketId}</div>
),
}));
describe('TradingView', () => {
const props = {
libraryPath: 'foo',
libraryHash: 'hash',
marketId: 'marketId',
};
const renderComponent = () => render(<TradingViewContainer {...props} />);
it.each(['loading', 'idle'])(
'renders loading state when script is %s',
(state: string) => {
const spyOnScript = jest
.spyOn(useScriptModule, 'useScript')
.mockReturnValue(state as 'idle' | 'loading');
renderComponent();
expect(screen.getByText('Loading Trading View')).toBeInTheDocument();
expect(spyOnScript).toHaveBeenCalledWith(
props.libraryPath + CHARTING_LIBRARY_FILE,
props.libraryHash
);
}
);
it('renders error state if script fails to load', () => {
jest.spyOn(useScriptModule, 'useScript').mockReturnValue('error');
renderComponent();
expect(
screen.getByText('Failed to initialize Trading view')
).toBeInTheDocument();
});
it('renders TradingView if script loads successfully', () => {
jest.spyOn(useScriptModule, 'useScript').mockReturnValue('ready');
renderComponent();
expect(screen.getByTestId('trading-view')).toHaveTextContent(
props.marketId
);
});
});
@@ -0,0 +1,56 @@
import { useScript } from '@vegaprotocol/react-helpers';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useT } from './use-t';
import { TradingView, type OnAutoSaveNeededCallback } from './trading-view';
import { CHARTING_LIBRARY_FILE, type ResolutionString } from './constants';
export const TradingViewContainer = ({
libraryPath,
libraryHash,
marketId,
interval,
studies,
onIntervalChange,
onAutoSaveNeeded,
}: {
libraryPath: string;
libraryHash: string;
marketId: string;
interval: ResolutionString;
studies: string[];
onIntervalChange: (interval: string) => void;
onAutoSaveNeeded: OnAutoSaveNeededCallback;
}) => {
const t = useT();
const scriptState = useScript(
libraryPath + CHARTING_LIBRARY_FILE,
libraryHash
);
if (scriptState === 'loading' || scriptState === 'idle') {
return (
<Splash>
<p>{t('Loading Trading View')}</p>
</Splash>
);
}
if (scriptState === 'error') {
return (
<Splash>
<p>{t('Failed to initialize Trading view')}</p>
</Splash>
);
}
return (
<TradingView
libraryPath={libraryPath}
marketId={marketId}
interval={interval}
studies={studies}
onIntervalChange={onIntervalChange}
onAutoSaveNeeded={onAutoSaveNeeded}
/>
);
};
+131
View File
@@ -0,0 +1,131 @@
import { useEffect, useRef } from 'react';
import {
useScreenDimensions,
useThemeSwitcher,
} from '@vegaprotocol/react-helpers';
import { useLanguage } from './use-t';
import { useDatafeed } from './use-datafeed';
import { type ResolutionString } from './constants';
export type OnAutoSaveNeededCallback = (data: { studies: string[] }) => void;
export const TradingView = ({
marketId,
libraryPath,
interval,
studies,
onIntervalChange,
onAutoSaveNeeded,
}: {
marketId: string;
libraryPath: string;
interval: ResolutionString;
studies: string[];
onIntervalChange: (interval: string) => void;
onAutoSaveNeeded: OnAutoSaveNeededCallback;
}) => {
const { isMobile } = useScreenDimensions();
const { theme } = useThemeSwitcher();
const language = useLanguage();
const chartContainerRef =
useRef<HTMLDivElement>() as React.MutableRefObject<HTMLInputElement>;
// Cant get types as charting_library is externally loaded
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const widgetRef = useRef<any>();
const datafeed = useDatafeed();
useEffect(
() => {
const disableOnSmallScreens = isMobile ? ['left_toolbar'] : [];
const overrides = getOverrides(theme);
const widgetOptions = {
symbol: marketId,
datafeed,
interval: interval,
container: chartContainerRef.current,
library_path: libraryPath,
custom_css_url: 'vega_styles.css',
// Trading view accepts just 'en' rather than 'en-US' which is what react-i18next provides
// https://www.tradingview.com/charting-library-docs/latest/core_concepts/Localization?_highlight=language#supported-languages
locale: language.split('-')[0],
enabled_features: ['tick_resolution'],
disabled_features: [
'header_symbol_search',
'header_compare',
'show_object_tree',
'timeframes_toolbar',
...disableOnSmallScreens,
],
fullscreen: false,
autosize: true,
theme,
overrides,
loading_screen: {
backgroundColor: overrides['paneProperties.background'],
},
};
// @ts-ignore parent component loads TradingView onto window obj
widgetRef.current = new window.TradingView.widget(widgetOptions);
widgetRef.current.onChartReady(() => {
widgetRef.current.applyOverrides(getOverrides(theme));
widgetRef.current.subscribe('onAutoSaveNeeded', () => {
const studies = widgetRef.current
.activeChart()
.getAllStudies()
.map((s: { id: string; name: string }) => s.name);
onAutoSaveNeeded({ studies });
});
const activeChart = widgetRef.current.activeChart();
// Show volume study by default, second bool arg adds it as a overlay on top of the chart
studies.forEach((study) => {
const asOverlay = study === 'Volume';
activeChart.createStudy(study, asOverlay);
});
// Subscribe to interval changes so it can be persisted in chart settings
activeChart.onIntervalChanged().subscribe(null, onIntervalChange);
});
return () => {
if (!widgetRef.current) return;
widgetRef.current.remove();
};
},
// No theme in deps to avoid full chart reload when the theme changes
// Instead the theme is changed programmitcally in a separate useEffect
// eslint-disable-next-line react-hooks/exhaustive-deps
[datafeed, marketId, language, libraryPath, isMobile]
);
// Update the trading view theme every time the app theme updates, doen separately
// to avoid full chart reload
useEffect(() => {
if (!widgetRef.current || !widgetRef.current._ready) return;
// Calling changeTheme will reset the default dark/light background to the TV default
// so we need to re-apply the pane bg override. A promise is also required
// https://github.com/tradingview/charting_library/issues/6546#issuecomment-1139517908
widgetRef.current.changeTheme(theme).then(() => {
widgetRef.current.applyOverrides(getOverrides(theme));
});
}, [theme]);
return <div ref={chartContainerRef} className="w-full h-full" />;
};
const getOverrides = (theme: 'dark' | 'light') => {
return {
// colors set here, trading view lets the user set a color
'paneProperties.background': theme === 'dark' ? '#05060C' : '#fff',
'paneProperties.backgroundType': 'solid',
};
};
+298
View File
@@ -0,0 +1,298 @@
import { useEffect, useMemo, useRef } from 'react';
import compact from 'lodash/compact';
import { useApolloClient } from '@apollo/client';
import { type Subscription } from 'zen-observable-ts';
/*
* TODO: figure out how we can get the chart types
import {
type LibrarySymbolInfo,
type IBasicDataFeed,
type ResolutionString,
type SeriesFormat,
} from '../charting_library/charting_library';
*/
import {
GetBarsDocument,
LastBarDocument,
type BarFragment,
type GetBarsQuery,
type GetBarsQueryVariables,
type LastBarSubscription,
type LastBarSubscriptionVariables,
} from './__generated__/Bars';
import { Interval } from '@vegaprotocol/types';
import {
SymbolDocument,
type SymbolQuery,
type SymbolQueryVariables,
} from './__generated__/Symbol';
import { getMarketExpiryDate, toBigNum } from '@vegaprotocol/utils';
const EXCHANGE = 'VEGA';
const resolutionMap: Record<string, Interval> = {
'1T': Interval.INTERVAL_BLOCK,
'1': Interval.INTERVAL_I1M,
'5': Interval.INTERVAL_I5M,
'15': Interval.INTERVAL_I15M,
'60': Interval.INTERVAL_I1H,
'360': Interval.INTERVAL_I6H,
'1D': Interval.INTERVAL_I1D,
} as const;
const supportedResolutions = Object.keys(resolutionMap);
const configurationData = {
// only showing Vega ofc
exchanges: [EXCHANGE],
// Represents the resolutions for bars supported by your datafeed
// @ts-ignore cant import types as chartin_library is external
supported_resolutions: supportedResolutions as ResolutionString[],
} as const;
export const useDatafeed = () => {
const hasHistory = useRef(false);
const subRef = useRef<Subscription>();
const client = useApolloClient();
const datafeed = useMemo(() => {
// @ts-ignore cant import types as chartin_library is external
const feed: IBasicDataFeed = {
// @ts-ignore cant import types as chartin_library is external
onReady: (callback) => {
setTimeout(() => callback(configurationData));
},
searchSymbols: () => {
/* no op, we handle finding markets in app */
},
resolveSymbol: async (
// @ts-ignore cant import types as chartin_library is external
marketId,
// @ts-ignore cant import types as chartin_library is external
onSymbolResolvedCallback,
// @ts-ignore cant import types as chartin_library is external
onResolveErrorCallback
) => {
try {
const result = await client.query<SymbolQuery, SymbolQueryVariables>({
query: SymbolDocument,
variables: {
marketId,
},
});
if (!result.data.market) {
onResolveErrorCallback('Cannot resolve symbol: market not found');
return;
}
const market = result.data.market;
const instrument = market.tradableInstrument.instrument;
const productType = instrument.product.__typename;
if (!productType) {
onResolveErrorCallback(
'Cannot resolve symbol: invalid product type'
);
return;
}
let type = 'undefined'; // https://www.tradingview.com/charting-library-docs/latest/api/modules/Charting_Library#symboltype
if (productType === 'Future' || productType === 'Perpetual') {
type = 'futures';
} else if (productType === 'Spot') {
type = 'spot';
}
const expirationDate = getMarketExpiryDate(instrument.metadata.tags);
const expirationTimestamp = expirationDate
? Math.floor(expirationDate.getTime() / 1000)
: null;
// @ts-ignore cant import types as chartin_library is external
const symbolInfo: LibrarySymbolInfo = {
ticker: market.id, // use ticker as our unique identifier so that code/name can be used for name/description
name: instrument.code,
full_name: `${EXCHANGE}:${instrument.code}`,
description: instrument.name,
listed_exchange: EXCHANGE,
expired: productType === 'Perpetual' ? false : true,
expirationDate: expirationTimestamp,
// @ts-ignore cant import types as chartin_library is external
format: 'price' as SeriesFormat,
type,
session: '24x7',
timezone: 'Etc/UTC',
exchange: EXCHANGE,
minmov: 1,
pricescale: Number('1' + '0'.repeat(market.decimalPlaces)), // for number of decimal places
visible_plots_set: 'ohlc',
volume_precision: market.positionDecimalPlaces,
data_status: 'streaming',
delay: 1000, // around 1 block time
has_intraday: true, // required for less than 1 day interval
has_empty_bars: true, // library will generate bars if there are gaps, useful for auctions
has_ticks: false, // switch to true when enabling block intervals
// @ts-ignore required for data conversion
vegaDecimalPlaces: market.decimalPlaces,
// @ts-ignore required for data conversion
vegaPositionDecimalPlaces: market.positionDecimalPlaces,
};
onSymbolResolvedCallback(symbolInfo);
} catch (err) {
onResolveErrorCallback('Cannot resolve symbol');
}
},
getBars: async (
// @ts-ignore cant import types as chartin_library is external
symbolInfo,
// @ts-ignore cant import types as chartin_library is external
resolution,
// @ts-ignore cant import types as chartin_library is external
periodParams,
// @ts-ignore cant import types as chartin_library is external
onHistoryCallback,
// @ts-ignore cant import types as chartin_library is external
onErrorCallback
) => {
if (!symbolInfo.ticker) {
onErrorCallback('No symbol.ticker');
return;
}
try {
const result = await client.query<
GetBarsQuery,
GetBarsQueryVariables
>({
query: GetBarsDocument,
variables: {
marketId: symbolInfo.ticker,
since: unixTimestampToDate(periodParams.from).toISOString(),
to: unixTimestampToDate(periodParams.to).toISOString(),
interval: resolutionMap[resolution],
},
});
const candleEdges = compact(
result.data.market?.candlesConnection?.edges
);
if (!candleEdges.length) {
onHistoryCallback([], { noData: true });
return;
}
const bars = candleEdges.map((edge) => {
return prepareBar(
edge.node,
// @ts-ignore added in resolveSymbol
symbolInfo.vegaDecimalPlaces,
// @ts-ignore added in resolveSymbol
symbolInfo.vegaPositionDecimalPlaces
);
});
hasHistory.current = true;
onHistoryCallback(bars, { noData: false });
} catch (err) {
onErrorCallback(
err instanceof Error ? err.message : 'Failed to get bars'
);
}
},
subscribeBars: (
// @ts-ignore cant import types as chartin_library is external
symbolInfo,
// @ts-ignore cant import types as chartin_library is external
resolution,
// @ts-ignore cant import types as chartin_library is external
onTick
// subscriberUID, // chart will subscribe and unsbuscribe when the parent market of the page changes so we don't need to use subscriberUID as of now
) => {
if (!symbolInfo.ticker) {
throw new Error('No symbolInfo.ticker');
}
// Dont start the subscription if there is no candle history. This protects against a
// problem where drawing on the chart throws an error if there is no prior history, instead
// no you'll just get the no data message
if (!hasHistory.current) {
return;
}
subRef.current = client
.subscribe<LastBarSubscription, LastBarSubscriptionVariables>({
query: LastBarDocument,
variables: {
marketId: symbolInfo.ticker,
interval: resolutionMap[resolution],
},
})
.subscribe(({ data }) => {
if (data) {
const bar = prepareBar(
data.candles,
// @ts-ignore added in resolveSymbol
symbolInfo.vegaDecimalPlaces,
// @ts-ignore added in resolveSymbol
symbolInfo.vegaPositionDecimalPlaces
);
onTick(bar);
}
});
},
/**
* We only have one active subscription no need to use the uid provided by unsubscribeBars
*/
unsubscribeBars: () => {
if (subRef.current) {
subRef.current.unsubscribe();
}
},
};
return feed;
}, [client]);
useEffect(() => {
return () => {
if (subRef.current) {
subRef.current.unsubscribe();
}
};
}, []);
return datafeed;
};
const prepareBar = (
bar: BarFragment,
decimalPlaces: number,
positionDecimalPlaces: number
) => {
return {
time: new Date(bar.periodStart).getTime(),
low: toBigNum(bar.low, decimalPlaces).toNumber(),
high: toBigNum(bar.high, decimalPlaces).toNumber(),
open: toBigNum(bar.open, decimalPlaces).toNumber(),
close: toBigNum(bar.close, decimalPlaces).toNumber(),
volume: toBigNum(bar.volume, positionDecimalPlaces).toNumber(),
};
};
const unixTimestampToDate = (timestamp: number) => {
return new Date(timestamp * 1000);
};
+4
View File
@@ -0,0 +1,4 @@
import { useTranslation } from 'react-i18next';
export const ns = 'trading-view';
export const useT = () => useTranslation(ns).t;
export const useLanguage = () => useTranslation(ns).i18n.language;
+15
View File
@@ -0,0 +1,15 @@
import '@testing-library/jest-dom';
import { locales } from '@vegaprotocol/i18n';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
// Set up i18n instance so that components have the correct default
// en translations
i18n.use(initReactI18next).init({
// we init with resources
resources: locales,
fallbackLng: 'en',
ns: ['trading-view'],
defaultNS: 'trading-view',
});
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json"
}
+24
View File
@@ -0,0 +1,24 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts"
]
},
"exclude": [
"jest.config.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts",
"src/**/*.spec.tsx",
"src/**/*.test.tsx",
"src/**/*.spec.js",
"src/**/*.test.js",
"src/**/*.spec.jsx",
"src/**/*.test.jsx"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}
+20
View File
@@ -0,0 +1,20 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node", "@testing-library/jest-dom"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}
+1 -2
View File
@@ -89,7 +89,6 @@ export const Tabs = ({
ref={menuRef}
className={classNames('flex-1 p-1', {
'bg-vega-clight-700 dark:bg-vega-cdark-700': wrapped,
'': wrapped,
})}
>
{Children.map(children, (child) => {
@@ -97,7 +96,7 @@ export const Tabs = ({
return (
<TabsPrimitive.Content
value={child.props.id}
className={classNames('flex flex-nowrap gap-1', {
className={classNames('flex items-center flex-nowrap gap-1', {
'justify-end': !wrapped,
})}
>
+3 -2
View File
@@ -146,6 +146,7 @@
"@storybook/react-webpack5": "7.5.3",
"@svgr/rollup": "^8.0.1",
"@svgr/webpack": "^6.1.2",
"@swc-node/register": "~1.6.7",
"@swc/cli": "^0.1.62",
"@swc/core": "~1.3.85",
"@swc/jest": "0.2.20",
@@ -161,9 +162,9 @@
"@types/lodash": "^4.14.171",
"@types/node": "18.14.2",
"@types/prismjs": "^1.26.0",
"@types/react": "18.2.24",
"@types/react": "18.2.33",
"@types/react-copy-to-clipboard": "5.0.7",
"@types/react-dom": "18.2.9",
"@types/react-dom": "18.2.14",
"@types/react-router-dom": "^5.3.3",
"@types/react-syntax-highlighter": "^15.5.5",
"@types/react-virtualized-auto-sizer": "^1.0.1",
+32 -19
View File
@@ -1,6 +1,14 @@
# Chart
## Display options
## Chart lib type
- **Must** be able to view the Vega chart by default
- **Must** be able to switch to the TradingView chart
- **Must** have the interval persisted between chart types
## Pennant chart
### Display options
- **Must** be able to change time interval from a list of intervals (<a name="6007-CHAR-001" href="#6007-CHAR-001">6007-CHAR-001</a>)
- 1m
@@ -36,7 +44,7 @@
- **Must** be able to add multiple overlays at the same time (<a name="6007-CHAR-008" href="#6007-CHAR-008">6007-CHAR-008</a>)
- **Must** be able to close any overlays selected (<a name="6007-CHAR-009" href="#6007-CHAR-009">6007-CHAR-009</a>)
## Price and Time
### Price and Time
- **Must** see details of price from where my mouse cursor is on the chart(<a name="6007-CHAR-010" href="#6007-CHAR-010">6007-CHAR-010</a>)
@@ -54,13 +62,13 @@
- **Must** y axis shows price range(<a name="6007-CHAR-014" href="#6007-CHAR-014">6007-CHAR-014</a>)
- **Must** show the last price line on the Y axis (<a name="6007-CHAR-015" href="#6007-CHAR-015">6007-CHAR-015</a>)
# Display Types
### Display Types
## Mountain
#### Mountain
- **Must** show area line chart with the line being at the last price (<a name="6007-CHAR-016" href="#6007-CHAR-016">6007-CHAR-016</a>)
## Candlestick
#### Candlestick
- **Must** body is green if the close is higher than the open (<a name="6007-CHAR-017" href="#6007-CHAR-017">6007-CHAR-017</a>)
- **Must** body is red if the close is lower than the open (<a name="6007-CHAR-018" href="#6007-CHAR-018">6007-CHAR-018</a>)
@@ -69,11 +77,11 @@
- **Must** show low price (<a name="6007-CHAR-021" href="#6007-CHAR-021">6007-CHAR-021</a>)
- **Must** show close price (<a name="6007-CHAR-022" href="#6007-CHAR-022">6007-CHAR-022</a>)
## Line
#### Line
- **Must** show line on the chart with the line being the the last price (<a name="6007-CHAR-023" href="#6007-CHAR-023">6007-CHAR-023</a>)
## OHLC
#### OHLC
- **Must** show open price (<a name="6007-CHAR-024" href="#6007-CHAR-024">6007-CHAR-024</a>)
- **Must** show high price (<a name="6007-CHAR-025" href="#6007-CHAR-025">6007-CHAR-025</a>)
@@ -82,63 +90,68 @@
- **Must** show in green if the close is higher than the open (<a name="6007-CHAR-028" href="#6007-CHAR-028">6007-CHAR-028</a>)
- **Must** show in red if the close is lower than the open (<a name="6007-CHAR-029" href="#6007-CHAR-029">6007-CHAR-029</a>)
# Overlays
### Overlays
## Bollinger bands
#### Bollinger bands
- **Must** show upper band (<a name="6007-CHAR-030" href="#6007-CHAR-030">6007-CHAR-030</a>)
- **Must** show lower band (<a name="6007-CHAR-031" href="#6007-CHAR-031">6007-CHAR-031</a>)
- **Must** show band values at time of cursor position (<a name="6007-CHAR-032" href="#6007-CHAR-032">6007-CHAR-032</a>)
## Envelope
#### Envelope
- **Must** show upper line (<a name="6007-CHAR-033" href="#6007-CHAR-033">6007-CHAR-033</a>)
- **Must** show lower line (<a name="6007-CHAR-034" href="#6007-CHAR-034">6007-CHAR-034</a>)
- **Must** show line values at time of cursor position (<a name="6007-CHAR-035" href="#6007-CHAR-035">6007-CHAR-035</a>)
## EMA
#### EMA
- **Must** show line (<a name="6007-CHAR-036" href="#6007-CHAR-036">6007-CHAR-036</a>)
- **Must** show line value at time of cursor position (<a name="6007-CHAR-037" href="#6007-CHAR-037">6007-CHAR-037</a>)
## Moving Average
#### Moving Average
- **Must** show line (<a name="6007-CHAR-038" href="#6007-CHAR-038">6007-CHAR-038</a>)
- **Must** show line value at time of cursor position (<a name="6007-CHAR-039" href="#6007-CHAR-039">6007-CHAR-039</a>)
## Price monitoring bounds
#### Price monitoring bounds
- **Must** show min line (<a name="6007-CHAR-040" href="#6007-CHAR-040">6007-CHAR-040</a>)
- **Must** show max line (<a name="6007-CHAR-041" href="#6007-CHAR-041">6007-CHAR-041</a>)
- **Must** show reference line (<a name="6007-CHAR-042" href="#6007-CHAR-042">6007-CHAR-042</a>)
- **Must** show line values at time of cursor position (<a name="6007-CHAR-043" href="#6007-CHAR-043">6007-CHAR-043</a>)
# Studies
### Studies
## Eldar-ray
#### Eldar-ray
- **Must** show bear power line (<a name="6007-CHAR-044" href="#6007-CHAR-044">6007-CHAR-044</a>)
- **Must** show bull power line (<a name="6007-CHAR-045" href="#6007-CHAR-045">6007-CHAR-045</a>)
- **Must** show line values at time of cursor position (<a name="6007-CHAR-046" href="#6007-CHAR-046">6007-CHAR-046</a>)
## Force index
#### Force index
- **Must** show force line (<a name="6007-CHAR-047" href="#6007-CHAR-047">6007-CHAR-047</a>)
- **Must** show line value at time of cursor position (<a name="6007-CHAR-048" href="#6007-CHAR-048">6007-CHAR-048</a>)
## MACD
#### MACD
- **Must** show MACD line (<a name="6007-CHAR-049" href="#6007-CHAR-049">6007-CHAR-049</a>)
- **Must** show signal line (<a name="6007-CHAR-050" href="#6007-CHAR-050">6007-CHAR-050</a>)
- **Must** show histogram (<a name="6007-CHAR-051" href="#6007-CHAR-051">6007-CHAR-051</a>)
- **Must** show line values at time of cursor position (<a name="6007-CHAR-052" href="#6007-CHAR-052">6007-CHAR-052</a>)
## RSI
#### RSI
- **Must** show RSI line (<a name="6007-CHAR-053" href="#6007-CHAR-053">6007-CHAR-053</a>)
- **Must** show line value at time of cursor position (<a name="6007-CHAR-054" href="#6007-CHAR-054">6007-CHAR-054</a>)
## Volume
#### Volume
- **Must** show volume bars (<a name="6007-CHAR-055" href="#6007-CHAR-055">6007-CHAR-055</a>)
- **Must** show bar value at time of cursor position (<a name="6007-CHAR-056" href="#6007-CHAR-056">6007-CHAR-056</a>)
## TradingView
- **Must** persist interval in chart settings
- **Must** must show an attribution to trading view
+1
View File
@@ -51,6 +51,7 @@
],
"@vegaprotocol/tendermint": ["libs/tendermint/src/index.ts"],
"@vegaprotocol/trades": ["libs/trades/src/index.ts"],
"@vegaprotocol/trading-view": ["libs/trading-view/src/index.ts"],
"@vegaprotocol/types": ["libs/types/src/index.ts"],
"@vegaprotocol/ui-toolkit": ["libs/ui-toolkit/src/index.ts"],
"@vegaprotocol/utils": ["libs/utils/src/index.ts"],
+35 -10
View File
@@ -6643,6 +6643,31 @@
"@svgr/plugin-jsx" "8.1.0"
"@svgr/plugin-svgo" "8.1.0"
"@swc-node/core@^1.10.6":
version "1.10.6"
resolved "https://registry.yarnpkg.com/@swc-node/core/-/core-1.10.6.tgz#5c2af68bd4c9c8f5d91178a724af341a4402f5b6"
integrity sha512-lDIi/rPosmKIknWzvs2/Fi9zWRtbkx8OJ9pQaevhsoGzJSal8Pd315k1W5AIrnknfdAB4HqRN12fk6AhqnrEEw==
"@swc-node/register@~1.6.7":
version "1.6.8"
resolved "https://registry.yarnpkg.com/@swc-node/register/-/register-1.6.8.tgz#4e2402b42ae5b538d5041e0c4d86d9c3c8d5b323"
integrity sha512-74ijy7J9CWr1Z88yO+ykXphV29giCrSpANQPQRooE0bObpkTO1g4RzQovIfbIaniBiGDDVsYwDoQ3FIrCE8HcQ==
dependencies:
"@swc-node/core" "^1.10.6"
"@swc-node/sourcemap-support" "^0.3.0"
colorette "^2.0.19"
debug "^4.3.4"
pirates "^4.0.5"
tslib "^2.5.0"
"@swc-node/sourcemap-support@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@swc-node/sourcemap-support/-/sourcemap-support-0.3.0.tgz#e8a0d139bd3e8db39635f63fde43dbe6c39237cc"
integrity sha512-gqBJSmJMWomZFxlppaKea7NeAqFrDrrS0RMt24No92M3nJWcyI9YKGEQKl+EyJqZ5gh6w1s0cTklMHMzRwA1NA==
dependencies:
source-map-support "^0.5.21"
tslib "^2.5.0"
"@swc/cli@^0.1.62":
version "0.1.62"
resolved "https://registry.yarnpkg.com/@swc/cli/-/cli-0.1.62.tgz#6442fde2fcf75175a300fb4fcf30f8c60bbb3ab3"
@@ -7415,10 +7440,10 @@
dependencies:
"@types/react" "*"
"@types/react-dom@18.2.9":
version "18.2.9"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.9.tgz#c4ce3c7c91a134e1bff58692aa2d2f2f4029c38b"
integrity sha512-6nNhVzZ9joQ6F7lozrASuQKC0Kf6ArYMU+DqA2ZrUbB+d+9lC6ZLn1GxiEBI1edmAwvTULtuJ6uPZpv3XudwUg==
"@types/react-dom@18.2.14":
version "18.2.14"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.14.tgz#c01ba40e5bb57fc1dc41569bb3ccdb19eab1c539"
integrity sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==
dependencies:
"@types/react" "*"
@@ -7484,10 +7509,10 @@
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/react@18.2.24":
version "18.2.24"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.24.tgz#3c7d68c02e0205a472f04abe4a0c1df35d995c05"
integrity sha512-Ee0Jt4sbJxMu1iDcetZEIKQr99J1Zfb6D4F3qfUWoR1JpInkY1Wdg4WwCyBjL257D0+jGqSl1twBjV8iCaC0Aw==
"@types/react@18.2.33":
version "18.2.33"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.33.tgz#055356243dc4350a9ee6c6a2c07c5cae12e38877"
integrity sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@@ -10228,7 +10253,7 @@ colorette@^1.1.0:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
colorette@^2.0.10, colorette@^2.0.16, colorette@^2.0.20:
colorette@^2.0.10, colorette@^2.0.16, colorette@^2.0.19, colorette@^2.0.20:
version "2.0.20"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
@@ -20355,7 +20380,7 @@ source-map-support@0.5.19:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map-support@^0.5.16, source-map-support@~0.5.20:
source-map-support@^0.5.16, source-map-support@^0.5.21, source-map-support@~0.5.20:
version "0.5.21"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==