Compare commits

..
Author SHA1 Message Date
Madalina Raicu ae1b88580a Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into fix/lp-supplied-quantum 2023-10-23 09:54:20 +01:00
Radosław Szpiech ae02b153f8 chore(trading): fix cancel all button and move trading orders tests (#5100) 2023-10-23 08:34:50 +02:00
Madalina Raicu 9ba1dc55b0 Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into fix/lp-supplied-quantum 2023-10-20 14:49:34 +01:00
Maciek fee5f862a7 chore(trading): remove account history tab (#5098) 2023-10-20 15:08:19 +02:00
Edd c25362b084 feat(governance): add restricted page (#5097) 2023-10-20 11:27:14 +00:00
Madalina Raicu 46b25cbafa chore: use quantum for liquidity supplied 2023-10-20 11:33:31 +01:00
Edd 7c105d2c6b feat(explorer): add restricted route (#5094) 2023-10-19 18:28:33 +00:00
Maciek 2dfcb02d1e chore(orders): 4970 add no rows and error info (#5076) 2023-10-19 14:45:55 +01:00
Sam Keen a98ab775c3 feat(governance): transfers destination field can fallback to type (#5092) 2023-10-19 11:16:00 +01:00
Maciek 097bf7c08c chore(trading): add button for copying asset id (#5075) 2023-10-18 17:02:34 +02:00
Bartłomiej Głowniaandbwallacee 236e35a92b feat(trading): show successor banner if market is settled or successor market exists (#5056)
Co-authored-by: bwallacee <ben@vega.xyz>
2023-10-18 14:48:52 +02:00
Maciek abf84b9d45 chore(ui-toolkit): 4970 async renderer - improve error handling (#5063) 2023-10-18 11:11:05 +02:00
m.ray 6d2a2ea0a0 chore(trading): update snap version to 0.3.1 (#5059) 2023-10-18 09:56:50 +01:00
Matthew Russell 6ac79e9f6d chore(trading,explorer,governance): update codeowners so dev team only is required for reviews 2023-10-17 20:14:48 -07:00
Matthew Russell 835ee64243 fix(trading): hydration error due to loader (#5043) 2023-10-17 20:05:53 -07:00
Matthew Russell e7c3b5054c fix(trading): market selector dropdown not updating (#5027) 2023-10-17 09:56:48 -07:00
Maciek 9dc9588a14 chore(ledger): 4895 refactor ledger csv extract (#5053) 2023-10-17 17:15:38 +02:00
Ben a0844d41bf chore(trading): move transfer test to jest (#5058) 2023-10-17 10:56:21 +01:00
m.ray 3d1aa74128 fix(trading): liquidation price tooltip link (#5050) 2023-10-17 10:44:49 +01:00
Ben 2376bfe58f chore(trading): deposit test to jest (#5036) 2023-10-16 11:44:37 -07:00
Matthew Russell 72ccf96523 fix(trading): remove additional page for closed markets (#5042) 2023-10-16 11:18:26 -07:00
Matthew Russell f416fa4a25 chore(trading,explorer,governance): adjust codeowners to require reviews from both teams (#5054) 2023-10-16 16:11:19 +01:00
Matthew Russell a24e3646cc fix(trading): glitchy waterfall loading (#5039) 2023-10-16 07:49:23 -07:00
Art 55b092b216 fix(trading): update referral program proposal changes (#5030) 2023-10-16 16:45:00 +02:00
Matthew Russell ef4cf6a87e chore(assets): add testnet deny list (#5044) 2023-10-16 07:41:31 -07:00
Sam Keen c9860a3412 fix(governance): governance initiated transfers fixes (#5025) 2023-10-16 12:02:06 +01:00
m.ray 127bd3faf3 chore(trading): code owners fix for two reviewers (#5046) 2023-10-16 12:00:20 +01:00
Bartłomiej Głownia 1791b3ca6a feat(trading): adjust layout for funding payments (#5034) 2023-10-16 11:19:08 +02:00
Matthew Russell 6044b86ff4 chore(trading): add background to sidebar and minor tweaks (#5037) 2023-10-13 11:30:04 -07:00
Matthew Russell 341d78e761 chore(trading): fix hydration errors and improve loading experience (#5038) 2023-10-13 16:02:33 +02:00
122 changed files with 1994 additions and 2384 deletions
-1
View File
@@ -1,3 +1,2 @@
* @vegaprotocol/frontend * @vegaprotocol/frontend
* @vegaprotocol/frontend-qa
*.graphql @vegaprotocol/core *.graphql @vegaprotocol/core
+2 -2
View File
@@ -1,6 +1,6 @@
import { import {
AppFailure,
NetworkLoader, NetworkLoader,
NodeFailure,
NodeGuard, NodeGuard,
NodeSwitcherDialog, NodeSwitcherDialog,
useEnvironment, useEnvironment,
@@ -31,7 +31,7 @@ function App() {
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}> <NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<NodeGuard <NodeGuard
skeleton={<div>{t('Loading')}</div>} skeleton={<div>{t('Loading')}</div>}
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />} failure={<NodeFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
> >
<Suspense fallback={splashLoading}> <Suspense fallback={splashLoading}>
<RouterProvider router={router} fallbackElement={splashLoading} /> <RouterProvider router={router} fallbackElement={splashLoading} />
+1 -1
View File
@@ -133,7 +133,7 @@ export const ErrorBoundary = () => {
); );
}; };
const GHOST = ( export const GHOST = (
<svg <svg
width="56" width="56"
height="85" height="85"
@@ -0,0 +1,108 @@
import { BackgroundVideo } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
const RestrictedPage = () => {
const errorTitle = '451 Unavailable';
const errorMessage =
'Due to uncertainty about the legal and regulatory status of the content hosted on this site, it is not available to visitors in your jurisdiction.';
return (
<div>
<BackgroundVideo className="brightness-50" />
<div
className={classNames(
'max-w-[620px] p-2 mt-[10vh]',
'mx-auto my-0',
'antialiased text-white',
'overflow-hidden relative',
'flex flex-col gap-2'
)}
>
<div className="flex gap-4">
<div>{GHOST}</div>
<h1 className="text-[2.7rem] font-alpha calt break-words uppercase">
{errorTitle}
</h1>
</div>
<div className="text-sm mt-10 overflow-auto font-mono">
{errorMessage}
</div>
</div>
</div>
);
};
const GHOST = (
<svg
width="56"
height="85"
viewBox="0 0 150 234"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.43 93.6821L0 104.097V172.435H5.13793V106.2L14.0779 97.3247L10.43 93.6821Z"
fill="black"
/>
<path d="M105.328 18.521H7.70703V172.435H105.328V18.521Z" fill="white" />
<path d="M38.5364 64.6953H33.3984V69.8258H38.5364V64.6953Z" fill="black" />
<path d="M43.6731 69.8257H38.5352V74.9561H43.6731V69.8257Z" fill="black" />
<path d="M48.8098 74.9561H43.6719V80.0865H48.8098V74.9561Z" fill="black" />
<path d="M38.5364 74.9561H33.3984V80.0865H38.5364V74.9561Z" fill="black" />
<path d="M48.8098 64.6953H43.6719V69.8258H48.8098V64.6953Z" fill="black" />
<path d="M69.3606 64.6953H64.2227V69.8258H69.3606V64.6953Z" fill="black" />
<path d="M74.5012 69.8257H69.3633V74.9561H74.5012V69.8257Z" fill="black" />
<path d="M79.6379 74.9561H74.5V80.0865H79.6379V74.9561Z" fill="black" />
<path d="M69.3606 74.9561H64.2227V80.0865H69.3606V74.9561Z" fill="black" />
<path d="M79.6379 64.6953H74.5V69.8258H79.6379V64.6953Z" fill="black" />
<path d="M79.6398 90.3477H33.3984V95.4781H79.6398V90.3477Z" fill="black" />
<path d="M48.8098 172.435H43.6719V234H48.8098V172.435Z" fill="black" />
<path d="M69.3606 172.435H64.2227V234H69.3606V172.435Z" fill="black" />
<path
d="M129.87 0.00408026L92.0273 14.3682L111.195 64.7203L149.038 50.3562L129.87 0.00408026Z"
fill="#FF0081"
/>
<path
d="M108.565 21.7836L103.762 23.6064L105.587 28.4024L110.39 26.5795L108.565 21.7836Z"
fill="white"
/>
<path
d="M115.189 24.7596L110.387 26.583L112.213 31.3785L117.015 29.5551L115.189 24.7596Z"
fill="white"
/>
<path
d="M121.826 27.7348L117.023 29.5576L118.849 34.3536L123.652 32.5307L121.826 27.7348Z"
fill="white"
/>
<path
d="M128.447 30.7059L123.645 32.5293L125.471 37.3247L130.273 35.5014L128.447 30.7059Z"
fill="white"
/>
<path
d="M135.084 33.6854L130.281 35.5083L132.107 40.3043L136.91 38.4814L135.084 33.6854Z"
fill="white"
/>
<path
d="M124.799 21.1156L119.996 22.939L121.822 27.7344L126.625 25.911L124.799 21.1156Z"
fill="white"
/>
<path
d="M127.775 14.492L122.973 16.3154L124.799 21.1109L129.601 19.2875L127.775 14.492Z"
fill="white"
/>
<path
d="M118.838 34.3499L114.035 36.1733L115.861 40.9688L120.664 39.1454L118.838 34.3499Z"
fill="white"
/>
<path
d="M115.857 40.9691L111.055 42.7925L112.881 47.5879L117.683 45.7645L115.857 40.9691Z"
fill="white"
/>
<path
d="M129.938 52.3822L139.906 91.5276L100.703 101.429L101.988 106.406L146.174 95.2215L134.922 51.0996L129.938 52.3822Z"
fill="black"
/>
</svg>
);
export default RestrictedPage;
@@ -1,5 +1,6 @@
export const Routes = { export const Routes = {
HOME: '/', HOME: '/',
RESTRICTED: '/restricted',
TX: 'txs', TX: 'txs',
BLOCKS: 'blocks', BLOCKS: 'blocks',
PARTIES: 'parties', PARTIES: 'parties',
@@ -30,6 +30,7 @@ import { remove0x } from '@vegaprotocol/utils';
import { PartyAccountsByAsset } from './parties/id/accounts'; import { PartyAccountsByAsset } from './parties/id/accounts';
import { Disclaimer } from './pages/disclaimer'; import { Disclaimer } from './pages/disclaimer';
import { FLAGS } from '@vegaprotocol/environment'; import { FLAGS } from '@vegaprotocol/environment';
import RestrictedPage from './restricted';
export type Navigable = { export type Navigable = {
path: string; path: string;
@@ -356,6 +357,14 @@ export const routerConfig: Route[] = [
...validators, ...validators,
], ],
}, },
{
path: Routes.RESTRICTED,
element: <RestrictedPage />,
handle: {
name: t('Restricted'),
text: t('Restricted'),
},
},
]; ];
export const router = createBrowserRouter(routerConfig); export const router = createBrowserRouter(routerConfig);
+15 -3
View File
@@ -38,10 +38,10 @@ import {
NetworkLoader, NetworkLoader,
useInitializeEnv, useInitializeEnv,
NodeGuard, NodeGuard,
AppFailure,
NodeSwitcherDialog, NodeSwitcherDialog,
useNodeSwitcherStore, useNodeSwitcherStore,
DocsLinks, DocsLinks,
NodeFailure,
} from '@vegaprotocol/environment'; } from '@vegaprotocol/environment';
import { ENV } from './config'; import { ENV } from './config';
import type { InMemoryCacheConfig } from '@apollo/client'; import type { InMemoryCacheConfig } from '@apollo/client';
@@ -106,6 +106,7 @@ const Web3Container = ({
useEthWithdrawApprovalsManager(); useEthWithdrawApprovalsManager();
return null; return null;
}; };
const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [ const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [
store.connectors, store.connectors,
store.initialize, store.initialize,
@@ -184,7 +185,7 @@ const Web3Container = ({
<TemplateSidebar sidebar={sideBar}> <TemplateSidebar sidebar={sideBar}>
<AppRouter /> <AppRouter />
</TemplateSidebar> </TemplateSidebar>
<footer className="p-4 border-t border-neutral-700 break-all"> <footer className="p-4 break-all border-t border-neutral-700">
<NetworkInfo /> <NetworkInfo />
</footer> </footer>
</AppLayout> </AppLayout>
@@ -239,6 +240,9 @@ const AppContainer = () => {
store.setDialogOpen, store.setDialogOpen,
]); ]);
// Hacky skip all the loading & web3 init for geo restricted users
const isRestricted = document?.location?.pathname?.includes('/restricted');
useEffect(() => { useEffect(() => {
if (ENV.dsn && telemetryOn === 'true') { if (ENV.dsn && telemetryOn === 'true') {
Sentry.init({ Sentry.init({
@@ -304,6 +308,14 @@ const AppContainer = () => {
} }
}, [GIT_COMMIT_HASH, GIT_BRANCH, VEGA_ENV, telemetryOn]); }, [GIT_COMMIT_HASH, GIT_BRANCH, VEGA_ENV, telemetryOn]);
if (isRestricted) {
return (
<Router>
<AppRouter />
</Router>
);
}
return ( return (
<Router> <Router>
<ScrollToTop /> <ScrollToTop />
@@ -312,7 +324,7 @@ const AppContainer = () => {
<NodeGuard <NodeGuard
skeleton={<div>{t('Loading')}</div>} skeleton={<div>{t('Loading')}</div>}
failure={ failure={
<AppFailure title={t('NodeUnsuitable', { url: VEGA_URL })} /> <NodeFailure title={t('NodeUnsuitable', { url: VEGA_URL })} />
} }
> >
<AsyncRenderer<EthereumConfig | null> <AsyncRenderer<EthereumConfig | null>
+32 -1
View File
@@ -9,6 +9,7 @@
"pageTitleRedemptionTranche": "Redeem from Tranche", "pageTitleRedemptionTranche": "Redeem from Tranche",
"pageTitleTranches": "Vesting tranches", "pageTitleTranches": "Vesting tranches",
"pageTitle404": "Page not found", "pageTitle404": "Page not found",
"pageTitle451": "451 unavailable",
"pageTitleNotPermitted": "Can not proceed!", "pageTitleNotPermitted": "Can not proceed!",
"pageTitleDisassociate": "Disassociate $VEGA tokens from a Vega key", "pageTitleDisassociate": "Disassociate $VEGA tokens from a Vega key",
"pageTitleProposals": "Proposals", "pageTitleProposals": "Proposals",
@@ -718,6 +719,8 @@
"UpdateAssetProposal": "Update asset proposal", "UpdateAssetProposal": "Update asset proposal",
"NewFreeformProposal": "New freeform proposal", "NewFreeformProposal": "New freeform proposal",
"NewRawProposal": "New proposal", "NewRawProposal": "New proposal",
"NewTransferProposal": "New transfer proposal",
"CancelTransferProposal": "Cancel transfer proposal",
"MARKET_STATE_UPDATE_TYPE_RESUME": "Resume market", "MARKET_STATE_UPDATE_TYPE_RESUME": "Resume market",
"MARKET_STATE_UPDATE_TYPE_SUSPEND": "Suspend market", "MARKET_STATE_UPDATE_TYPE_SUSPEND": "Suspend market",
"MARKET_STATE_UPDATE_TYPE_TERMINATE": "Terminate market", "MARKET_STATE_UPDATE_TYPE_TERMINATE": "Terminate market",
@@ -738,6 +741,8 @@
"UpdateVolumeDiscountProgram": "Update volume discount program", "UpdateVolumeDiscountProgram": "Update volume discount program",
"NewAsset": "New asset", "NewAsset": "New asset",
"UpdateAsset": "Update asset", "UpdateAsset": "Update asset",
"NewTransfer": "New transfer",
"CancelTransfer": "Cancel transfer",
"AssetID": "Asset ID", "AssetID": "Asset ID",
"Freeform": "Freeform", "Freeform": "Freeform",
"RawProposal": "Let me choose (raw proposal)", "RawProposal": "Let me choose (raw proposal)",
@@ -914,5 +919,31 @@
"EndOfProgramTimestamp": "End of program", "EndOfProgramTimestamp": "End of program",
"EndOfProgramTimestampDescription": "Time after which when the current epoch ends, the programs will end and benefits will be disabled.", "EndOfProgramTimestampDescription": "Time after which when the current epoch ends, the programs will end and benefits will be disabled.",
"BenefitTierVolumeDiscountFactor": "Volume discount factor", "BenefitTierVolumeDiscountFactor": "Volume discount factor",
"BenefitTierVolumeDiscountFactorDescription": "Discount given to those in this benefit tier" "BenefitTierVolumeDiscountFactorDescription": "Discount given to those in this benefit tier",
"ACCOUNT_TYPE_INSURANCE": "Insurance account",
"ACCOUNT_TYPE_GLOBAL_INSURANCE": "Global insurance account",
"ACCOUNT_TYPE_SETTLEMENT": "Settlement account",
"ACCOUNT_TYPE_MARGIN": "Margin account ",
"ACCOUNT_TYPE_GENERAL": "General account",
"ACCOUNT_TYPE_FEES_INFRASTRUCTURE": "Infrastructure fees account",
"ACCOUNT_TYPE_FEES_LIQUIDITY": "Liquidity fees account",
"ACCOUNT_TYPE_FEES_MAKER": "Maker fees account",
"ACCOUNT_TYPE_BOND": "Bond account",
"ACCOUNT_TYPE_EXTERNAL": "External account",
"ACCOUNT_TYPE_GLOBAL_REWARD": "Global reward account",
"ACCOUNT_TYPE_PENDING_TRANSFERS": "Pending transfers account",
"ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES": "Maker paid fees reward account",
"ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES": "Maker received fees reward account",
"ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES": "Liquidity provider received fees reward account",
"ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS": "Market proposers reward account",
"ACCOUNT_TYPE_HOLDING": "Holding account",
"ACCOUNT_TYPE_LP_LIQUIDITY_FEES": "Liquidity provider fees account",
"ACCOUNT_TYPE_NETWORK_TREASURY": "Network treasury account",
"ACCOUNT_TYPE_VESTING_REWARDS": "Vesting rewards account",
"ACCOUNT_TYPE_VESTED_REWARDS": "Vested rewards account",
"ACCOUNT_TYPE_REWARD_AVERAGE_POSITION": "Average position reward account",
"ACCOUNT_TYPE_REWARD_RELATIVE_RETURN": "Relative return reward account",
"ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY": "Return volatility reward account",
"ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING": "Validator ranking reward account",
"ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD": "Pending fee referral reward account"
} }
@@ -12,18 +12,19 @@ import {
generateProposal, generateProposal,
generateYesVotes, generateYesVotes,
} from '../../test-helpers/generate-proposals'; } from '../../test-helpers/generate-proposals';
import { ProposalHeader } from './proposal-header'; import { ProposalHeader, NewTransferSummary } from './proposal-header';
import { import {
lastWeek, lastWeek,
nextWeek, nextWeek,
mockWalletContext, mockWalletContext,
createUserVoteQueryMock, createUserVoteQueryMock,
} from '../../test-helpers/mocks'; } from '../../test-helpers/mocks';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import { FLAGS } from '@vegaprotocol/environment'; import { FLAGS } from '@vegaprotocol/environment';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { VoteState } from '../vote-details/use-user-vote'; import { VoteState } from '../vote-details/use-user-vote';
import { useNewTransferProposalDetails } from '@vegaprotocol/proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
jest.mock('@vegaprotocol/proposals', () => ({ jest.mock('@vegaprotocol/proposals', () => ({
...jest.requireActual('@vegaprotocol/proposals'), ...jest.requireActual('@vegaprotocol/proposals'),
@@ -31,6 +32,7 @@ jest.mock('@vegaprotocol/proposals', () => ({
code: 'PARENT_CODE', code: 'PARENT_CODE',
parentMarketId: 'PARENT_ID', parentMarketId: 'PARENT_ID',
}), }),
useNewTransferProposalDetails: jest.fn(),
})); }));
const renderComponent = ( const renderComponent = (
@@ -418,3 +420,78 @@ describe('Proposal header', () => {
expect(await screen.findByTestId('user-voted-yes')).toBeInTheDocument(); expect(await screen.findByTestId('user-voted-yes')).toBeInTheDocument();
}); });
}); });
jest.mock('@vegaprotocol/proposals');
describe('<NewTransferSummary />', () => {
it('renders null if no details are provided', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue(null);
const { container } = render(<NewTransferSummary proposalId="1" />);
expect(container.firstChild).toBeNull();
});
it('handles OneOffGovernanceTransfer', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
kind: { __typename: 'OneOffGovernanceTransfer', deliverOn: null },
source: '0x123',
sourceType: 'wallet',
destination: '0x456',
destinationType: 'contract',
});
const { getByText } = render(<NewTransferSummary proposalId="1" />);
const textMatch = (content: string) => content.includes('One off transfer');
expect(getByText(textMatch)).toBeInTheDocument();
});
it('handles RecurringGovernanceTransfer', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
kind: {
__typename: 'RecurringGovernanceTransfer',
startEpoch: 1,
endEpoch: 5,
},
source: '0x123',
sourceType: 'wallet',
destination: '0x456',
destinationType: 'contract',
});
const { getByText } = render(<NewTransferSummary proposalId="1" />);
const textMatch = (content: string) =>
content.includes('Recurring transfer');
expect(getByText(textMatch)).toBeInTheDocument();
});
it('should fallback to translated sourceType when source is not set', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
kind: {
__typename: 'RecurringGovernanceTransfer',
startEpoch: 1,
endEpoch: 5,
},
source: undefined,
sourceType: 'ACCOUNT_TYPE_GENERAL',
destination: '0x456',
destinationType: 'ACCOUNT_TYPE_GENERAL',
});
render(<NewTransferSummary proposalId="1" />);
expect(screen.getByText('General account')).toBeInTheDocument();
});
it('should fallback to translated destinationType when destination is not set', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
kind: {
__typename: 'RecurringGovernanceTransfer',
startEpoch: 1,
endEpoch: 5,
},
source: '0x123',
sourceType: 'ACCOUNT_TYPE_GENERAL',
destination: undefined,
destinationType: 'ACCOUNT_TYPE_GLOBAL_INSURANCE',
});
render(<NewTransferSummary proposalId="1" />);
expect(screen.getByText('Global insurance account')).toBeInTheDocument();
});
});
@@ -237,7 +237,11 @@ export const ProposalHeader = ({
); );
}; };
const SuccessorCode = ({ proposalId }: { proposalId?: string | null }) => { export const SuccessorCode = ({
proposalId,
}: {
proposalId?: string | null;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const successor = useSuccessorMarketProposalDetails(proposalId); const successor = useSuccessorMarketProposalDetails(proposalId);
@@ -254,7 +258,11 @@ const SuccessorCode = ({ proposalId }: { proposalId?: string | null }) => {
) : null; ) : null;
}; };
const NewTransferSummary = ({ proposalId }: { proposalId?: string | null }) => { export const NewTransferSummary = ({
proposalId,
}: {
proposalId?: string | null;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const details = useNewTransferProposalDetails(proposalId); const details = useNewTransferProposalDetails(proposalId);
@@ -263,13 +271,23 @@ const NewTransferSummary = ({ proposalId }: { proposalId?: string | null }) => {
return ( return (
<span> <span>
{GovernanceTransferKindMapping[details.kind.__typename]}{' '} {GovernanceTransferKindMapping[details.kind.__typename]}{' '}
{t('transfer from')} <Lozenge>{truncateMiddle(details.source)}</Lozenge>{' '} {t('transfer from')}{' '}
{t('to')} <Lozenge>{truncateMiddle(details.destination)}</Lozenge> <Lozenge>
{details.source
? truncateMiddle(details.source)
: t(details.sourceType)}
</Lozenge>{' '}
{t('to')}{' '}
<Lozenge>
{details.destination
? truncateMiddle(details.destination)
: t(details.destinationType)}
</Lozenge>
</span> </span>
); );
}; };
const CancelTransferSummary = ({ export const CancelTransferSummary = ({
proposalId, proposalId,
}: { }: {
proposalId?: string | null; proposalId?: string | null;
@@ -63,44 +63,42 @@ const mockReferralProposal = generateProposal({
terms: { terms: {
change: { change: {
__typename: 'UpdateReferralProgram', __typename: 'UpdateReferralProgram',
changes: { benefitTiers: [
benefitTiers: [ {
{ minimumEpochs: 6,
minimumEpochs: 6, minimumRunningNotionalTakerVolume: '10000',
minimumRunningNotionalTakerVolume: '10000', referralDiscountFactor: '0.001',
referralDiscountFactor: '0.001', referralRewardFactor: '0.001',
referralRewardFactor: '0.001', },
}, {
{ minimumEpochs: 24,
minimumEpochs: 24, minimumRunningNotionalTakerVolume: '500000',
minimumRunningNotionalTakerVolume: '500000', referralDiscountFactor: '0.005',
referralDiscountFactor: '0.005', referralRewardFactor: '0.005',
referralRewardFactor: '0.005', },
}, {
{ minimumEpochs: 48,
minimumEpochs: 48, minimumRunningNotionalTakerVolume: '1000000',
minimumRunningNotionalTakerVolume: '1000000', referralDiscountFactor: '0.01',
referralDiscountFactor: '0.01', referralRewardFactor: '0.01',
referralRewardFactor: '0.01', },
}, ],
], endOfProgram: '2026-10-03T10:34:34Z',
endOfProgramTimestamp: '2026-10-03T10:34:34Z', windowLength: 3,
windowLength: 3, stakingTiers: [
stakingTiers: [ {
{ minimumStakedTokens: '1',
minimumStakedTokens: '1', referralRewardMultiplier: '1',
referralRewardMultiplier: '1', },
}, {
{ minimumStakedTokens: '2',
minimumStakedTokens: '2', referralRewardMultiplier: '2',
referralRewardMultiplier: '2', },
}, {
{ minimumStakedTokens: '5',
minimumStakedTokens: '5', referralRewardMultiplier: '3',
referralRewardMultiplier: '3', },
}, ],
],
},
}, },
}, },
}); });
@@ -132,7 +130,6 @@ describe('<ProposalReferralProgramDetails />', () => {
terms: { terms: {
change: { change: {
__typename: 'UpdateReferralProgram', __typename: 'UpdateReferralProgram',
changes: {},
}, },
}, },
}); });
@@ -54,11 +54,10 @@ export const ProposalReferralProgramDetails = ({
return null; return null;
} }
const benefitTiers = proposal?.terms?.change?.changes?.benefitTiers; const benefitTiers = proposal?.terms?.change?.benefitTiers;
const stakingTiers = proposal?.terms?.change?.changes?.stakingTiers; const stakingTiers = proposal?.terms?.change?.stakingTiers;
const windowLength = proposal?.terms?.change?.changes?.windowLength; const windowLength = proposal?.terms?.change?.windowLength;
const endOfProgramTimestamp = const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgram;
proposal?.terms?.change?.changes?.endOfProgramTimestamp;
if ( if (
!benefitTiers && !benefitTiers &&
@@ -30,6 +30,8 @@ export const useProposalNetworkParams = ({
NetworkParams.governance_proposal_freeform_requiredParticipation, NetworkParams.governance_proposal_freeform_requiredParticipation,
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority, NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority,
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredParticipation, NetworkParams.governance_proposal_VolumeDiscountProgram_requiredParticipation,
NetworkParams.governance_proposal_transfer_requiredParticipation,
NetworkParams.governance_proposal_transfer_requiredMajority,
]); ]);
const fallback = { const fallback = {
@@ -111,6 +113,14 @@ export const useProposalNetworkParams = ({
params.governance_proposal_VolumeDiscountProgram_requiredParticipation params.governance_proposal_VolumeDiscountProgram_requiredParticipation
), ),
}; };
case 'NewTransfer':
case 'CancelTransfer':
return {
requiredMajority: params.governance_proposal_transfer_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_transfer_requiredParticipation
),
};
default: default:
return fallback; return fallback;
} }
@@ -47,19 +47,17 @@ fragment UpdateReferralProgram on Proposal {
terms { terms {
change { change {
... on UpdateReferralProgram { ... on UpdateReferralProgram {
changes { benefitTiers {
benefitTiers { minimumEpochs
minimumEpochs minimumRunningNotionalTakerVolume
minimumRunningNotionalTakerVolume referralDiscountFactor
referralDiscountFactor referralRewardFactor
referralRewardFactor }
} endOfProgram: endOfProgramTimestamp
endOfProgramTimestamp windowLength
windowLength stakingTiers {
stakingTiers { minimumStakedTokens
minimumStakedTokens referralRewardMultiplier
referralRewardMultiplier
}
} }
} }
} }
File diff suppressed because one or more lines are too long
@@ -47,19 +47,17 @@ fragment UpdateReferralPrograms on Proposal {
terms { terms {
change { change {
... on UpdateReferralProgram { ... on UpdateReferralProgram {
changes { benefitTiers {
benefitTiers { minimumEpochs
minimumEpochs minimumRunningNotionalTakerVolume
minimumRunningNotionalTakerVolume referralDiscountFactor
referralDiscountFactor referralRewardFactor
referralRewardFactor }
} endOfProgram: endOfProgramTimestamp
endOfProgramTimestamp windowLength
windowLength stakingTiers {
stakingTiers { minimumStakedTokens
minimumStakedTokens referralRewardMultiplier
referralRewardMultiplier
}
} }
} }
} }
@@ -7,7 +7,7 @@ export type NewMarketProductFieldsFragment = { __typename?: 'Proposal', terms: {
export type UpdateMarketStatesFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } }; export type UpdateMarketStatesFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
export type UpdateReferralProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram', changes: { __typename?: 'ReferralProgram', endOfProgramTimestamp: string, windowLength: number, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } }; export type UpdateReferralProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: string, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
export type UpdateVolumeDiscountProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } } }; export type UpdateVolumeDiscountProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } } };
@@ -21,7 +21,7 @@ export type ProposalsQueryVariables = Types.Exact<{
}>; }>;
export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null, product?: { __typename: 'FutureProduct' } | { __typename: 'PerpetualProduct' } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram', changes: { __typename?: 'ReferralProgram', endOfProgramTimestamp: string, windowLength: number, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null }; export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null, product?: { __typename: 'FutureProduct' } | { __typename: 'PerpetualProduct' } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: string, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null };
export const NewMarketProductFieldsFragmentDoc = gql` export const NewMarketProductFieldsFragmentDoc = gql`
fragment NewMarketProductFields on Proposal { fragment NewMarketProductFields on Proposal {
@@ -75,19 +75,17 @@ export const UpdateReferralProgramsFragmentDoc = gql`
terms { terms {
change { change {
... on UpdateReferralProgram { ... on UpdateReferralProgram {
changes { benefitTiers {
benefitTiers { minimumEpochs
minimumEpochs minimumRunningNotionalTakerVolume
minimumRunningNotionalTakerVolume referralDiscountFactor
referralDiscountFactor referralRewardFactor
referralRewardFactor }
} endOfProgram: endOfProgramTimestamp
endOfProgramTimestamp windowLength
windowLength stakingTiers {
stakingTiers { minimumStakedTokens
minimumStakedTokens referralRewardMultiplier
referralRewardMultiplier
}
} }
} }
} }
@@ -0,0 +1,112 @@
import { useTranslation } from 'react-i18next';
import { useDocumentTitle } from '../../hooks/use-document-title';
import type { RouteChildProps } from '..';
import { BackgroundVideo } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
const Restricted = ({ name }: RouteChildProps) => {
useDocumentTitle(name);
const { t } = useTranslation();
const errorMessage =
'Due to uncertainty about the legal and regulatory status of the content hosted on this site, it is not available to visitors in your jurisdiction.';
return (
<div>
<BackgroundVideo className="brightness-50" />
<div
className={classNames(
'max-w-[620px] p-2 mt-[10vh]',
'mx-auto my-0',
'antialiased text-white',
'overflow-hidden relative',
'flex flex-col gap-2'
)}
>
<div className="flex gap-4">
<div>{GHOST}</div>
<h1 className="leading-relaxed mb-0 text-[2.7rem] font-alpha calt break-words uppercase">
{t('pageTitle451')}
</h1>
</div>
<div className="text-sm mt-10 overflow-auto font-mono">
{errorMessage}
</div>
</div>
</div>
);
};
const GHOST = (
<svg
width="56"
height="85"
viewBox="0 0 150 234"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.43 93.6821L0 104.097V172.435H5.13793V106.2L14.0779 97.3247L10.43 93.6821Z"
fill="black"
/>
<path d="M105.328 18.521H7.70703V172.435H105.328V18.521Z" fill="white" />
<path d="M38.5364 64.6953H33.3984V69.8258H38.5364V64.6953Z" fill="black" />
<path d="M43.6731 69.8257H38.5352V74.9561H43.6731V69.8257Z" fill="black" />
<path d="M48.8098 74.9561H43.6719V80.0865H48.8098V74.9561Z" fill="black" />
<path d="M38.5364 74.9561H33.3984V80.0865H38.5364V74.9561Z" fill="black" />
<path d="M48.8098 64.6953H43.6719V69.8258H48.8098V64.6953Z" fill="black" />
<path d="M69.3606 64.6953H64.2227V69.8258H69.3606V64.6953Z" fill="black" />
<path d="M74.5012 69.8257H69.3633V74.9561H74.5012V69.8257Z" fill="black" />
<path d="M79.6379 74.9561H74.5V80.0865H79.6379V74.9561Z" fill="black" />
<path d="M69.3606 74.9561H64.2227V80.0865H69.3606V74.9561Z" fill="black" />
<path d="M79.6379 64.6953H74.5V69.8258H79.6379V64.6953Z" fill="black" />
<path d="M79.6398 90.3477H33.3984V95.4781H79.6398V90.3477Z" fill="black" />
<path d="M48.8098 172.435H43.6719V234H48.8098V172.435Z" fill="black" />
<path d="M69.3606 172.435H64.2227V234H69.3606V172.435Z" fill="black" />
<path
d="M129.87 0.00408026L92.0273 14.3682L111.195 64.7203L149.038 50.3562L129.87 0.00408026Z"
fill="#FF0081"
/>
<path
d="M108.565 21.7836L103.762 23.6064L105.587 28.4024L110.39 26.5795L108.565 21.7836Z"
fill="white"
/>
<path
d="M115.189 24.7596L110.387 26.583L112.213 31.3785L117.015 29.5551L115.189 24.7596Z"
fill="white"
/>
<path
d="M121.826 27.7348L117.023 29.5576L118.849 34.3536L123.652 32.5307L121.826 27.7348Z"
fill="white"
/>
<path
d="M128.447 30.7059L123.645 32.5293L125.471 37.3247L130.273 35.5014L128.447 30.7059Z"
fill="white"
/>
<path
d="M135.084 33.6854L130.281 35.5083L132.107 40.3043L136.91 38.4814L135.084 33.6854Z"
fill="white"
/>
<path
d="M124.799 21.1156L119.996 22.939L121.822 27.7344L126.625 25.911L124.799 21.1156Z"
fill="white"
/>
<path
d="M127.775 14.492L122.973 16.3154L124.799 21.1109L129.601 19.2875L127.775 14.492Z"
fill="white"
/>
<path
d="M118.838 34.3499L114.035 36.1733L115.861 40.9688L120.664 39.1454L118.838 34.3499Z"
fill="white"
/>
<path
d="M115.857 40.9691L111.055 42.7925L112.881 47.5879L117.683 45.7645L115.857 40.9691Z"
fill="white"
/>
<path
d="M129.938 52.3822L139.906 91.5276L100.703 101.429L101.988 106.406L146.174 95.2215L134.922 51.0996L129.938 52.3822Z"
fill="black"
/>
</svg>
);
export default Restricted;
+7 -1
View File
@@ -4,6 +4,7 @@ import Home from './token';
import NotFound from './not-found'; import NotFound from './not-found';
import NotPermitted from './not-permitted'; import NotPermitted from './not-permitted';
import Routes from './routes'; import Routes from './routes';
import Restricted from './restricted';
const LazyTranches = React.lazy( const LazyTranches = React.lazy(
() => () =>
@@ -361,8 +362,13 @@ const routerConfig = [
element: <LazyDisclaimer name="Disclaimer" />, element: <LazyDisclaimer name="Disclaimer" />,
}, },
{ {
path: '*', path: Routes.RESTRICTED,
// Not lazy as loaded when a user first hits the site // Not lazy as loaded when a user first hits the site
element: <Restricted name="451 Unavailable" />,
},
{
path: '*',
// Also not lazy as loaded when a user first hits the site
element: <NotFound name="NotFound" />, element: <NotFound name="NotFound" />,
}, },
...redirects, ...redirects,
+5
View File
@@ -7,6 +7,7 @@ const Routes = {
PROPOSALS_REJECTED: '/proposals/rejected', PROPOSALS_REJECTED: '/proposals/rejected',
PROTOCOL_UPGRADES: '/protocol-upgrades', PROTOCOL_UPGRADES: '/protocol-upgrades',
NOT_PERMITTED: '/not-permitted', NOT_PERMITTED: '/not-permitted',
RESTRICTED: '/restricted',
NOT_FOUND: '/not-found', NOT_FOUND: '/not-found',
CONTRACTS: '/contracts', CONTRACTS: '/contracts',
TOKEN: '/token', TOKEN: '/token',
@@ -33,6 +34,10 @@ export const TOP_LEVEL_ROUTES = [
name: 'Rewards', name: 'Rewards',
path: Routes.REWARDS, path: Routes.REWARDS,
}, },
{
name: 'Restricted',
path: Routes.RESTRICTED,
},
]; ];
export const TOKEN_DROPDOWN_ROUTES = [ export const TOKEN_DROPDOWN_ROUTES = [
@@ -1,201 +0,0 @@
import { removeDecimal } from '@vegaprotocol/cypress';
import { ethers } from 'ethers';
import { connectEthereumWallet } from '../support/ethereum-wallet';
import { selectAsset } from '../support/helpers';
const assetSelectField = 'select[name="asset"]';
const toAddressField = 'input[name="to"]';
const amountField = 'input[name="amount"]';
const formFieldError = 'input-error-text';
const ASSET_EURO = 1;
describe('deposit form validation', { tags: '@smoke' }, () => {
function openDepositForm() {
cy.mockWeb3Provider();
cy.mockSubscription();
cy.mockTradingPage();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
connectEthereumWallet('MetaMask');
cy.wait('@Assets');
}
before(() => {
openDepositForm();
});
it('handles empty fields', () => {
cy.getByTestId('deposit-submit').click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
// once Ethereum wallet is connected and key selected the only field that will
// error is the asset select
cy.getByTestId(formFieldError).should('have.length', 1);
cy.get('[data-testid="input-error-text"][aria-describedby="asset"]').should(
'have.length',
1
);
});
it('unable to select assets not enabled', () => {
// Assets not enabled in mocks
cy.get(assetSelectField + ' option:contains(Asset 2)').should('not.exist');
cy.get(assetSelectField + ' option:contains(Asset 3)').should('not.exist');
cy.get(assetSelectField + ' option:contains(Asset 4)').should('not.exist');
});
it('invalid public key when entering address manually', () => {
cy.getByTestId('enter-pubkey-manually').click();
cy.get(toAddressField).clear().type('INVALID_DEPOSIT_TO_ADDRESS');
cy.get(`[data-testid="${formFieldError}"][aria-describedby="to"]`).should(
'have.text',
'Invalid Vega key'
);
});
it('invalid amount', () => {
mockWeb3DepositCalls({
allowance: '1000',
depositLifetimeLimit: '1000',
balance: '800',
deposited: '0',
dps: 5,
});
// Deposit amount smaller than minimum viable for selected asset
// Select an amount so that we have a known decimal places value to work with
selectAsset(ASSET_EURO);
cy.get(amountField)
.clear()
.type('0.00000000000000000000000000000000001')
.next(`[data-testid="${formFieldError}"]`)
.should('have.text', 'Value is below minimum');
});
it('insufficient funds', () => {
// 1001-DEPO-004
mockWeb3DepositCalls({
allowance: '1000',
depositLifetimeLimit: '1000',
balance: '800',
deposited: '0',
dps: 5,
});
cy.get(amountField)
.clear()
.type('850')
.next(`[data-testid="${formFieldError}"]`)
.should(
'have.text',
"You can't deposit more than you have in your Ethereum wallet, 800 tEURO"
);
});
});
describe('deposit actions', { tags: '@smoke' }, () => {
before(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/markets/market-1');
});
it.skip('Deposit to trade is visible', () => {
cy.getByTestId('Collateral').click();
cy.get('[row-id="asset-id"]').contains('tEURO').should('be.visible');
cy.contains('[data-testid="deposit"]', 'Deposit').should('be.visible');
cy.contains('[data-testid="deposit"]', 'Deposit').click();
cy.getByTestId('deposit-submit').should('be.visible');
});
});
function mockWeb3DepositCalls({
allowance,
depositLifetimeLimit,
balance,
deposited,
dps,
}: {
allowance: string;
depositLifetimeLimit: string;
balance: string;
deposited: string;
dps: number;
}) {
const assetContractAddress = '0x0158031158bb4df2ad02eaa31e8963e84ea978a4';
const collateralBridgeAddress = '0x7fe27d970bc8afc3b11cc8d9737bfb66b1efd799';
const toResult = (value: string, dps: number) => {
const rawValue = removeDecimal(value, dps);
return ethers.utils.hexZeroPad(
ethers.utils.hexlify(parseInt(rawValue)),
32
);
};
cy.intercept('POST', 'http://localhost:8545', (req) => {
// Mock chainId call
if (req.body.method === 'eth_chainId') {
req.alias = 'eth_chainId';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: '0xaa36a7', // 11155111 for sepolia chain id
});
}
// Mock deposited amount
if (req.body.method === 'eth_getStorageAt') {
req.alias = 'eth_getStorageAt';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(deposited, dps),
});
}
if (req.body.method === 'eth_call') {
// Mock approved amount for asset on collateral bridge
if (
req.body.params[0].to === assetContractAddress &&
req.body.params[0].data ===
'0xdd62ed3e000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f0000000000000000000000007fe27d970bc8afc3b11cc8d9737bfb66b1efd799'
) {
req.alias = 'eth_call_allowance';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(allowance, dps),
});
}
// Mock balance of asset in Ethereum wallet
else if (
req.body.params[0].to === assetContractAddress &&
req.body.params[0].data ===
'0x70a08231000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f'
) {
req.alias = 'eth_call_balanceOf';
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(balance, dps),
});
}
// Mock deposit lifetime limit
else if (
req.body.params[0].to === collateralBridgeAddress &&
req.body.params[0].data ===
'0x354a897a0000000000000000000000000158031158bb4df2ad02eaa31e8963e84ea978a4'
) {
req.alias = 'eth_call_get_deposit_maximum'; // deposit lifetime limit
req.reply({
id: req.body.id,
jsonrpc: req.body.jsonrpc,
result: toResult(depositLifetimeLimit, dps),
});
}
}
});
}
@@ -1,529 +0,0 @@
import * as Schema from '@vegaprotocol/types';
import type { OrderAmendment, OrderCancellation } from '@vegaprotocol/wallet';
import {
updateOrder,
getSubscriptionMocks,
} from '../support/order-update-subscription';
import {
testOrderCancellation,
testOrderAmendment,
} from '../support/order-validation';
const orderSymbol = 'instrument-code';
const orderSize = 'size';
const orderType = 'type';
const orderStatus = 'status';
const orderRemaining = 'remaining';
const orderPrice = 'price';
const orderTimeInForce = 'timeInForce';
const orderUpdatedAt = 'updatedAt';
const cancelOrderBtn = 'cancel';
const cancelAllOrdersBtn = 'cancelAll';
const editOrderBtn = 'edit';
describe('orders list', { tags: '@smoke', testIsolation: true }, () => {
beforeEach(() => {
const subscriptionMocks = getSubscriptionMocks();
cy.spy(subscriptionMocks, 'OrdersUpdate');
cy.mockTradingPage();
cy.mockSubscription(subscriptionMocks);
cy.setVegaWallet();
cy.visit('/#/markets/market-0');
cy.getByTestId('All').click();
cy.wait('@Markets');
});
it('renders orders', () => {
cy.getByTestId('tab-orders').should('be.visible');
cy.getByTestId(cancelAllOrdersBtn).should('be.visible');
cy.getByTestId(cancelOrderBtn).should('have.length.at.least', 1);
cy.getByTestId(editOrderBtn).should('have.length.at.least', 1);
cy.getByTestId('tab-orders').within(() => {
cy.get(`[role='rowgroup']`)
.first()
.within(() => {
cy.get(`[col-id='${orderSymbol}']`).each(($symbol) => {
cy.wrap($symbol).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderRemaining}']`).each(($remaining) => {
cy.wrap($remaining).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderSize}']`).each(($size) => {
cy.wrap($size).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderType}']`).each(($type) => {
cy.wrap($type).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderStatus}']`).each(($status) => {
cy.wrap($status).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderPrice}']`).each(($price) => {
cy.wrap($price).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderTimeInForce}']`).each(($timeInForce) => {
cy.wrap($timeInForce).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderUpdatedAt}']`).each(($dateTime) => {
cy.wrap($dateTime).invoke('text').should('not.be.empty');
});
});
});
});
it('partially filled orders should not show close/edit buttons', () => {
const partiallyFilledId =
'94aead3ca92dc932efcb503631b03a410e2a5d4606cae6083e2406dc38e52f78';
cy.getByTestId('tab-orders').should('be.visible');
cy.get('.ag-header-container').within(() => {
cy.get('[col-id="status"]').realHover();
cy.get('[col-id="status"] .ag-icon-menu').click();
});
cy.contains('Partially Filled').click();
cy.getByTestId('All').click();
cy.get(`[row-id="${partiallyFilledId}"]`)
.eq(0)
.within(() => {
cy.get(`[col-id='${orderStatus}']`).should(
'have.text',
'Partially Filled'
);
cy.get(`[col-id='${orderRemaining}']`).should('have.text', '7');
cy.get(`[col-id='${orderSize}']`).should('have.text', '-10');
cy.getByTestId(cancelOrderBtn).should('not.exist');
cy.getByTestId(editOrderBtn).should('not.exist');
});
});
it('orders are sorted by most recent order', () => {
// 7003-MORD-002
const expectedOrderList = [
'BTCUSD.MF21',
'SOLUSD',
'AAPL.MF21',
'BTCUSD.MF21',
'BTCUSD.MF21',
];
cy.get('.ag-header-container').within(() => {
cy.get('[col-id="status"]').realHover();
cy.get('[col-id="status"] .ag-icon-menu').click();
});
cy.contains('Reset').click();
cy.getByTestId('All').click();
cy.getByTestId('tab-orders')
.get(
`.ag-center-cols-container [col-id='${orderSymbol}'] [data-testid="market-code"]`
)
.should('have.length.at.least', expectedOrderList.length)
.then(($symbols) => {
const symbolNames: string[] = [];
cy.wrap($symbols)
.each(($symbol) => {
cy.wrap($symbol)
.invoke('text')
.then((text) => {
symbolNames.push(text);
});
})
.then(() => {
expect(symbolNames).to.include.ordered.members(expectedOrderList);
});
});
});
});
describe('subscribe orders', { tags: '@smoke' }, () => {
let orderId = '0';
beforeEach(() => {
const subscriptionMocks = getSubscriptionMocks();
cy.spy(subscriptionMocks, 'OrdersUpdate');
cy.mockTradingPage();
cy.mockSubscription(subscriptionMocks);
cy.setVegaWallet();
cy.visit('/#/markets/market-0');
cy.getByTestId('All').click();
cy.getByTestId('tab-orders').within(() => {
cy.get('[col-id="status"][role="columnheader"]')
.focus()
.find('.ag-header-cell-menu-button')
.click();
cy.get('.ag-filter-apply-panel-button').click();
});
orderId = (parseInt(orderId, 10) + 1).toString();
});
// 7002-SORD-053
// 7002-SORD-040
// 7003-MORD-001
it('must see an active order', () => {
// 7002-SORD-041
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Active');
});
it('must see an expired order', () => {
// 7002-SORD-042
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_EXPIRED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Expired');
});
it('must see a cancelled order', () => {
// 7002-SORD-043
// NOT COVERED: see the txn that cancelled it and a link to the block explorer, if cancelled by a user transaction.
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_CANCELLED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Cancelled');
});
it('must see a stopped order', () => {
// 7002-SORD-044
// NOT COVERED: see an explanation of why stopped
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_STOPPED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Stopped');
});
it('must see a partially filled order', () => {
// 7002-SORD-045
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_PARTIALLY_FILLED,
size: '5',
remaining: '1',
});
cy.getByTestId(`order-status-${orderId}`).should(
'have.text',
'Partially Filled'
);
cy.getByTestId(`order-status-${orderId}`)
.parentsUntil(`.ag-row`)
.siblings(`[col-id=${orderRemaining}]`)
.should('have.text', '4');
});
it('must see a filled order', () => {
// 7002-SORD-046
// 7003-MORD-020
// NOT COVERED: Must be able to see/link to all trades that were created from this order
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_FILLED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Filled');
cy.get(`[col-id="${orderSymbol}"]`).contains('[title="Future"]', 'Futr');
});
it('must see a rejected order', () => {
// 7002-SORD-047
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_REJECTED,
rejectionReason: Schema.OrderRejectionReason.ORDER_ERROR_INTERNAL_ERROR,
});
cy.getByTestId(`order-status-${orderId}`).should(
'have.text',
'Rejected: Internal error'
);
});
it('must see a parked order', () => {
// 7002-SORD-048
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_PARKED,
});
cy.getByTestId(`order-status-${orderId}`).should(
'have.text',
'Parked: Internal error'
);
});
it('must see the size of the order and direction/side -', () => {
// 7003-MORD-003
// 7003-MORD-004
updateOrder({
id: orderId,
size: '15',
side: Schema.Side.SIDE_SELL,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.get(`[row-id=${orderId}]`)
.find(`[col-id="${orderSize}"]`)
.should('have.text', '-15');
});
it('must see the size of the order and direction/side +', () => {
// 7003-MORD-003
// 7003-MORD-004
updateOrder({
id: orderId,
size: '5',
side: Schema.Side.SIDE_BUY,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.get(`[row-id=${orderId}]`)
.find(`[col-id="${orderSize}"]`)
.should('have.text', '+5');
});
it('for limit typy must see the Limit price that was set on the order', () => {
// 7003-MORD-005
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="price"]')
.should('have.text', '200.00');
});
it('must see a pegged order - ask', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_BUY,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_ASK,
offset: '250000',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Ask - 2.50 Peg limit');
});
it('must see a pegged order - bid', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_SELL,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_BID,
offset: '100',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Bid + 0.001 Peg limit');
});
it('must see a pegged order - mid', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_SELL,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_MID,
offset: '0.5',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Mid + 0.00001 Peg limit');
});
it('for market typy must not see a price for active or parked orders', () => {
// 7003-MORD-005
updateOrder({
id: orderId,
type: Schema.OrderType.TYPE_MARKET,
status: Schema.OrderStatus.STATUS_PARKED,
peggedOrder: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="price"]')
.should('have.text', '-');
});
it('must see the time in force applied to the order', () => {
// 7003-MORD-006
updateOrder({
id: orderId,
type: Schema.OrderType.TYPE_MARKET,
status: Schema.OrderStatus.STATUS_ACTIVE,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
});
cy.get(`[row-id=${orderId}]`)
.find(`[col-id='${orderTimeInForce}']`)
.should('have.text', 'GTC');
});
it('for Active order when is part of a liquidity or peg shape, must not see an option to amend the individual order ', () => {
// 7003-MORD-008
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: {},
liquidityProvisionId: '6536',
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="cancel"]`)
.should('not.exist');
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="edit"]`)
.should('not.exist');
});
it('for Active order when is part of a liquidity, must not see an option to amend the individual order ', () => {
// 7003-MORD-008
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: {},
liquidityProvisionId: '6536',
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="cancel"]`)
.should('not.exist');
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="edit"]`)
.should('not.exist');
});
it('for Active order when is part of a peg shape, must not see an option to amend the individual order ', () => {
// 7003-MORD-008
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: {},
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="cancel"]`)
.should('not.exist');
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="edit"]`)
.should('not.exist');
});
});
describe('amend and cancel order', { tags: '@smoke' }, () => {
beforeEach(() => {
const subscriptionMocks = getSubscriptionMocks();
cy.spy(subscriptionMocks, 'OrdersUpdate');
cy.mockTradingPage();
cy.mockSubscription(subscriptionMocks);
cy.setVegaWallet();
cy.visit('/#/markets/market-0');
cy.getByTestId('All').click();
cy.getByTestId('tab-orders').within(() => {
cy.get('[col-id="status"][role="columnheader"]')
.focus()
.find('.ag-header-cell-menu-button')
.click();
cy.get('.ag-filter-apply-panel-button').click();
});
cy.mockVegaWalletTransaction();
});
const orderId = '1234567890';
// this test is flakey
it('must be able to amend the price of an order', () => {
// 7003-MORD-007
// 7003-MORD-012
// 7003-MORD-014
// 7003-MORD-015
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[data-testid="icon-edit"]')
.then(($btn) => {
cy.wrap($btn).click();
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
cy.get('#limitPrice').focus().clear().type('100');
cy.getByTestId('edit-order').find('[type="submit"]').click();
const order: OrderAmendment = {
orderId: orderId,
marketId: 'market-0',
price: '10000000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
sizeDelta: 0,
};
testOrderAmendment(order);
});
});
it('must be able to cancel an individual order', () => {
// 7003-MORD-007
// 7003-MORD-009
// 7003-MORD-010
// 7003-MORD-011
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="icon-cross"]`)
.then(($btn) => {
cy.wrap($btn).click({ force: true });
const order: OrderCancellation = {
orderId: orderId,
marketId: 'market-0',
};
testOrderCancellation(order);
});
});
it('must be able to cancel all orders on all markets', () => {
// 7003-MORD-009
// 7003-MORD-010
// 7003-MORD-011
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[data-testid="cancelAll"]`)
.should('have.text', 'Cancel all')
.then(($btn) => {
cy.wrap($btn).click({ force: true });
const order: OrderCancellation = {};
testOrderCancellation(order);
});
});
it('must be warned (pre-submit) if the input price has too many digits after the decimal place for the market', () => {
// 7003-MORD-013
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[data-testid="icon-edit"]')
.then(($btn) => {
cy.wrap($btn).click({ force: true });
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
cy.get('#limitPrice').focus().clear().type('0.111111');
cy.getByTestId('edit-order').find('[type="submit"]').click();
cy.getByTestId('input-error-text').should(
'have.text',
'Price accepts up to 5 decimal places'
);
});
});
});
@@ -1,91 +0,0 @@
import { selectAsset } from '../support/helpers';
const amountField = 'input[name="amount"]';
const transferText = 'transfer-intro-text';
const errorText = 'input-error-text';
const formFieldError = 'input-error-text';
const keyID = `[data-testid="${transferText}"] > .rounded-md`;
const manageVegaWallet = 'manage-vega-wallet';
const submitTransferBtn = '[type="submit"]';
const toAddressField = '[name="toAddress"]';
const transferForm = 'transfer-form';
const walletTransfer = 'wallet-transfer';
const ASSET_EURO = 1;
const ASSET_SEPOLIA_TBTC = 2;
describe(
'transfer form validation',
{ tags: '@regression', testIsolation: true },
() => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Accounts');
cy.wait('@Assets');
cy.mockVegaWalletTransaction();
});
it('transfer Text', () => {
// 1003-TRAN-003
cy.getByTestId(transferText)
.should('exist')
.get(keyID)
.invoke('text')
.should('match', /[\w.]{6}…[\w.]{6}/);
});
it('invalid vega key validation', () => {
//1003-TRAN-013
//1003-TRAN-004
cy.getByTestId(transferForm).should('be.visible');
cy.contains('Enter manually').click();
cy.getByTestId(transferForm).find(toAddressField).type('asd');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Invalid Vega key');
cy.contains('label', 'Vega key').should('be.visible');
cy.contains('label', 'Asset').should('be.visible');
cy.contains('label', 'Amount').should('be.visible');
});
it('empty fields', () => {
// 1003-TRAN-012
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
cy.getByTestId(formFieldError).should('have.length', 3);
});
it('min amount', () => {
// 1002-WITH-010
// 1003-TRAN-014
selectAsset(ASSET_SEPOLIA_TBTC);
cy.get(amountField).clear().type('0');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'Value is below minimum'
);
});
it('max amount', () => {
// 1003-TRAN-002
// 1003-TRAN-011
// 1003-TRAN-002
selectAsset(ASSET_EURO); // Will be above maximum because the vega wallet doesn't have any collateral
cy.get(amountField).clear().type('1001', { delay: 100 });
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'You cannot transfer more than your available collateral'
);
});
}
);
@@ -1,5 +0,0 @@
import MarketPage from '../market';
export const ClosedMarketPage = () => {
return <MarketPage closed />;
};
@@ -1 +0,0 @@
export { ClosedMarketPage as default } from './closed-market';
@@ -0,0 +1,48 @@
import { t } from '@vegaprotocol/i18n';
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import {
SidebarButton,
SidebarDivider,
ViewType,
} from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const LiquiditySidebar = () => {
const currentRouteId = useGetCurrentRouteId();
return (
<>
<SidebarButton
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
<SidebarDivider />
<SidebarButton
view={ViewType.Order}
icon={VegaIconNames.TICKET}
tooltip={t('Order')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
routeId={currentRouteId}
/>
</>
);
};
@@ -43,7 +43,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
const asset = getAsset(market); const asset = getAsset(market);
return ( return (
<div className="flex gap-8"> <>
<HeaderStat heading={t('Mark Price')} testId="market-price"> <HeaderStat heading={t('Mark Price')} testId="market-price">
<MarketMarkPrice <MarketMarkPrice
marketId={market.id} marketId={market.id}
@@ -86,6 +86,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
<MarketLiquiditySupplied <MarketLiquiditySupplied
marketId={market.id} marketId={market.id}
assetDecimals={asset?.decimals || 0} assetDecimals={asset?.decimals || 0}
quantum={asset.quantum}
/> />
{market.tradableInstrument.instrument.product.__typename === 'Future' && ( {market.tradableInstrument.instrument.product.__typename === 'Future' && (
<HeaderStat <HeaderStat
@@ -144,7 +145,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
</HeaderStat> </HeaderStat>
)} )}
<MarketProposalNotification marketId={market.id} /> <MarketProposalNotification marketId={market.id} />
</div> </>
); );
}; };
+4 -19
View File
@@ -9,10 +9,9 @@ import { useGlobalStore, usePageTitleStore } from '../../stores';
import { TradeGrid } from './trade-grid'; import { TradeGrid } from './trade-grid';
import { TradePanels } from './trade-panels'; import { TradePanels } from './trade-panels';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Links, Routes } from '../../lib/links'; import { Links } from '../../lib/links';
import { ViewType, useSidebar } from '../../components/sidebar'; import { ViewType, useSidebar } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { MarketState } from '@vegaprotocol/types';
const calculatePrice = (markPrice?: string, decimalPlaces?: number) => { const calculatePrice = (markPrice?: string, decimalPlaces?: number) => {
return markPrice && decimalPlaces return markPrice && decimalPlaces
@@ -57,7 +56,7 @@ const TitleUpdater = ({
return null; return null;
}; };
export const MarketPage = ({ closed }: { closed?: boolean }) => { export const MarketPage = () => {
const { marketId } = useParams(); const { marketId } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const currentRouteId = useGetCurrentRouteId(); const currentRouteId = useGetCurrentRouteId();
@@ -70,25 +69,11 @@ export const MarketPage = ({ closed }: { closed?: boolean }) => {
const { data, loading } = useMarket(marketId); const { data, loading } = useMarket(marketId);
useEffect(() => {
if (
data?.state &&
[
MarketState.STATE_SETTLED,
MarketState.STATE_TRADING_TERMINATED,
].includes(data.state) &&
currentRouteId !== Routes.CLOSED_MARKETS &&
marketId
) {
navigate(Links.CLOSED_MARKETS(marketId));
}
}, [data?.state, currentRouteId, navigate, marketId]);
useEffect(() => { useEffect(() => {
if (data?.id && data.id !== lastMarketId && !closed) { if (data?.id && data.id !== lastMarketId && !closed) {
update({ marketId: data.id }); update({ marketId: data.id });
} }
}, [update, lastMarketId, data?.id, closed]); }, [update, lastMarketId, data?.id]);
useEffect(() => { useEffect(() => {
if (largeScreen && view === undefined) { if (largeScreen && view === undefined) {
@@ -97,7 +82,7 @@ export const MarketPage = ({ closed }: { closed?: boolean }) => {
currentRouteId currentRouteId
); );
} }
}, [setViews, view, currentRouteId, largeScreen, closed]); }, [setViews, view, currentRouteId, largeScreen]);
const pinnedAsset = data && getAsset(data); const pinnedAsset = data && getAsset(data);
@@ -73,10 +73,19 @@ const MainGrid = memo(
{market && {market &&
market.tradableInstrument.instrument.product.__typename === market.tradableInstrument.instrument.product.__typename ===
'Perpetual' ? ( 'Perpetual' ? (
<Tab id="funding" name={t('Funding')}> <Tab id="funding-history" name={t('Funding history')}>
<TradingViews.funding.component marketId={marketId} /> <TradingViews.funding.component marketId={marketId} />
</Tab> </Tab>
) : null} ) : null}
{market &&
market.tradableInstrument.instrument.product.__typename ===
'Perpetual' ? (
<Tab id="funding-payments" name={t('Funding payments')}>
<TradingViews.fundingPayments.component
marketId={marketId}
/>
</Tab>
) : null}
</Tabs> </Tabs>
</TradeGridChild> </TradeGridChild>
</ResizableGridPanel> </ResizableGridPanel>
@@ -114,7 +123,7 @@ const MainGrid = memo(
<Tab <Tab
id="open-orders" id="open-orders"
name={t('Open')} name={t('Open')}
menu={<TradingViews.activeOrders.menu marketId={marketId} />} menu={<TradingViews.activeOrders.menu />}
> >
<TradingViews.orders.component filter={Filter.Open} /> <TradingViews.orders.component filter={Filter.Open} />
</Tab> </Tab>
@@ -127,7 +136,7 @@ const MainGrid = memo(
<Tab <Tab
id="orders" id="orders"
name={t('All')} name={t('All')}
menu={<TradingViews.orders.menu marketId={marketId} />} menu={<TradingViews.orders.menu />}
> >
<TradingViews.orders.component /> <TradingViews.orders.component />
</Tab> </Tab>
@@ -139,9 +148,6 @@ const MainGrid = memo(
<Tab id="fills" name={t('Fills')}> <Tab id="fills" name={t('Fills')}>
<TradingViews.fills.component /> <TradingViews.fills.component />
</Tab> </Tab>
<Tab id="funding-payments" name={t('Funding payments')}>
<TradingViews.fundingPayments.component />
</Tab>
<Tab <Tab
id="accounts" id="accounts"
name={t('Collateral')} name={t('Collateral')}
@@ -43,7 +43,7 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
return ( return (
<div className="flex gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default"> <div className="flex gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
<Menu marketId={market?.id || ''} /> <Menu />
</div> </div>
); );
} }
@@ -363,12 +363,12 @@ describe('Closed', () => {
const container = within( const container = within(
document.querySelector('.ag-center-cols-container') as HTMLElement document.querySelector('.ag-center-cols-container') as HTMLElement
); );
const cell = container.getAllByRole('gridcell', {
name: (_name, element) => element.getAttribute('col-id') === 'code',
})[0];
expect(within(cell).getByTestId('stack-cell-secondary')).toHaveTextContent( const cells = await container.findAllByRole('gridcell');
'PRNT' const cell = cells.find((el) => el.getAttribute('col-id') === 'code');
);
expect(
within(cell as HTMLElement).getByTestId('stack-cell-secondary')
).toHaveTextContent('PRNT');
}); });
}); });
+9 -6
View File
@@ -1,3 +1,4 @@
import type { CellClickedEvent } from 'ag-grid-community';
import compact from 'lodash/compact'; import compact from 'lodash/compact';
import { isAfter } from 'date-fns'; import { isAfter } from 'date-fns';
import type { import type {
@@ -5,6 +6,7 @@ import type {
VegaValueFormatterParams, VegaValueFormatterParams,
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import { AgGrid, COL_DEFS } from '@vegaprotocol/datagrid'; import { AgGrid, COL_DEFS } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type { Asset } from '@vegaprotocol/types'; import type { Asset } from '@vegaprotocol/types';
@@ -17,13 +19,11 @@ import {
import { closedMarketsWithDataProvider, getAsset } from '@vegaprotocol/markets'; import { closedMarketsWithDataProvider, getAsset } from '@vegaprotocol/markets';
import type { DataSourceFilterFragment } from '@vegaprotocol/markets'; import type { DataSourceFilterFragment } from '@vegaprotocol/markets';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import { SettlementDateCell } from './settlement-date-cell'; import { SettlementDateCell } from './settlement-date-cell';
import { SettlementPriceCell } from './settlement-price-cell'; import { SettlementPriceCell } from './settlement-price-cell';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { MarketCodeCell } from './market-code-cell'; import { MarketCodeCell } from './market-code-cell';
import { MarketActionsDropdown } from './market-table-actions'; import { MarketActionsDropdown } from './market-table-actions';
import type { CellClickedEvent } from 'ag-grid-community';
import { useClosedMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
type SettlementAsset = Pick< type SettlementAsset = Pick<
Asset, Asset,
@@ -127,7 +127,7 @@ const ClosedMarketsDataGrid = ({
rowData: Row[]; rowData: Row[];
error: Error | undefined; error: Error | undefined;
}) => { }) => {
const handleOnSelect = useClosedMarketClickHandler(); const handleOnSelect = useMarketClickHandler();
const openAssetDialog = useAssetDetailsDialogStore((store) => store.open); const openAssetDialog = useAssetDetailsDialogStore((store) => store.open);
const colDefs = useMemo(() => { const colDefs = useMemo(() => {
@@ -302,8 +302,11 @@ const ClosedMarketsDataGrid = ({
return; return;
} }
// @ts-ignore metaKey exists handleOnSelect(
handleOnSelect(data.id, event ? event.metaKey : false); data.id,
// @ts-ignore metaKey exists
event ? event.metaKey : false
);
}} }}
/> />
); );
@@ -0,0 +1,67 @@
import { Route, Routes, useParams } from 'react-router-dom';
import { MarketState } from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
import { useMarket } from '@vegaprotocol/markets';
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import {
SidebarButton,
SidebarDivider,
ViewType,
} from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const MarketsSidebar = () => {
const { marketId } = useParams();
const currentRouteId = useGetCurrentRouteId();
const { data } = useMarket(marketId);
const active =
data &&
[MarketState.STATE_ACTIVE, MarketState.STATE_PENDING].includes(data.state);
return (
<>
<SidebarButton
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
<Routes>
<Route
path=":marketId"
element={
<>
<SidebarDivider />
{active && (
<SidebarButton
view={ViewType.Order}
icon={VegaIconNames.TICKET}
tooltip={t('Order')}
routeId={currentRouteId}
/>
)}
<SidebarButton
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
routeId={currentRouteId}
/>
</>
}
/>
</Routes>
</>
);
};
@@ -1,39 +0,0 @@
query AccountHistory(
$partyId: ID!
$assetId: ID!
$accountTypes: [AccountType!]
$dateRange: DateRange
$marketIds: [ID!]
) {
balanceChanges(
filter: {
partyIds: [$partyId]
accountTypes: $accountTypes
assetId: $assetId
marketIds: $marketIds
}
dateRange: $dateRange
) {
edges {
node {
timestamp
partyId
balance
marketId
assetId
accountType
}
}
}
}
query AccountsWithBalance($partyId: ID!, $dateRange: DateRange) {
balanceChanges(filter: { partyIds: [$partyId] }, dateRange: $dateRange) {
edges {
node {
assetId
accountType
}
}
}
}
@@ -1,117 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type AccountHistoryQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
assetId: Types.Scalars['ID'];
accountTypes?: Types.InputMaybe<Array<Types.AccountType> | Types.AccountType>;
dateRange?: Types.InputMaybe<Types.DateRange>;
marketIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
}>;
export type AccountHistoryQuery = { __typename?: 'Query', balanceChanges: { __typename?: 'AggregatedBalanceConnection', edges: Array<{ __typename?: 'AggregatedBalanceEdge', node: { __typename?: 'AggregatedBalance', timestamp: any, partyId?: string | null, balance: string, marketId?: string | null, assetId?: string | null, accountType?: Types.AccountType | null } } | null> } };
export type AccountsWithBalanceQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
dateRange?: Types.InputMaybe<Types.DateRange>;
}>;
export type AccountsWithBalanceQuery = { __typename?: 'Query', balanceChanges: { __typename?: 'AggregatedBalanceConnection', edges: Array<{ __typename?: 'AggregatedBalanceEdge', node: { __typename?: 'AggregatedBalance', assetId?: string | null, accountType?: Types.AccountType | null } } | null> } };
export const AccountHistoryDocument = gql`
query AccountHistory($partyId: ID!, $assetId: ID!, $accountTypes: [AccountType!], $dateRange: DateRange, $marketIds: [ID!]) {
balanceChanges(
filter: {partyIds: [$partyId], accountTypes: $accountTypes, assetId: $assetId, marketIds: $marketIds}
dateRange: $dateRange
) {
edges {
node {
timestamp
partyId
balance
marketId
assetId
accountType
}
}
}
}
`;
/**
* __useAccountHistoryQuery__
*
* To run a query within a React component, call `useAccountHistoryQuery` and pass it any options that fit your needs.
* When your component renders, `useAccountHistoryQuery` 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 } = useAccountHistoryQuery({
* variables: {
* partyId: // value for 'partyId'
* assetId: // value for 'assetId'
* accountTypes: // value for 'accountTypes'
* dateRange: // value for 'dateRange'
* marketIds: // value for 'marketIds'
* },
* });
*/
export function useAccountHistoryQuery(baseOptions: Apollo.QueryHookOptions<AccountHistoryQuery, AccountHistoryQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<AccountHistoryQuery, AccountHistoryQueryVariables>(AccountHistoryDocument, options);
}
export function useAccountHistoryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AccountHistoryQuery, AccountHistoryQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<AccountHistoryQuery, AccountHistoryQueryVariables>(AccountHistoryDocument, options);
}
export type AccountHistoryQueryHookResult = ReturnType<typeof useAccountHistoryQuery>;
export type AccountHistoryLazyQueryHookResult = ReturnType<typeof useAccountHistoryLazyQuery>;
export type AccountHistoryQueryResult = Apollo.QueryResult<AccountHistoryQuery, AccountHistoryQueryVariables>;
export const AccountsWithBalanceDocument = gql`
query AccountsWithBalance($partyId: ID!, $dateRange: DateRange) {
balanceChanges(filter: {partyIds: [$partyId]}, dateRange: $dateRange) {
edges {
node {
assetId
accountType
}
}
}
}
`;
/**
* __useAccountsWithBalanceQuery__
*
* To run a query within a React component, call `useAccountsWithBalanceQuery` and pass it any options that fit your needs.
* When your component renders, `useAccountsWithBalanceQuery` 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 } = useAccountsWithBalanceQuery({
* variables: {
* partyId: // value for 'partyId'
* dateRange: // value for 'dateRange'
* },
* });
*/
export function useAccountsWithBalanceQuery(baseOptions: Apollo.QueryHookOptions<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>(AccountsWithBalanceDocument, options);
}
export function useAccountsWithBalanceLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>(AccountsWithBalanceDocument, options);
}
export type AccountsWithBalanceQueryHookResult = ReturnType<typeof useAccountsWithBalanceQuery>;
export type AccountsWithBalanceLazyQueryHookResult = ReturnType<typeof useAccountsWithBalanceLazyQuery>;
export type AccountsWithBalanceQueryResult = Apollo.QueryResult<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>;
@@ -1,349 +0,0 @@
import { addDecimal, fromNanoSeconds } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useVegaWallet } from '@vegaprotocol/wallet';
import compact from 'lodash/compact';
import uniqBy from 'lodash/uniqBy';
import type { ChangeEvent } from 'react';
import { useCallback, useMemo, useState } from 'react';
import type { AccountHistoryQuery } from './__generated__/AccountHistory';
import { useAccountHistoryQuery } from './__generated__/AccountHistory';
import * as Schema from '@vegaprotocol/types';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import {
AsyncRenderer,
Splash,
Toggle,
TradingButton,
TradingDropdown,
TradingDropdownContent,
TradingDropdownItem,
TradingDropdownTrigger,
} from '@vegaprotocol/ui-toolkit';
import { AccountTypeMapping } from '@vegaprotocol/types';
import { PriceChart } from 'pennant';
import 'pennant/dist/style.css';
import type { Account } from '@vegaprotocol/accounts';
import { accountsDataProvider } from '@vegaprotocol/accounts';
import {
useLocalStorageSnapshot,
useThemeSwitcher,
} from '@vegaprotocol/react-helpers';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { getAsset, type Market } from '@vegaprotocol/markets';
export const DateRange = {
RANGE_1D: '1D',
RANGE_7D: '7D',
RANGE_1M: '1M',
RANGE_3M: '3M',
RANGE_1Y: '1Y',
RANGE_YTD: 'YTD',
RANGE_ALL: 'All',
};
const dateRangeToggleItems = Object.entries(DateRange).map(([_, value]) => ({
label: t(value),
value: value,
}));
export const calculateStartDate = (range: string): string | undefined => {
const now = new Date();
switch (range) {
case DateRange.RANGE_1D:
return new Date(now.setDate(now.getDate() - 1)).toISOString();
case DateRange.RANGE_7D:
return new Date(now.setDate(now.getDate() - 7)).toISOString();
case DateRange.RANGE_1M:
return new Date(now.setMonth(now.getMonth() - 1)).toISOString();
case DateRange.RANGE_3M:
return new Date(now.setMonth(now.getMonth() - 3)).toISOString();
case DateRange.RANGE_1Y:
return new Date(now.setFullYear(now.getFullYear() - 1)).toISOString();
case DateRange.RANGE_YTD:
return new Date(now.setMonth(0)).toISOString();
default:
return undefined;
}
};
export const AccountHistoryContainer = () => {
const { pubKey } = useVegaWallet();
const { data: assets } = useAssetsDataProvider();
if (!pubKey) {
return <Splash>{t('Please connect Vega wallet')}</Splash>;
}
return (
<AsyncRenderer loading={!assets} error={undefined} data={assets}>
{assets && <AccountHistoryManager pubKey={pubKey} assetData={assets} />}
</AsyncRenderer>
);
};
const AccountHistoryManager = ({
pubKey,
assetData,
}: {
pubKey: string;
assetData: AssetFieldsFragment[];
}) => {
const [accountType, setAccountType] = useState<Schema.AccountType>(
Schema.AccountType.ACCOUNT_TYPE_GENERAL
);
const variablesForOneTimeQuery = useMemo(
() => ({
partyId: pubKey,
}),
[pubKey]
);
const { data: accounts } = useDataProvider({
dataProvider: accountsDataProvider,
variables: variablesForOneTimeQuery,
skip: !pubKey,
});
const assetIds = useMemo(
() => accounts?.map((e) => e?.asset?.id) || [],
[accounts]
);
const assets = useMemo(
() =>
assetData
.filter((a) => assetIds.includes(a.id))
.sort((a, b) => a.name.localeCompare(b.name)),
[assetData, assetIds]
);
const [assetId, setAssetId] = useLocalStorageSnapshot(
'account-history-active-asset-id'
);
const asset = useMemo(
() => assets.find((a) => a.id === assetId) || assets[0],
[assetId, assets]
);
const [range, setRange] = useState<typeof DateRange[keyof typeof DateRange]>(
DateRange.RANGE_1M
);
const [market, setMarket] = useState<Market | null>(null);
const marketFilterCb = useCallback(
(item: Market) => {
const itemAsset = getAsset(item);
return !asset?.id || itemAsset?.id === asset?.id;
},
[asset?.id]
);
const markets = useMemo<Market[] | null>(() => {
const arr =
accounts
?.filter((item: Account) => Boolean(item && item.market))
.map<Market>((item) => item.market as Market) ?? null;
return arr
? uniqBy(arr.filter(marketFilterCb), 'id').sort((a, b) =>
a.tradableInstrument.instrument.code.localeCompare(
b.tradableInstrument.instrument.code
)
)
: null;
}, [accounts, marketFilterCb]);
const resolveMarket = useCallback(
(m: Market) => {
setMarket(m);
const itemAsset = getAsset(m);
const newAssetId = itemAsset?.id;
const newAsset = assets.find((item) => item.id === newAssetId);
if ((!asset || (assets && newAssetId !== asset.id)) && newAsset) {
setAssetId(newAsset.id);
}
},
[asset, assets, setAssetId]
);
const variables = useMemo(
() => ({
partyId: pubKey,
assetId: asset?.id || '',
accountTypes: accountType ? [accountType] : undefined,
dateRange:
range === 'All' ? undefined : { start: calculateStartDate(range) },
marketIds: market?.id ? [market.id] : undefined,
}),
[pubKey, asset, accountType, range, market?.id]
);
const { data } = useAccountHistoryQuery({
variables,
skip: !asset || !pubKey,
});
return (
<div className="flex flex-col h-full gap-2">
<div className="flex flex-wrap justify-between px-1 pt-2 gap-2">
<div className="flex items-center gap-1 shrink-0">
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{accountType
? `${
AccountTypeMapping[
accountType as keyof typeof Schema.AccountType
]
} Account`
: t('Select account type')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{[
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
Schema.AccountType.ACCOUNT_TYPE_BOND,
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
].map((type) => (
<TradingDropdownItem
key={type}
onClick={() => {
setAccountType(type as Schema.AccountType);
// if not a margin account clear any market selection
if (type !== Schema.AccountType.ACCOUNT_TYPE_MARGIN) {
setMarket(null);
}
}}
>
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{asset ? asset.symbol : t('Select asset')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{assets.map((a) => (
<TradingDropdownItem
key={a.id}
onClick={() => {
setAssetId(a.id);
// if the selected asset is different to the selected market clear the market
if (market && a.id !== getAsset(market).id) {
setMarket(null);
}
}}
>
{a.symbol}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{market
? market.tradableInstrument.instrument.code
: t('Select market')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{market && (
<TradingDropdownItem key="0" onClick={() => setMarket(null)}>
{t('All markets')}
</TradingDropdownItem>
)}
{markets?.map((m) => (
<TradingDropdownItem
key={m.id}
onClick={() => resolveMarket(m)}
>
{m.tradableInstrument.instrument.code}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
</div>
<div className="justify-items-end">
<Toggle
id="account-history-date-range"
name="account-history-date-range"
toggles={dateRangeToggleItems}
checkedValue={range}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setRange(e.target.value as keyof typeof DateRange)
}
size="sm"
/>
</div>
</div>
<div className="flex-1">
{asset && (
<div className="h-full">
<AccountHistoryChart
data={data}
accountType={accountType}
asset={asset}
/>
</div>
)}
</div>
</div>
);
};
export const AccountHistoryChart = ({
data,
accountType,
asset,
}: {
data: AccountHistoryQuery | undefined;
accountType: Schema.AccountType;
asset: AssetFieldsFragment;
}) => {
const { theme } = useThemeSwitcher();
const values: { cols: [string, string]; rows: [Date, number][] } | null =
useMemo(() => {
if (!data?.balanceChanges.edges.length) {
return null;
}
const valuesData = compact(data.balanceChanges.edges)
.reduce((acc, edge) => {
if (edge.node.accountType === accountType) {
acc?.push({
datetime: fromNanoSeconds(edge.node.timestamp),
balance: Number(addDecimal(edge.node.balance, asset.decimals)),
});
}
return acc;
}, [] as { datetime: Date; balance: number }[])
.reverse();
return {
cols: ['Date', `${asset.symbol} account balance`],
rows: compact(valuesData).map((d) => [d.datetime, d.balance]),
};
}, [accountType, asset.decimals, asset.symbol, data?.balanceChanges.edges]);
if (!data || !values?.rows.length) {
return <Splash> {t('No account history data')}</Splash>;
}
return <PriceChart data={values} theme={theme} />;
};
@@ -0,0 +1,31 @@
import { t } from '@vegaprotocol/i18n';
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { SidebarButton, ViewType } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const PortfolioSidebar = () => {
const currentRouteId = useGetCurrentRouteId();
return (
<>
<SidebarButton
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
</>
);
};
@@ -15,7 +15,6 @@ import { PositionsMenu } from '../../components/positions-menu';
import { WithdrawalsContainer } from '../../components/withdrawals-container'; import { WithdrawalsContainer } from '../../components/withdrawals-container';
import { OrdersContainer } from '../../components/orders-container'; import { OrdersContainer } from '../../components/orders-container';
import { LedgerContainer } from '../../components/ledger-container'; import { LedgerContainer } from '../../components/ledger-container';
import { AccountHistoryContainer } from './account-history-container';
import { import {
ResizableGrid, ResizableGrid,
ResizableGridPanel, ResizableGridPanel,
@@ -66,10 +65,7 @@ export const Portfolio = () => {
<ResizableGrid vertical onChange={handleOnLayoutChange}> <ResizableGrid vertical onChange={handleOnLayoutChange}>
<ResizableGridPanel minSize={75}> <ResizableGridPanel minSize={75}>
<PortfolioGridChild> <PortfolioGridChild>
<Tabs storageKey="console-portfolio-top"> <Tabs storageKey="console-portfolio-top-1">
<Tab id="account-history" name={t('Account history')}>
<AccountHistoryContainer />
</Tab>
<Tab <Tab
id="positions" id="positions"
name={t('Positions')} name={t('Positions')}
@@ -4,11 +4,11 @@ import {
AppLoader, AppLoader,
DocsLinks, DocsLinks,
NetworkLoader, NetworkLoader,
NodeFailure,
NodeGuard, NodeGuard,
useEnvironment, useEnvironment,
} from '@vegaprotocol/environment'; } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { MaintenancePage } from '@vegaprotocol/ui-toolkit';
import { VegaWalletProvider } from '@vegaprotocol/wallet'; import { VegaWalletProvider } from '@vegaprotocol/wallet';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Web3Provider } from './web3-provider'; import { Web3Provider } from './web3-provider';
@@ -20,15 +20,10 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
VEGA_ENV, VEGA_ENV,
VEGA_WALLET_URL, VEGA_WALLET_URL,
VEGA_EXPLORER_URL, VEGA_EXPLORER_URL,
MAINTENANCE_PAGE,
MOZILLA_EXTENSION_URL, MOZILLA_EXTENSION_URL,
CHROME_EXTENSION_URL, CHROME_EXTENSION_URL,
} = useEnvironment(); } = useEnvironment();
if (MAINTENANCE_PAGE) {
return <MaintenancePage />;
}
if ( if (
!VEGA_URL || !VEGA_URL ||
!VEGA_WALLET_URL || !VEGA_WALLET_URL ||
@@ -37,7 +32,7 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
!MOZILLA_EXTENSION_URL || !MOZILLA_EXTENSION_URL ||
!DocsLinks !DocsLinks
) { ) {
return null; return <AppLoader />;
} }
return ( return (
@@ -50,9 +45,14 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
> >
<NodeGuard <NodeGuard
skeleton={<AppLoader />} skeleton={<AppLoader />}
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />} failure={<NodeFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
> >
<Web3Provider> <Web3Provider
skeleton={<AppLoader />}
failure={
<AppFailure title={t(`Could not configure web3 provider`)} />
}
>
<VegaWalletProvider <VegaWalletProvider
config={{ config={{
network: VEGA_ENV, network: VEGA_ENV,
@@ -6,14 +6,20 @@ import {
useWeb3ConnectStore, useWeb3ConnectStore,
createDefaultProvider, createDefaultProvider,
} from '@vegaprotocol/web3'; } from '@vegaprotocol/web3';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { useEffect } from 'react'; import { useEffect } from 'react';
export const Web3Provider = ({ children }: { children: ReactNode }) => { export const Web3Provider = ({
children,
skeleton,
failure,
}: {
children: ReactNode;
skeleton: ReactNode;
failure: ReactNode;
}) => {
const { config, loading, error } = useEthereumConfig(); const { config, loading, error } = useEthereumConfig();
const { ETHEREUM_PROVIDER_URL, ETH_LOCAL_PROVIDER_URL, ETH_WALLET_MNEMONIC } = const { ETHEREUM_PROVIDER_URL, ETH_LOCAL_PROVIDER_URL, ETH_WALLET_MNEMONIC } =
useEnvironment(); useEnvironment();
@@ -49,23 +55,20 @@ export const Web3Provider = ({ children }: { children: ReactNode }) => {
ETH_WALLET_MNEMONIC, ETH_WALLET_MNEMONIC,
]); ]);
if (error) {
return <>{failure}</>;
}
if (loading || !connectors.length) {
return <>{skeleton}</>;
}
return ( return (
<AsyncRenderer <Web3ProviderInternal
loading={loading} connectors={connectors}
error={error} defaultProvider={defaultProvider}
data={connectors}
noDataCondition={(d) => {
if (!d) return true;
return d.length < 1;
}}
noDataMessage={t('Could not fetch Ethereum configuration')}
> >
<Web3ProviderInternal <>{children}</>
connectors={connectors} </Web3ProviderInternal>
defaultProvider={defaultProvider}
>
<>{children}</>
</Web3ProviderInternal>
</AsyncRenderer>
); );
}; };
@@ -9,10 +9,36 @@ import { useMemo } from 'react';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers'; import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Splash } from '@vegaprotocol/ui-toolkit';
import {
DateRange, const calculateStartDate = (range: string): string | undefined => {
calculateStartDate, const now = new Date();
} from '../../client-pages/portfolio/account-history-container'; switch (range) {
case DateRange.RANGE_1D:
return new Date(now.setDate(now.getDate() - 1)).toISOString();
case DateRange.RANGE_7D:
return new Date(now.setDate(now.getDate() - 7)).toISOString();
case DateRange.RANGE_1M:
return new Date(now.setMonth(now.getMonth() - 1)).toISOString();
case DateRange.RANGE_3M:
return new Date(now.setMonth(now.getMonth() - 3)).toISOString();
case DateRange.RANGE_1Y:
return new Date(now.setFullYear(now.getFullYear() - 1)).toISOString();
case DateRange.RANGE_YTD:
return new Date(now.setMonth(0)).toISOString();
default:
return undefined;
}
};
const DateRange = {
RANGE_1D: '1D',
RANGE_7D: '7D',
RANGE_1M: '1M',
RANGE_3M: '3M',
RANGE_1Y: '1Y',
RANGE_YTD: 'YTD',
RANGE_ALL: 'All',
};
export const FundingContainer = ({ marketId }: { marketId: string }) => { export const FundingContainer = ({ marketId }: { marketId: string }) => {
const { theme } = useThemeSwitcher(); const { theme } = useThemeSwitcher();
@@ -9,7 +9,11 @@ import type { DataGridSlice } from '../../stores/datagrid-store-slice';
import { createDataGridSlice } from '../../stores/datagrid-store-slice'; import { createDataGridSlice } from '../../stores/datagrid-store-slice';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler'; import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
export const FundingPaymentsContainer = () => { export const FundingPaymentsContainer = ({
marketId,
}: {
marketId?: string;
}) => {
const onMarketClick = useMarketClickHandler(true); const onMarketClick = useMarketClickHandler(true);
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
@@ -33,6 +37,7 @@ export const FundingPaymentsContainer = () => {
return ( return (
<FundingPaymentsManager <FundingPaymentsManager
partyId={pubKey} partyId={pubKey}
marketId={marketId}
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
gridProps={gridStoreCallbacks} gridProps={gridStoreCallbacks}
/> />
+3 -3
View File
@@ -17,11 +17,11 @@ export const Header = ({ title, children }: TradeMarketHeaderProps) => {
); );
return ( return (
<header className={headerClasses}> <header className={headerClasses}>
<div className="hidden lg:flex flex-col justify-center items-start pl-3 lg:pl-4 pt-2 xl:pb-2 pb-0"> <div className="flex-col items-start justify-center hidden pt-2 pb-0 pl-3 lg:flex lg:pl-4 xl:pb-2">
{title} {title}
</div> </div>
<div data-testid="header-summary" className="min-w-0"> <div data-testid="header-summary" className="min-w-0">
<div className="px-3 lg:px-4 py-2 flex flex-nowrap gap-4 items-center text-xs overflow-x-auto"> <div className="flex items-center px-3 py-2 overflow-x-auto text-xs lg:px-4 flex-nowrap gap-6">
{children} {children}
</div> </div>
</div> </div>
@@ -70,7 +70,7 @@ export const HeaderTitle = ({ children }: { children: ReactNode }) => {
return ( return (
<h1 <h1
data-testid="header-title" data-testid="header-title"
className="flex gap-4 items-center text-lg whitespace-nowrap xl:pr-4 xl:border-r border-default" className="flex items-center text-lg gap-4 whitespace-nowrap xl:pr-4 xl:border-r border-default"
> >
{children} {children}
</h1> </h1>
@@ -1,11 +1,16 @@
import { Outlet, Routes, Route } from 'react-router-dom'; import type { ReactNode } from 'react';
import { Outlet } from 'react-router-dom';
import { Sidebar, SidebarContent, useSidebar } from '../sidebar'; import { Sidebar, SidebarContent, useSidebar } from '../sidebar';
import classNames from 'classnames'; import classNames from 'classnames';
import { MarketHeader } from '../market-header';
import { LiquidityHeader } from '../liquidity-header';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const LayoutWithSidebar = () => { export const LayoutWithSidebar = ({
header,
sidebar,
}: {
header?: ReactNode;
sidebar?: ReactNode;
}) => {
const currentRouteId = useGetCurrentRouteId(); const currentRouteId = useGetCurrentRouteId();
const views = useSidebar((store) => store.views); const views = useSidebar((store) => store.views);
const sidebarView = views[currentRouteId] || null; const sidebarView = views[currentRouteId] || null;
@@ -20,16 +25,7 @@ export const LayoutWithSidebar = () => {
return ( return (
<div className={gridClasses}> <div className={gridClasses}>
<div className="col-span-full"> <div className="col-span-full">{header}</div>
<Routes>
<Route path="markets/:marketId" element={<MarketHeader />} />
<Route
path="markets/all/closed/:marketId"
element={<MarketHeader />}
/>
<Route path="liquidity/:marketId" element={<LiquidityHeader />} />
</Routes>
</div>
<main <main
className={classNames('col-start-1 col-end-1', { className={classNames('col-start-1 col-end-1', {
'lg:col-end-3': !sidebarOpen, 'lg:col-end-3': !sidebarOpen,
@@ -40,9 +36,12 @@ export const LayoutWithSidebar = () => {
</main> </main>
<aside <aside
// min-h-0 is needed as this element is part of a grid, we want the content to be scrollable, without it it will push the grid element taller // min-h-0 is needed as this element is part of a grid, we want the content to be scrollable, without it it will push the grid element taller
className={classNames('col-start-1 lg:col-start-2 min-h-0', { className={classNames(
hidden: !sidebarOpen, 'col-start-1 lg:col-start-2 min-h-0 lg:border-l border-default lg:bg-vega-clight-800 dark:lg:bg-vega-cdark-800',
})} {
hidden: !sidebarOpen,
}
)}
> >
<SidebarContent /> <SidebarContent />
</aside> </aside>
@@ -54,7 +53,7 @@ export const LayoutWithSidebar = () => {
'lg:row-start-2 lg:row-span-full lg:col-start-3' 'lg:row-start-2 lg:row-span-full lg:col-start-3'
)} )}
> >
<Sidebar /> <Sidebar options={sidebar} />
</div> </div>
</div> </div>
); );
@@ -18,7 +18,7 @@ import BigNumber from 'bignumber.js';
import { useCheckLiquidityStatus } from '@vegaprotocol/liquidity'; import { useCheckLiquidityStatus } from '@vegaprotocol/liquidity';
import { AuctionTrigger, MarketTradingMode } from '@vegaprotocol/types'; import { AuctionTrigger, MarketTradingMode } from '@vegaprotocol/types';
import { import {
addDecimalsFormatNumber, addDecimalsFormatNumberQuantum,
formatNumberPercentage, formatNumberPercentage,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
@@ -30,12 +30,14 @@ interface Props {
marketId?: string; marketId?: string;
noUpdate?: boolean; noUpdate?: boolean;
assetDecimals: number; assetDecimals: number;
quantum: string;
} }
export const MarketLiquiditySupplied = ({ export const MarketLiquiditySupplied = ({
marketId, marketId,
assetDecimals, assetDecimals,
noUpdate = false, noUpdate = false,
quantum,
}: Props) => { }: Props) => {
const [market, setMarket] = useState<MarketData>(); const [market, setMarket] = useState<MarketData>();
const { params } = useNetworkParams([ const { params } = useNetworkParams([
@@ -79,11 +81,12 @@ export const MarketLiquiditySupplied = ({
}); });
const supplied = market?.suppliedStake const supplied = market?.suppliedStake
? addDecimalsFormatNumber( ? addDecimalsFormatNumberQuantum(
new BigNumber(market?.suppliedStake) new BigNumber(market?.suppliedStake)
.multipliedBy(stakeToCcyVolume || 1) .multipliedBy(stakeToCcyVolume || 1)
.toString(), .toString(),
assetDecimals assetDecimals,
quantum
) )
: '-'; : '-';
@@ -108,9 +111,10 @@ export const MarketLiquiditySupplied = ({
<span>{t('Supplied stake')}</span> <span>{t('Supplied stake')}</span>
<span> <span>
{market?.suppliedStake {market?.suppliedStake
? addDecimalsFormatNumber( ? addDecimalsFormatNumberQuantum(
new BigNumber(market?.suppliedStake).toString(), market?.suppliedStake,
assetDecimals assetDecimals,
quantum
) )
: '-'} : '-'}
</span> </span>
@@ -119,9 +123,10 @@ export const MarketLiquiditySupplied = ({
<span>{t('Target stake')}</span> <span>{t('Target stake')}</span>
<span> <span>
{market?.targetStake {market?.targetStake
? addDecimalsFormatNumber( ? addDecimalsFormatNumberQuantum(
new BigNumber(market?.targetStake).toString(), market?.targetStake,
assetDecimals assetDecimals,
quantum
) )
: '-'} : '-'}
</span> </span>
@@ -21,6 +21,7 @@ const market = {
} as unknown as Market; } as unknown as Market;
let mockDataSuccessorMarket: PartialDeep<Market> | null = null; let mockDataSuccessorMarket: PartialDeep<Market> | null = null;
let mockDataMarketState: Market['state'] | null = null;
jest.mock('@vegaprotocol/data-provider', () => ({ jest.mock('@vegaprotocol/data-provider', () => ({
...jest.requireActual('@vegaprotocol/data-provider'), ...jest.requireActual('@vegaprotocol/data-provider'),
useDataProvider: jest.fn().mockImplementation((args) => { useDataProvider: jest.fn().mockImplementation((args) => {
@@ -43,6 +44,12 @@ jest.mock('@vegaprotocol/utils', () => ({
let mockCandles = {}; let mockCandles = {};
jest.mock('@vegaprotocol/markets', () => ({ jest.mock('@vegaprotocol/markets', () => ({
...jest.requireActual('@vegaprotocol/markets'), ...jest.requireActual('@vegaprotocol/markets'),
useMarketState: (marketId: string) =>
marketId
? {
data: mockDataMarketState,
}
: { data: undefined },
useSuccessorMarket: (marketId: string) => useSuccessorMarket: (marketId: string) =>
marketId marketId
? { ? {
@@ -81,30 +88,6 @@ describe('MarketSuccessorBanner', () => {
}); });
expect(container).toBeEmptyDOMElement(); expect(container).toBeEmptyDOMElement();
}); });
it('successor market not in continuous mode', () => {
mockDataSuccessorMarket = {
...mockDataSuccessorMarket,
tradingMode: Types.MarketTradingMode.TRADING_MODE_NO_TRADING,
};
const { container } = render(<MarketSuccessorBanner market={market} />, {
wrapper: MockedProvider,
});
expect(container).toBeEmptyDOMElement();
expect(allUtils.getMarketExpiryDate).toHaveBeenCalled();
});
it('successor market is not active', () => {
mockDataSuccessorMarket = {
...mockDataSuccessorMarket,
state: Types.MarketState.STATE_PENDING,
};
const { container } = render(<MarketSuccessorBanner market={market} />, {
wrapper: MockedProvider,
});
expect(container).toBeEmptyDOMElement();
expect(allUtils.getMarketExpiryDate).toHaveBeenCalled();
});
}); });
describe('should be displayed', () => { describe('should be displayed', () => {
@@ -120,6 +103,17 @@ describe('MarketSuccessorBanner', () => {
).toHaveAttribute('href', '/#/markets/successorMarketID'); ).toHaveAttribute('href', '/#/markets/successorMarketID');
}); });
it('no successor market data, market settled', () => {
mockDataSuccessorMarket = null;
mockDataMarketState = Types.MarketState.STATE_SETTLED;
render(<MarketSuccessorBanner market={market} />, {
wrapper: MockedProvider,
});
expect(
screen.getByText('This market has been settled')
).toBeInTheDocument();
});
it('should display optionally successor volume', () => { it('should display optionally successor volume', () => {
mockDataSuccessorMarket = { mockDataSuccessorMarket = {
...mockDataSuccessorMarket, ...mockDataSuccessorMarket,
@@ -137,7 +131,9 @@ describe('MarketSuccessorBanner', () => {
render(<MarketSuccessorBanner market={market} />, { render(<MarketSuccessorBanner market={market} />, {
wrapper: MockedProvider, wrapper: MockedProvider,
}); });
expect(screen.getByText('has 101.367 24h vol.')).toBeInTheDocument(); expect(
screen.getByText('has a 24h trading volume of 101.367')
).toBeInTheDocument();
}); });
it('should display optionally duration', () => { it('should display optionally duration', () => {
@@ -4,6 +4,7 @@ import type { Market } from '@vegaprotocol/markets';
import { import {
calcCandleVolume, calcCandleVolume,
useCandles, useCandles,
useMarketState,
useSuccessorMarket, useSuccessorMarket,
} from '@vegaprotocol/markets'; } from '@vegaprotocol/markets';
import { import {
@@ -29,7 +30,9 @@ export const MarketSuccessorBanner = ({
}: { }: {
market: Market | null; market: Market | null;
}) => { }) => {
const { data: successorData } = useSuccessorMarket(market?.id); const { data: marketState } = useMarketState(market?.id);
const isSettled = marketState === Types.MarketState.STATE_SETTLED;
const { data: successorData, loading } = useSuccessorMarket(market?.id);
const [visible, setVisible] = useState(true); const [visible, setVisible] = useState(true);
@@ -45,11 +48,6 @@ export const MarketSuccessorBanner = ({
? intervalToDuration({ start: new Date(), end: expiry }) ? intervalToDuration({ start: new Date(), end: expiry })
: null; : null;
const isInContinuesMode =
successorData?.state === Types.MarketState.STATE_ACTIVE &&
successorData?.tradingMode ===
Types.MarketTradingMode.TRADING_MODE_CONTINUOUS;
const { oneDayCandles } = useCandles({ const { oneDayCandles } = useCandles({
marketId: successorData?.id, marketId: successorData?.id,
}); });
@@ -66,7 +64,7 @@ export const MarketSuccessorBanner = ({
) )
: null; : null;
if (isInContinuesMode && visible) { if (!loading && (isSettled || successorData) && visible) {
return ( return (
<NotificationBanner <NotificationBanner
intent={Intent.Primary} intent={Intent.Primary}
@@ -74,34 +72,47 @@ export const MarketSuccessorBanner = ({
setVisible(false); setVisible(false);
}} }}
> >
<div className="uppercase mb-1"> <div className="uppercase">
{t('This market has been succeeded')} {successorData
</div> ? t('This market has been succeeded')
<div> : t('This market has been settled')}
{duration && (
<span>
{t('This market expires in %s.', [
formatDuration(duration, {
format: [
'years',
'months',
'weeks',
'days',
'hours',
'minutes',
],
}),
])}
</span>
)}{' '}
{t('The successor market')}{' '}
<ExternalLink href={`/#/markets/${successorData?.id}`}>
{successorData?.tradableInstrument.instrument.name}
</ExternalLink>
{successorVolume && (
<span> {t('has %s 24h vol.', [successorVolume])}</span>
)}
</div> </div>
{(duration || successorData) && (
<div className="mt-1">
{duration && (
<span>
{t('This market expires in %s.', [
formatDuration(duration, {
format: [
'years',
'months',
'weeks',
'days',
'hours',
'minutes',
],
}),
])}
</span>
)}
{successorData && (
<>
{' '}
{t('The successor market')}
{!successorVolume ? ' is ' : ' '}
<ExternalLink href={`/#/markets/${successorData?.id}`}>
{successorData?.tradableInstrument.instrument.name}
</ExternalLink>
{successorVolume && (
<span>
{' '}
{t('has a 24h trading volume of %s', [successorVolume])}
</span>
)}
</>
)}
</div>
)}
</NotificationBanner> </NotificationBanner>
); );
} }
@@ -1,6 +1,6 @@
import { Popover, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; import { Popover, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { Header, HeaderTitle } from '../header'; import { Header, HeaderTitle } from '../header';
import { useParams } from 'react-router-dom'; import { Route, Routes, useParams } from 'react-router-dom';
import { MarketSelector } from '../../components/market-selector/market-selector'; import { MarketSelector } from '../../components/market-selector/market-selector';
import { MarketHeaderStats } from '../../client-pages/market/market-header-stats'; import { MarketHeaderStats } from '../../client-pages/market/market-header-stats';
import { useMarket, useMarketList } from '@vegaprotocol/markets'; import { useMarket, useMarketList } from '@vegaprotocol/markets';
@@ -19,36 +19,44 @@ export const MarketHeader = () => {
if (!data) return null; if (!data) return null;
return ( return (
<Header <Routes>
title={ <Route
<Popover path=":marketId"
open={open} element={
onChange={setOpen} <Header
trigger={ title={
<HeaderTitle> <Popover
<span> open={open}
{data.tradableInstrument.instrument.code} onChange={setOpen}
<span className="ml-2 text-xs uppercase text-muted"> trigger={
{' '} <HeaderTitle>
{data.tradableInstrument.instrument.product.__typename && <span>
ProductTypeShortName[ {data.tradableInstrument.instrument.code}
data.tradableInstrument.instrument.product.__typename <span className="ml-1 text-xs uppercase text-muted">
]}{' '} {data.tradableInstrument.instrument.product
</span> .__typename &&
</span> ProductTypeShortName[
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} /> data.tradableInstrument.instrument.product
</HeaderTitle> .__typename
} ]}
alignOffset={-10} </span>
> </span>
<MarketSelector <VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} />
currentMarketId={marketId} </HeaderTitle>
onSelect={() => setOpen(false)} }
/> alignOffset={-10}
</Popover> >
} <MarketSelector
> currentMarketId={marketId}
<MarketHeaderStats market={data} /> onSelect={() => setOpen(false)}
</Header> />
</Popover>
}
>
<MarketHeaderStats market={data} />
</Header>
}
/>
</Routes>
); );
}; };
@@ -56,6 +56,7 @@ const MarketData = ({
variables: { variables: {
marketId: market.id, marketId: market.id,
}, },
fetchPolicy: 'no-cache',
}); });
const marketData = data?.marketsData[0]; const marketData = data?.marketsData[0];
@@ -70,6 +71,8 @@ const MarketData = ({
const marketTradingMode = marketData const marketTradingMode = marketData
? marketData.marketTradingMode ? marketData.marketTradingMode
: market.data
? market.data.marketTradingMode
: market.tradingMode; : market.tradingMode;
const mode = [ const mode = [
@@ -95,7 +98,7 @@ const MarketData = ({
<> <>
<div className="w-2/5" role="gridcell"> <div className="w-2/5" role="gridcell">
<h3 className="flex items-baseline"> <h3 className="flex items-baseline">
<span className="text-sm lg:text-base text-ellipsis whitespace-nowrap overflow-hidden"> <span className="overflow-hidden text-sm lg:text-base text-ellipsis whitespace-nowrap">
{market.tradableInstrument.instrument.code} {market.tradableInstrument.instrument.code}
</span> </span>
{allProducts && productType && ( {allProducts && productType && (
@@ -2,7 +2,10 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { MarketSelector } from './market-selector'; import { MarketSelector } from './market-selector';
import { useMarketList } from '@vegaprotocol/markets'; import { useMarketList } from '@vegaprotocol/markets';
import { createMarketFragment } from '@vegaprotocol/mock'; import {
createMarketFragment,
createMarketsDataFragment,
} from '@vegaprotocol/mock';
import { MarketState } from '@vegaprotocol/types'; import { MarketState } from '@vegaprotocol/types';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
@@ -36,6 +39,10 @@ describe('MarketSelector', () => {
const markets = [ const markets = [
createMarketFragment({ createMarketFragment({
id: 'market-0', id: 'market-0',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
code: 'a', code: 'a',
@@ -56,7 +63,10 @@ describe('MarketSelector', () => {
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-1', id: 'market-1',
state: MarketState.STATE_SUSPENDED, // @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_SUSPENDED,
}),
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
code: 'b', code: 'b',
@@ -77,7 +87,10 @@ describe('MarketSelector', () => {
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-2', id: 'market-2',
state: MarketState.STATE_CLOSED, // @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_CLOSED,
}),
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
product: { product: {
@@ -91,7 +104,10 @@ describe('MarketSelector', () => {
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-3', id: 'market-3',
state: MarketState.STATE_ACTIVE, // @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
code: 'c', code: 'c',
@@ -112,6 +128,10 @@ describe('MarketSelector', () => {
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-4', id: 'market-4',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
code: 'cd', code: 'cd',
@@ -132,7 +152,10 @@ describe('MarketSelector', () => {
}), }),
]; ];
const activeMarkets = markets.filter((m) => isMarketActive(m.state)); const activeMarkets = markets.filter((m) =>
// @ts-ignore candles get joined outside this type
isMarketActive(m.data.marketState)
);
mockUseMarketList.mockReturnValue({ mockUseMarketList.mockReturnValue({
data: markets, data: markets,
loading: false, loading: false,
@@ -4,7 +4,10 @@ import { useMarketSelectorList } from './use-market-selector-list';
import { isMarketActive } from '../../lib/utils'; import { isMarketActive } from '../../lib/utils';
import { Product } from './product-selector'; import { Product } from './product-selector';
import { Sort } from './sort-dropdown'; import { Sort } from './sort-dropdown';
import { createMarketFragment } from '@vegaprotocol/mock'; import {
createMarketFragment,
createMarketsDataFragment,
} from '@vegaprotocol/mock';
import { MarketState } from '@vegaprotocol/types'; import { MarketState } from '@vegaprotocol/types';
import { useMarketList } from '@vegaprotocol/markets'; import { useMarketList } from '@vegaprotocol/markets';
import type { Filter } from './market-selector'; import type { Filter } from './market-selector';
@@ -31,22 +34,40 @@ describe('useMarketSelectorList', () => {
it('returns all markets active and suspended markets', () => { it('returns all markets active and suspended markets', () => {
const markets = [ const markets = [
createMarketFragment({ id: 'market-0' }), createMarketFragment({
id: 'market-0',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
}),
createMarketFragment({ createMarketFragment({
id: 'market-1', id: 'market-1',
state: MarketState.STATE_SUSPENDED, // @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_SUSPENDED,
}),
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-2', id: 'market-2',
state: MarketState.STATE_CLOSED, // @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_CLOSED,
}),
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-3', id: 'market-3',
state: MarketState.STATE_CLOSED, // @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_CLOSED,
}),
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-4', id: 'market-4',
state: MarketState.STATE_PENDING, // @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_PENDING,
}),
}), }),
]; ];
mockUseMarketList.mockReturnValue({ mockUseMarketList.mockReturnValue({
@@ -56,7 +77,8 @@ describe('useMarketSelectorList', () => {
}); });
const { result } = setup(); const { result } = setup();
const expectedFilteredMarkets = markets.filter((m) => const expectedFilteredMarkets = markets.filter((m) =>
isMarketActive(m.state) // @ts-ignore candles get joined outside this type
isMarketActive(m.data.marketState)
); );
expect(result.current).toEqual({ expect(result.current).toEqual({
data: markets, data: markets,
@@ -70,6 +92,10 @@ describe('useMarketSelectorList', () => {
const markets = [ const markets = [
createMarketFragment({ createMarketFragment({
id: 'market-0', id: 'market-0',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
product: { product: {
@@ -90,6 +116,10 @@ describe('useMarketSelectorList', () => {
// }), // }),
createMarketFragment({ createMarketFragment({
id: 'market-2', id: 'market-2',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
product: { product: {
@@ -135,6 +165,10 @@ describe('useMarketSelectorList', () => {
const markets = [ const markets = [
createMarketFragment({ createMarketFragment({
id: 'market-0', id: 'market-0',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
product: { product: {
@@ -148,6 +182,10 @@ describe('useMarketSelectorList', () => {
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-1', id: 'market-1',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
product: { product: {
@@ -161,6 +199,10 @@ describe('useMarketSelectorList', () => {
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-2', id: 'market-2',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
product: { product: {
@@ -174,6 +216,10 @@ describe('useMarketSelectorList', () => {
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-3', id: 'market-3',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
product: { product: {
@@ -238,6 +284,10 @@ describe('useMarketSelectorList', () => {
const markets = [ const markets = [
createMarketFragment({ createMarketFragment({
id: 'market-0', id: 'market-0',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
code: 'abc', code: 'abc',
@@ -247,6 +297,10 @@ describe('useMarketSelectorList', () => {
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-1', id: 'market-1',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
code: 'def', code: 'def',
@@ -256,6 +310,10 @@ describe('useMarketSelectorList', () => {
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-2', id: 'market-2',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
code: 'defg', code: 'defg',
@@ -265,6 +323,10 @@ describe('useMarketSelectorList', () => {
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-3', id: 'market-3',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
code: 'ggg', code: 'ggg',
@@ -333,11 +395,11 @@ describe('useMarketSelectorList', () => {
const markets = [ const markets = [
createMarketFragment({ createMarketFragment({
id: 'market-0', id: 'market-0',
state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment // @ts-ignore data not on fragment
data: { data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
markPrice: '1', markPrice: '1',
}, }),
// @ts-ignore candles not on fragment // @ts-ignore candles not on fragment
candles: [ candles: [
{ {
@@ -349,9 +411,10 @@ describe('useMarketSelectorList', () => {
id: 'market-1', id: 'market-1',
state: MarketState.STATE_ACTIVE, state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment // @ts-ignore data not on fragment
data: { data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
markPrice: '1', markPrice: '1',
}, }),
// @ts-ignore candles not on fragment // @ts-ignore candles not on fragment
candles: [ candles: [
{ {
@@ -363,9 +426,10 @@ describe('useMarketSelectorList', () => {
id: 'market-2', id: 'market-2',
state: MarketState.STATE_ACTIVE, state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment // @ts-ignore data not on fragment
data: { data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
markPrice: '1', markPrice: '1',
}, }),
// @ts-ignore candles not on fragment // @ts-ignore candles not on fragment
candles: [ candles: [
{ {
@@ -377,9 +441,10 @@ describe('useMarketSelectorList', () => {
id: 'market-3', id: 'market-3',
state: MarketState.STATE_ACTIVE, state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment // @ts-ignore data not on fragment
data: { data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
markPrice: '1', markPrice: '1',
}, }),
// @ts-ignore candles not on fragment // @ts-ignore candles not on fragment
candles: [ candles: [
{ {
@@ -414,6 +479,10 @@ describe('useMarketSelectorList', () => {
const markets = [ const markets = [
createMarketFragment({ createMarketFragment({
id: 'market-0', id: 'market-0',
// @ts-ignore data not on fragment
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
// @ts-ignore actual fragment doesn't contain candles and is joined later // @ts-ignore actual fragment doesn't contain candles and is joined later
candles: [ candles: [
{ {
@@ -426,6 +495,10 @@ describe('useMarketSelectorList', () => {
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-1', id: 'market-1',
// @ts-ignore data not on fragment
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
// @ts-ignore actual fragment doesn't contain candles and is joined later // @ts-ignore actual fragment doesn't contain candles and is joined later
candles: [ candles: [
{ {
@@ -438,6 +511,10 @@ describe('useMarketSelectorList', () => {
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-2', id: 'market-2',
// @ts-ignore data not on fragment
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
// @ts-ignore actual fragment doesn't contain candles and is joined later // @ts-ignore actual fragment doesn't contain candles and is joined later
candles: [ candles: [
{ {
@@ -482,18 +559,30 @@ describe('useMarketSelectorList', () => {
const markets = [ const markets = [
createMarketFragment({ createMarketFragment({
id: 'market-0', id: 'market-0',
// @ts-ignore data not on fragment
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
marketTimestamps: { marketTimestamps: {
open: subDays(new Date(), 3).toISOString(), open: subDays(new Date(), 3).toISOString(),
}, },
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-1', id: 'market-1',
// @ts-ignore data not on fragment
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
marketTimestamps: { marketTimestamps: {
open: subDays(new Date(), 1).toISOString(), open: subDays(new Date(), 1).toISOString(),
}, },
}), }),
createMarketFragment({ createMarketFragment({
id: 'market-2', id: 'market-2',
// @ts-ignore data not on fragment
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
marketTimestamps: { marketTimestamps: {
open: subDays(new Date(), 2).toISOString(), open: subDays(new Date(), 2).toISOString(),
}, },
@@ -22,8 +22,12 @@ export const useMarketSelectorList = ({
const markets = useMemo(() => { const markets = useMemo(() => {
if (!data?.length) return []; if (!data?.length) return [];
const markets = data const markets = data
// only active // show only active markets, using m.data.marketState as this will be
.filter((m) => isMarketActive(m.state)) // data that will get refreshed when calling reload
.filter((m) => {
if (!m.data) return false;
return isMarketActive(m.data.marketState);
})
// only selected product type // only selected product type
.filter((m) => { .filter((m) => {
if ( if (
@@ -63,7 +63,7 @@ describe('Navbar', () => {
const expectedLinks = [ const expectedLinks = [
['/', ''], ['/', ''],
['/markets/all', 'Markets'], ['/markets', 'Markets'],
[`/markets/${marketId}`, 'Trading'], [`/markets/${marketId}`, 'Trading'],
['/portfolio', 'Portfolio'], ['/portfolio', 'Portfolio'],
['/referrals', 'Referrals'], ['/referrals', 'Referrals'],
@@ -80,7 +80,7 @@ describe('Navbar', () => {
}); });
it('Markets page route should not match empty market page', () => { it('Markets page route should not match empty market page', () => {
renderComponent(['/markets/all']); renderComponent(['/markets']);
expect(screen.getByRole('link', { name: 'Markets' })).toHaveClass('active'); expect(screen.getByRole('link', { name: 'Markets' })).toHaveClass('active');
expect(screen.getByRole('link', { name: 'Trading' })).not.toHaveClass( expect(screen.getByRole('link', { name: 'Trading' })).not.toHaveClass(
'active' 'active'
@@ -96,7 +96,7 @@ describe('Navbar', () => {
const menu = within(menuEl); const menu = within(menuEl);
const expectedLinks = [ const expectedLinks = [
['/markets/all', 'Markets'], ['/markets', 'Markets'],
[`/markets/${marketId}`, 'Trading'], [`/markets/${marketId}`, 'Trading'],
['/portfolio', 'Portfolio'], ['/portfolio', 'Portfolio'],
['/referrals', 'Referrals'], ['/referrals', 'Referrals'],
@@ -264,6 +264,7 @@ const NavbarLink = ({
<N.Link asChild={true}> <N.Link asChild={true}>
<NavLink <NavLink
to={to} to={to}
end={true}
className={classNames( className={classNames(
'block lg:flex lg:h-full flex-col justify-center', 'block lg:flex lg:h-full flex-col justify-center',
'px-6 py-2 lg:p-0 text-lg lg:text-sm', 'px-6 py-2 lg:p-0 text-lg lg:text-sm',
@@ -3,14 +3,25 @@ import { Filter, OrderListManager } from '@vegaprotocol/orders';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { import { useNavigateWithMeta } from '../../lib/hooks/use-market-click-handler';
useMarketClickHandler,
useMarketLiquidityClickHandler,
} from '../../lib/hooks/use-market-click-handler';
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import type { DataGridStore } from '../../stores/datagrid-store-slice'; import type { DataGridStore } from '../../stores/datagrid-store-slice';
import { OrderStatus } from '@vegaprotocol/types'; import { OrderStatus } from '@vegaprotocol/types';
import { Links } from '../../lib/links';
const resolveNoRowsMessage = (filter?: Filter) => {
switch (filter) {
case Filter.Open:
return t('No open orders');
case Filter.Closed:
return t('No closed orders');
case Filter.Rejected:
return t('No rejected orders');
default:
return t('No orders');
}
};
export const FilterStatusValue = { export const FilterStatusValue = {
[Filter.Open]: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED], [Filter.Open]: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED],
@@ -32,8 +43,7 @@ const AUTO_SIZE_COLUMNS = ['instrument-code'];
export const OrdersContainer = ({ filter }: OrderContainerProps) => { export const OrdersContainer = ({ filter }: OrderContainerProps) => {
const { pubKey, isReadOnly } = useVegaWallet(); const { pubKey, isReadOnly } = useVegaWallet();
const onMarketClick = useMarketClickHandler(true); const navigate = useNavigateWithMeta();
const onOrderTypeClick = useMarketLiquidityClickHandler();
const { gridState, updateGridState } = useOrderListGridState(filter); const { gridState, updateGridState } = useOrderListGridState(filter);
const gridStoreCallbacks = useDataGridEvents( const gridStoreCallbacks = useDataGridEvents(
gridState, gridState,
@@ -46,15 +56,21 @@ export const OrdersContainer = ({ filter }: OrderContainerProps) => {
if (!pubKey) { if (!pubKey) {
return <Splash>{t('Please connect Vega wallet')}</Splash>; return <Splash>{t('Please connect Vega wallet')}</Splash>;
} }
const noRowsMessage = resolveNoRowsMessage(filter);
return ( return (
<OrderListManager <OrderListManager
partyId={pubKey} partyId={pubKey}
filter={filter} filter={filter}
onMarketClick={onMarketClick} onMarketClick={(marketId, metaKey) => {
onOrderTypeClick={onOrderTypeClick} navigate(Links.MARKET(marketId), metaKey);
}}
onOrderTypeClick={(marketId, metaKey) =>
navigate(Links.LIQUIDITY(marketId), metaKey)
}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
gridProps={gridStoreCallbacks} gridProps={gridStoreCallbacks}
noRowsMessage={noRowsMessage}
/> />
); );
}; };
@@ -38,79 +38,68 @@ const walletContext = {
} as VegaWalletContextShape; } as VegaWalletContextShape;
describe('Sidebar', () => { describe('Sidebar', () => {
it.each(['/markets/all', '/portfolio'])( beforeEach(() => {
'does not render ticket and info', useSidebar.setState({ views: {} });
(path) => { });
render(
<VegaWalletContext.Provider value={walletContext}>
<MemoryRouter initialEntries={[path]}>
<Sidebar />
</MemoryRouter>
</VegaWalletContext.Provider>
);
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument(); it('renders options prop', () => {
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Withdraw)).toBeInTheDocument();
expect(screen.getByTestId('node-health')).toBeInTheDocument();
// no order or info sidebars
expect(screen.queryByTestId(ViewType.Order)).not.toBeInTheDocument();
expect(screen.queryByTestId(ViewType.Info)).not.toBeInTheDocument();
}
);
it('renders ticket and info on market pages', () => {
render( render(
<VegaWalletContext.Provider value={walletContext}> <VegaWalletContext.Provider value={walletContext}>
<MemoryRouter initialEntries={['/markets/ABC']}> <MemoryRouter>
<Sidebar /> <Sidebar options={<div data-testid="options" />} />
</MemoryRouter> </MemoryRouter>
</VegaWalletContext.Provider> </VegaWalletContext.Provider>
); );
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Withdraw)).toBeInTheDocument();
expect(screen.getByTestId('node-health')).toBeInTheDocument(); expect(screen.getByTestId('node-health')).toBeInTheDocument();
expect(screen.getByTestId('options')).toBeInTheDocument();
// order and info sidebars are shown
expect(screen.getByTestId(ViewType.Order)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Info)).toBeInTheDocument();
}); });
it('renders selected state', async () => { it('renders selected state', async () => {
const routeId = '/markets/ABC';
render( render(
<VegaWalletContext.Provider value={walletContext}> <VegaWalletContext.Provider value={walletContext}>
<MemoryRouter initialEntries={['/markets/ABC']}> <MemoryRouter initialEntries={[routeId]}>
<Sidebar /> <Sidebar
options={
<SidebarButton
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip="Deposit"
routeId={'/markets/:marketId'}
/>
}
/>
</MemoryRouter> </MemoryRouter>
</VegaWalletContext.Provider> </VegaWalletContext.Provider>
); );
const settingsButton = screen.getByTestId(ViewType.Settings); const settingsButton = screen.getByTestId(ViewType.Settings);
const orderButton = screen.getByTestId(ViewType.Order); const depositButton = screen.getByTestId(ViewType.Deposit);
// select settings first // select settings first
await userEvent.click(settingsButton); await userEvent.click(settingsButton);
expect(settingsButton).toHaveClass('bg-vega-yellow text-black'); expect(settingsButton).toHaveClass('bg-vega-yellow text-black');
// switch to order // switch to deposit
await userEvent.click(orderButton); await userEvent.click(depositButton);
expect(settingsButton).not.toHaveClass('bg-vega-yellow text-black'); expect(settingsButton).not.toHaveClass('bg-vega-yellow text-black');
expect(orderButton).toHaveClass('bg-vega-yellow text-black'); expect(depositButton).toHaveClass('bg-vega-yellow text-black');
// close order // close order
await userEvent.click(orderButton); await userEvent.click(depositButton);
expect(orderButton).not.toHaveClass('bg-vega-yellow text-black'); expect(depositButton).not.toHaveClass('bg-vega-yellow text-black');
}); });
}); });
describe('SidebarContent', () => { describe('SidebarContent', () => {
beforeEach(() => {
useSidebar.setState({ views: {} });
});
it('renders the correct content', () => { it('renders the correct content', () => {
const { container } = render( const { container } = render(
<VegaWalletContext.Provider value={walletContext}> <VegaWalletContext.Provider value={walletContext}>
+4 -98
View File
@@ -1,7 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Route, Routes, useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { create } from 'zustand'; import { create } from 'zustand';
import { TransferContainer } from '@vegaprotocol/accounts'; import { TransferContainer } from '@vegaprotocol/accounts';
import { DealTicketContainer } from '@vegaprotocol/deal-ticket'; import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
@@ -50,108 +50,14 @@ type SidebarView =
type: ViewType.Settings; type: ViewType.Settings;
}; };
const MarketSidebarButtons = () => { export const Sidebar = ({ options }: { options?: ReactNode }) => {
const currentRouteId = useGetCurrentRouteId();
return (
<>
<SidebarButton
view={ViewType.Order}
icon={VegaIconNames.TICKET}
tooltip={t('Order')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
routeId={currentRouteId}
/>
</>
);
};
const AssetSidebarButtons = () => {
const currentRouteId = useGetCurrentRouteId();
return (
<>
<SidebarButton
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
</>
);
};
export const Sidebar = () => {
const currentRouteId = useGetCurrentRouteId(); const currentRouteId = useGetCurrentRouteId();
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1'; const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen); const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
const { pubKeys } = useVegaWallet(); const { pubKeys } = useVegaWallet();
return ( return (
<div className="flex h-full p-1 lg:flex-col gap-2" data-testid="sidebar"> <div className="flex h-full p-1 lg:flex-col gap-2" data-testid="sidebar">
<nav className={navClasses}> {options && <nav className={navClasses}>{options}</nav>}
<Routes>
<Route path="markets/all" element={<AssetSidebarButtons />} />
<Route path="portfolio">
<Route
// Show deposit/withdraw/transfer sidebar options only on portflio dashboard (index)
index={true}
element={<AssetSidebarButtons />}
/>
</Route>
<Route
path="markets/:marketId"
element={
<>
<AssetSidebarButtons />
<SidebarDivider />
<MarketSidebarButtons />
</>
}
/>
<Route
path="liquidity/:marketId"
element={
<>
<AssetSidebarButtons />
<SidebarDivider />
<MarketSidebarButtons />
</>
}
/>
<Route
path="markets/all/closed/:marketId"
element={
<>
<AssetSidebarButtons />
<SidebarDivider />
<SidebarButton
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
routeId={currentRouteId}
/>
</>
}
/>
</Routes>
</nav>
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}> <nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}>
<SidebarButton <SidebarButton
view={ViewType.ViewAs} view={ViewType.ViewAs}
@@ -234,7 +140,7 @@ export const SidebarButton = ({
); );
}; };
const SidebarDivider = () => { export const SidebarDivider = () => {
return ( return (
<div <div
className="w-px h-4 bg-vega-clight-600 dark:bg-vega-cdark-600 lg:w-4 lg:h-px" className="w-px h-4 bg-vega-clight-600 dark:bg-vega-cdark-600 lg:w-4 lg:h-px"
@@ -6,7 +6,7 @@ import { TelemetryApproval } from './telemetry-approval';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useOnboardingStore } from '../welcome-dialog/use-get-onboarding-step'; import { useOnboardingStore } from '../welcome-dialog/use-get-onboarding-step';
const TELEMETRY_APPROVAL_TOAST_ID = 'telemetry_tost_id'; const TELEMETRY_APPROVAL_TOAST_ID = 'telemetry_toast_id';
export const Telemetry = () => { export const Telemetry = () => {
const onboardingDissmissed = useOnboardingStore((store) => store.dismissed); const onboardingDissmissed = useOnboardingStore((store) => store.dismissed);
@@ -1,5 +1,4 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useCallback } from 'react';
import { Links } from '../../lib/links'; import { Links } from '../../lib/links';
export const useMarketClickHandler = (replace = false) => { export const useMarketClickHandler = (replace = false) => {
@@ -15,17 +14,10 @@ export const useMarketClickHandler = (replace = false) => {
}; };
}; };
export const useMarketLiquidityClickHandler = () => { export const useNavigateWithMeta = (replace = false) => {
return useCallback((selectedId: string, metaKey?: boolean) => {
window.open(`/#/liquidity/${selectedId}`, metaKey ? '_blank' : '_self');
}, []);
};
export const useClosedMarketClickHandler = (replace = false) => {
const navigate = useNavigate(); const navigate = useNavigate();
return (selectedId: string, metaKey?: boolean) => { return (link: string, metaKey?: boolean) => {
const link = Links.CLOSED_MARKETS(selectedId);
if (metaKey) { if (metaKey) {
window.open(`/#${link}`, '_blank'); window.open(`/#${link}`, '_blank');
} else { } else {
+1 -4
View File
@@ -4,8 +4,7 @@ import trimEnd from 'lodash/trimEnd';
// href creation // href creation
export const Routes = { export const Routes = {
HOME: '/', HOME: '/',
MARKETS: '/markets/all', MARKETS: '/markets',
CLOSED_MARKETS: '/markets/all/closed/:marketId',
MARKET: '/markets/:marketId', MARKET: '/markets/:marketId',
LIQUIDITY: '/liquidity/:marketId', LIQUIDITY: '/liquidity/:marketId',
PORTFOLIO: '/portfolio', PORTFOLIO: '/portfolio',
@@ -29,8 +28,6 @@ export const Links: ConsoleLinks = {
MARKET: (marketId: string) => MARKET: (marketId: string) =>
trimEnd(Routes.MARKET.replace(':marketId', marketId)), trimEnd(Routes.MARKET.replace(':marketId', marketId)),
MARKETS: () => Routes.MARKETS, MARKETS: () => Routes.MARKETS,
CLOSED_MARKETS: (marketId: string) =>
trimEnd(Routes.CLOSED_MARKETS.replace(':marketId', marketId)),
PORTFOLIO: () => Routes.PORTFOLIO, PORTFOLIO: () => Routes.PORTFOLIO,
LIQUIDITY: (marketId: string) => LIQUIDITY: (marketId: string) =>
trimEnd(Routes.LIQUIDITY.replace(':marketId', marketId)), trimEnd(Routes.LIQUIDITY.replace(':marketId', marketId)),
+12 -71
View File
@@ -1,21 +1,7 @@
import { useEffect, useMemo, useState } from 'react'; import { useMemo } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import {
useEagerConnect as useVegaEagerConnect,
useVegaWallet,
} from '@vegaprotocol/wallet';
import {
useVegaTransactionManager,
useVegaTransactionUpdater,
} from '@vegaprotocol/web3';
import {
useEagerConnect as useEthereumEagerConnect,
useEthTransactionManager,
useEthTransactionUpdater,
useEthWithdrawApprovalsManager,
} from '@vegaprotocol/web3';
import { import {
envTriggerMapping, envTriggerMapping,
Networks, Networks,
@@ -25,21 +11,11 @@ import {
useNodeSwitcherStore, useNodeSwitcherStore,
} from '@vegaprotocol/environment'; } from '@vegaprotocol/environment';
import './styles.css'; import './styles.css';
import { useGlobalStore, usePageTitleStore } from '../stores'; import { usePageTitleStore } from '../stores';
import DialogsContainer from './dialogs-container'; import DialogsContainer from './dialogs-container';
import ToastsManager from './toasts-manager'; import ToastsManager from './toasts-manager';
import { import { HashRouter, useLocation, Route, Routes } from 'react-router-dom';
HashRouter,
useLocation,
Route,
Routes,
useSearchParams,
} from 'react-router-dom';
import { Connectors } from '../lib/vega-connectors';
import { Bootstrapper } from '../components/bootstrapper'; import { Bootstrapper } from '../components/bootstrapper';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import { useTelemetryApproval } from '../lib/hooks/use-telemetry-approval';
import { AnnouncementBanner } from '../components/banner'; import { AnnouncementBanner } from '../components/banner';
import { Navbar } from '../components/navbar'; import { Navbar } from '../components/navbar';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -53,6 +29,9 @@ import { NavHeader } from '../components/navbar/nav-header';
import { Telemetry } from '../components/telemetry'; import { Telemetry } from '../components/telemetry';
import { Routes as AppRoutes } from '../lib/links'; import { Routes as AppRoutes } from '../lib/links';
import { SSRLoader } from './ssr-loader'; import { SSRLoader } from './ssr-loader';
import { PartyActiveOrdersHandler } from './party-active-orders-handler';
import { MaybeConnectEagerly } from './maybe-connect-eagerly';
import { TransactionHandlers } from './transaction-handlers';
const DEFAULT_TITLE = t('Welcome to Vega trading!'); const DEFAULT_TITLE = t('Welcome to Vega trading!');
@@ -77,15 +56,6 @@ const Title = () => {
); );
}; };
const InitializeHandlers = () => {
useVegaTransactionManager();
useVegaTransactionUpdater();
useEthTransactionManager();
useEthTransactionUpdater();
useEthWithdrawApprovalsManager();
return null;
};
function AppBody({ Component }: AppProps) { function AppBody({ Component }: AppProps) {
const location = useLocation(); const location = useLocation();
const { VEGA_ENV } = useEnvironment(); const { VEGA_ENV } = useEnvironment();
@@ -125,9 +95,9 @@ function AppBody({ Component }: AppProps) {
</div> </div>
<DialogsContainer /> <DialogsContainer />
<ToastsManager /> <ToastsManager />
<InitializeHandlers /> <TransactionHandlers />
<MaybeConnectEagerly /> <MaybeConnectEagerly />
<PartyData /> <PartyActiveOrdersHandler />
<Telemetry /> <Telemetry />
</div> </div>
); );
@@ -140,10 +110,14 @@ function VegaTradingApp(props: AppProps) {
store.setDialogOpen, store.setDialogOpen,
]); ]);
// Start validation of env vars and determine VEGA_URL
useInitializeEnv(); useInitializeEnv();
// Prevent HashRouter from being server side rendered as it // Prevent HashRouter from being server side rendered as it
// relies on presence of document object // relies on presence of document object
//
// This is the last point at which we get pregenerated
// HTML so render a ssr friendly loader component
if (status === 'default') { if (status === 'default') {
return <SSRLoader />; return <SSRLoader />;
} }
@@ -159,36 +133,3 @@ function VegaTradingApp(props: AppProps) {
} }
export default VegaTradingApp; export default VegaTradingApp;
const PartyData = () => {
const { pubKey } = useVegaWallet();
const variables = { partyId: pubKey || '' };
const skip = !pubKey;
useDataProvider({
dataProvider: activeOrdersProvider,
variables,
skip,
});
return null;
};
const MaybeConnectEagerly = () => {
const { VEGA_ENV, SENTRY_DSN } = useEnvironment();
const update = useGlobalStore((store) => store.update);
const eagerConnecting = useVegaEagerConnect(Connectors);
const [isTelemetryApproved] = useTelemetryApproval();
useEthereumEagerConnect(
isTelemetryApproved ? { dsn: SENTRY_DSN, env: VEGA_ENV } : {}
);
const { pubKey, connect } = useVegaWallet();
const [searchParams] = useSearchParams();
const [query] = useState(searchParams.get('address'));
if (query && !pubKey) {
connect(Connectors['view']);
}
useEffect(() => {
update({ eagerConnecting });
}, [update, eagerConnecting]);
return null;
};
+73 -67
View File
@@ -1,5 +1,5 @@
import type { RouteObject } from 'react-router-dom'; import type { RouteObject } from 'react-router-dom';
import { Navigate, Outlet, useRoutes } from 'react-router-dom'; import { Navigate, useRoutes } from 'react-router-dom';
import { lazy, Suspense } from 'react'; import { lazy, Suspense } from 'react';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit'; import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
@@ -13,7 +13,7 @@ import { Assets } from '../client-pages/assets';
import { Deposit } from '../client-pages/deposit'; import { Deposit } from '../client-pages/deposit';
import { Withdraw } from '../client-pages/withdraw'; import { Withdraw } from '../client-pages/withdraw';
import { Transfer } from '../client-pages/transfer'; import { Transfer } from '../client-pages/transfer';
import { Routes } from '../lib/links'; import { Routes as AppRoutes } from '../lib/links';
import { LayoutWithSky } from '../client-pages/referrals/layout'; import { LayoutWithSky } from '../client-pages/referrals/layout';
import { Referrals } from '../client-pages/referrals/referrals'; import { Referrals } from '../client-pages/referrals/referrals';
import { ReferralStatistics } from '../client-pages/referrals/referral-statistics'; import { ReferralStatistics } from '../client-pages/referrals/referral-statistics';
@@ -22,11 +22,15 @@ import { CreateCodeContainer } from '../client-pages/referrals/create-code-form'
import { NotFound as ReferralNotFound } from '../client-pages/referrals/error-boundary'; import { NotFound as ReferralNotFound } from '../client-pages/referrals/error-boundary';
import { compact } from 'lodash'; import { compact } from 'lodash';
import { FLAGS } from '@vegaprotocol/environment'; import { FLAGS } from '@vegaprotocol/environment';
import { LiquidityHeader } from '../components/liquidity-header';
import { MarketHeader } from '../components/market-header';
import { PortfolioSidebar } from '../client-pages/portfolio/portfolio-sidebar';
import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar';
import { MarketsSidebar } from '../client-pages/markets/markets-sidebar';
// These must remain dynamically imported as pennant cannot be compiled by nextjs due to ESM // These must remain dynamically imported as pennant cannot be compiled by nextjs due to ESM
// Using dynamic imports is a workaround for this until pennant is published as ESM // Using dynamic imports is a workaround for this until pennant is published as ESM
const MarketPage = lazy(() => import('../client-pages/market')); const MarketPage = lazy(() => import('../client-pages/market'));
const ClosedMarketPage = lazy(() => import('../client-pages/closed-market'));
const Portfolio = lazy(() => import('../client-pages/portfolio')); const Portfolio = lazy(() => import('../client-pages/portfolio'));
const NotFound = () => ( const NotFound = () => (
@@ -36,18 +40,21 @@ const NotFound = () => (
); );
export const routerConfig: RouteObject[] = compact([ export const routerConfig: RouteObject[] = compact([
// Pages that don't use the LayoutWithSidebar must come first {
// to ensure they are matched before the catch all route '/*' index: true,
element: <Home />,
id: AppRoutes.HOME,
},
{ {
path: 'disclaimer', path: 'disclaimer',
element: <LayoutCentered />, element: <LayoutCentered />,
id: Routes.DISCLAIMER, id: AppRoutes.DISCLAIMER,
children: [{ index: true, element: <Disclaimer /> }], children: [{ index: true, element: <Disclaimer /> }],
}, },
// Referrals routing (the pages should be available if the feature flag is on) // Referrals routing (the pages should be available if the feature flag is on)
FLAGS.REFERRALS FLAGS.REFERRALS
? { ? {
path: Routes.REFERRALS, path: AppRoutes.REFERRALS,
element: <LayoutWithSky />, element: <LayoutWithSky />,
children: [ children: [
{ {
@@ -58,11 +65,11 @@ export const routerConfig: RouteObject[] = compact([
element: <ReferralStatistics />, element: <ReferralStatistics />,
}, },
{ {
path: Routes.REFERRALS_CREATE_CODE, path: AppRoutes.REFERRALS_CREATE_CODE,
element: <CreateCodeContainer />, element: <CreateCodeContainer />,
}, },
{ {
path: Routes.REFERRALS_APPLY_CODE, path: AppRoutes.REFERRALS_APPLY_CODE,
element: <ApplyCodeForm />, element: <ApplyCodeForm />,
}, },
], ],
@@ -74,75 +81,74 @@ export const routerConfig: RouteObject[] = compact([
], ],
} }
: undefined, : undefined,
// All other pages will use the sidebar
{ {
path: '/*', path: 'markets/*',
element: <LayoutWithSidebar />, element: (
<LayoutWithSidebar
header={<MarketHeader />}
sidebar={<MarketsSidebar />}
/>
),
children: [ children: [
{ {
index: true, index: true,
element: <Home />, element: <MarketsPage />,
id: Routes.HOME, id: AppRoutes.MARKETS,
}, },
{ {
path: 'markets', path: 'all',
element: <Outlet />, element: <Navigate to="/markets" />,
children: [
{
index: true,
element: <Navigate to="all" />,
},
{
path: 'all',
element: <MarketsPage />,
id: Routes.MARKETS,
},
{
path: ':marketId',
element: <MarketPage />,
id: Routes.MARKET,
},
{
path: 'all/closed/:marketId',
element: <ClosedMarketPage />,
id: Routes.CLOSED_MARKETS,
},
],
}, },
{ {
path: 'portfolio', path: ':marketId',
element: <Outlet />, element: <MarketPage />,
children: [ id: AppRoutes.MARKET,
{
index: true,
element: <Portfolio />,
id: Routes.PORTFOLIO,
},
{
path: 'assets',
element: <Assets />,
id: Routes.ASSETS,
children: [
{ index: true, element: <Navigate to="deposit" /> },
{ path: 'deposit', element: <Deposit />, id: Routes.DEPOSIT },
{ path: 'withdraw', element: <Withdraw />, id: Routes.WITHDRAW },
{ path: 'transfer', element: <Transfer />, id: Routes.TRANSFER },
],
},
],
},
{
path: 'liquidity/:marketId',
element: <Liquidity />,
id: Routes.LIQUIDITY,
},
// NotFound page is here so its caught within parent '/*' route
{
path: '*',
element: <NotFound />,
}, },
], ],
}, },
{
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 />,
},
]); ]);
export const ClientRouter = () => { export const ClientRouter = () => {
@@ -0,0 +1,32 @@
import { useSearchParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { useEnvironment } from '@vegaprotocol/environment';
import { useEagerConnect as useEthereumEagerConnect } from '@vegaprotocol/web3';
import {
useEagerConnect as useVegaEagerConnect,
useVegaWallet,
} from '@vegaprotocol/wallet';
import { useGlobalStore } from '../stores';
import { Connectors } 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 [isTelemetryApproved] = useTelemetryApproval();
useEthereumEagerConnect(
isTelemetryApproved ? { dsn: SENTRY_DSN, env: VEGA_ENV } : {}
);
const { pubKey, connect } = useVegaWallet();
const [searchParams] = useSearchParams();
const [query] = useState(searchParams.get('address'));
if (query && !pubKey) {
connect(Connectors['view']);
}
useEffect(() => {
update({ eagerConnecting });
}, [update, eagerConnecting]);
return null;
};
@@ -0,0 +1,15 @@
import { useDataProvider } from '@vegaprotocol/data-provider';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import { useVegaWallet } from '@vegaprotocol/wallet';
export const PartyActiveOrdersHandler = () => {
const { pubKey } = useVegaWallet();
const variables = { partyId: pubKey || '' };
const skip = !pubKey;
useDataProvider({
dataProvider: activeOrdersProvider,
variables,
skip,
});
return null;
};
+5 -6
View File
@@ -1,8 +1,8 @@
export const SSRLoader = () => { import { pseudoRandom } from '@vegaprotocol/ui-toolkit';
const randomDelay = () => {
return parseFloat((Math.random() * (4 - 1) + 1).toFixed(2));
};
const generate = pseudoRandom(1);
export const SSRLoader = () => {
return ( return (
<div <div
style={{ style={{
@@ -38,10 +38,9 @@ export const SSRLoader = () => {
width: 10, width: 10,
height: 10, height: 10,
animation: 'flickering 0.4s linear alternate infinite', animation: 'flickering 0.4s linear alternate infinite',
animationDelay: `-${randomDelay()}s`, animationDelay: `-${generate()}s`,
animationDirection: i % 2 === 0 ? 'reverse' : 'alternate', animationDirection: i % 2 === 0 ? 'reverse' : 'alternate',
background: 'black', background: 'black',
opacity: Math.random() > 0.5 ? 1 : 0,
}} }}
/> />
); );
@@ -0,0 +1,20 @@
import {
useVegaTransactionManager,
useVegaTransactionUpdater,
} from '@vegaprotocol/web3';
import {
useEthTransactionManager,
useEthTransactionUpdater,
useEthWithdrawApprovalsManager,
} from '@vegaprotocol/web3';
import { useLedgerDownloadManager } from '@vegaprotocol/ledger';
export const TransactionHandlers = () => {
useVegaTransactionManager();
useVegaTransactionUpdater();
useEthTransactionManager();
useEthTransactionUpdater();
useEthWithdrawApprovalsManager();
useLedgerDownloadManager();
return null;
};
@@ -45,6 +45,28 @@ const secondRow = {
} as AccountFields; } as AccountFields;
const multiRowData = [singleRow, secondRow]; const multiRowData = [singleRow, secondRow];
const zeroBalanceRow = {
__typename: 'AccountBalance',
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
balance: '0',
market: {
__typename: 'Market',
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
},
asset: {
__typename: 'Asset',
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
decimals: 5,
},
available: '0',
used: '0',
total: '0',
} as AccountFields;
const zeroBalanceRowData = [zeroBalanceRow];
const onClickDepositMock = jest.fn();
describe('AccountsTable', () => { describe('AccountsTable', () => {
it('should render correct columns', async () => { it('should render correct columns', async () => {
// 7001-COLL-001 // 7001-COLL-001
@@ -71,6 +93,27 @@ describe('AccountsTable', () => {
).toEqual(expectedHeaders); ).toEqual(expectedHeaders);
}); });
it('should render deposit button', async () => {
render(
<AccountTable
rowData={zeroBalanceRowData}
onClickAsset={() => null}
isReadOnly={false}
onClickDeposit={onClickDepositMock}
pinnedAsset={{
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
name: 'tBTC',
}}
/>
);
const depositButton = screen.getByTestId('deposit');
expect(depositButton).toBeVisible();
await userEvent.click(depositButton);
expect(onClickDepositMock).toHaveBeenCalled();
});
it('should apply correct formatting', async () => { it('should apply correct formatting', async () => {
const { container } = render( const { container } = render(
<AccountTable <AccountTable
@@ -88,6 +88,9 @@ describe('TransferForm', () => {
}); });
it('validates a manually entered address', async () => { it('validates a manually entered address', async () => {
// 1003-TRAN-012
// 1003-TRAN-013
// 1003-TRAN-004
render(<TransferForm {...props} />); render(<TransferForm {...props} />);
submit(); submit();
expect(await screen.findAllByText('Required')).toHaveLength(3); expect(await screen.findAllByText('Required')).toHaveLength(3);
@@ -116,6 +119,11 @@ describe('TransferForm', () => {
}); });
it('validates fields and submits', async () => { it('validates fields and submits', async () => {
// 1003-TRAN-002
// 1003-TRAN-003
// 1002-WITH-010
// 1003-TRAN-011
// 1003-TRAN-014
render(<TransferForm {...props} />); render(<TransferForm {...props} />);
// check current pubkey not shown // check current pubkey not shown
+10 -1
View File
@@ -57,7 +57,16 @@ export const rows: Rows = [
key: AssetDetail.ID, key: AssetDetail.ID,
label: t('ID'), label: t('ID'),
tooltip: '', tooltip: '',
value: (asset) => truncateMiddle(asset.id), value: (asset) => (
<>
{truncateMiddle(asset.id)}{' '}
<CopyWithTooltip text={asset.id}>
<button title={t('Copy id to clipboard')}>
<VegaIcon size={14} name={VegaIconNames.COPY} />
</button>
</CopyWithTooltip>
</>
),
}, },
{ {
key: AssetDetail.TYPE, key: AssetDetail.TYPE,
+25 -4
View File
@@ -4,9 +4,10 @@ import {
} from '@vegaprotocol/data-provider'; } from '@vegaprotocol/data-provider';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import { AssetsDocument } from './__generated__/Assets'; import { AssetsDocument } from './__generated__/Assets';
import * as Schema from '@vegaprotocol/types'; import { AssetStatus } from '@vegaprotocol/types';
import type { AssetsQuery } from './__generated__/Assets'; import type { AssetsQuery } from './__generated__/Assets';
import type { Asset } from './asset-data-provider'; import type { Asset } from './asset-data-provider';
import { DENY_LIST } from './constants';
export interface BuiltinAssetSource { export interface BuiltinAssetSource {
__typename: 'BuiltinAsset'; __typename: 'BuiltinAsset';
@@ -48,6 +49,7 @@ export const assetsMapProvider = makeDerivedDataProvider<
} }
); );
/** Returns a record of assets by id */
export const useAssetsMapProvider = () => export const useAssetsMapProvider = () =>
useDataProvider({ useDataProvider({
dataProvider: assetsMapProvider, dataProvider: assetsMapProvider,
@@ -58,13 +60,32 @@ export const enabledAssetsProvider = makeDerivedDataProvider<
ReturnType<typeof getData>, ReturnType<typeof getData>,
never never
>([assetsProvider], ([assets]) => >([assetsProvider], ([assets]) =>
(assets as ReturnType<typeof getData>).filter( (assets as ReturnType<typeof getData>)
(a) => a.status === Schema.AssetStatus.STATUS_ENABLED .filter((a) => a.status === AssetStatus.STATUS_ENABLED)
) .filter((a) => {
const env = process.env['NX_VEGA_ENV'];
if (!env || !DENY_LIST[env]) return true;
if (DENY_LIST[env].includes(a.id)) {
return false;
}
return true;
})
); );
/** Returns all assets */
export const useAssetsDataProvider = () => export const useAssetsDataProvider = () =>
useDataProvider({ useDataProvider({
dataProvider: assetsProvider, dataProvider: assetsProvider,
variables: undefined, variables: undefined,
}); });
/** Returns assets that are enabled and are not on the deny list */
export const useEnabledAssets = () => {
return useDataProvider({
dataProvider: enabledAssetsProvider,
variables: undefined,
});
};
+63
View File
@@ -1,4 +1,67 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
export const WITHDRAW_THRESHOLD_TOOLTIP_TEXT = t( export const WITHDRAW_THRESHOLD_TOOLTIP_TEXT = t(
"The maximum you can withdraw instantly. There's no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them" "The maximum you can withdraw instantly. There's no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them"
); );
// List of defunct and no longer used assets that were created for various testnets
export const DENY_LIST: Record<string, string[]> = {
TESTNET: [
'07f8ce4e3df6c649f3fd794112f545ede9fbd9cfa76a9e380a7502a8d525e4db',
'0b87ac58d4af7fc11c8b417153fcb62631cfd9643835ef28db3f5a1caef0b379',
'169fd651dd1303fc6f3e1dee8349439140cf467a190bc9a04bff3e617509cfd4',
'16ae5dbb1fd7aa2ddef725703bfe66b3647a4da7b844bfdd04e985756f53d9d6',
'177e8f6c25a955bd18475084b99b2b1d37f28f3dec393fab7755a7e69c3d8c3b',
'20ffe5b1f70364a6b976007d96d306345bc7b880256ddffbbea2c590096ef375',
'2792b6ead78a812bd0169b2adbe9c2ea5b8c21510e40b93bf8d2cba6a0838e90',
'27c0229ab9edc6a3ba6a3e9716c1b324d6515a3168004256879e2de7c240f453',
'285923fed8c66ffb416b163e8ec72d3a87b9b8e2570e7ee7fe97d7092a918bc8',
'2af02165ef5ea1d1284b4251117eaae45faa961ec918e3f69b66903a18822387',
'2bf406160d06280d92412605eb397d08142736f4e17967e404dc892da278e9da',
'373d90d97bd3f97f0c13cf449926cfd7f1ed5f3746c3091e8dd894d60bf5eac8',
'3c7d6bd83dac3174923ba65f3af01f42858e267ca5b722fe2b0b3fec14e98ec2',
'434b502cabb7a9a36d3036a599d589537da293d8bbab224692629351a7926249',
'43bb50d6ba0a871088014cb204079ace4811970ce3d52b15896f6e7a88f900b7',
'46e9d23142762db57751b1b507e15747fe9478de504cbc4a0bffd828c39e3776',
'4b6957c6a184b002db80c7a4dd811d0e70866be4a4c8fffa4894e78aa132b407',
'4cbe1ad4cf232fb2232176e3f440f6e6fdc3ba7b1d95a99cb69116b9e379eae8',
'4e4e80abff30cab933b8c4ac6befc618372eb76b2cbddc337eff0b4a3a4d25b8',
'51eed0c17e3287996ba6006ce56de2591550ebfad46631f3a0129886967420e1',
'522e09779d984ae77b60036f4ecd80f0491f5f0c8cc386a0712b78a020ab62e6',
'5c950d4cfe1aaf9174c4358ee5df03f7e6bc556eac06a012cf6c07db154e20c4',
'5d8eb4e4e385b85c5c11f47111f315d5d201e04f4ef5e294763bf51036974b91',
'5dd5c226e944b4c3df3e8dbbbc473e72fb134f4c5b82a1968cc3cf7c1b40fe9e',
'638b4dbe4f3ce5b81546bef29108e8374a7293012c6189ca00f287f8c62a98a5',
'644c3a6e258382542a108c4dca16fae93657b6ff40238b2bb55854a125316052',
'71ea74ef1aa7582bcdd4ef843b3e79584ded4d16e692f3946cac4c9d61aed4f2',
'72f051ea66686ae004086f1ad086866f720f25896319abf3427cae101a58d985',
'827bbacde0dd5bfec857933dbbf642147f2afe6a9db0a543839b1b5239ca6786',
'83f4970aa1aeab85d01d8a976cbfd0fb3afedf391e18197e8c94c4021b3dffa1',
'84a20b554854c9af10ef84d308795dfae455834d1a207798292d66e19fd72c92',
'84fff099818dc4f5319477f0812b4341565cfb32ccf735beede734e386b8108f',
'85771250f0aa84727a1c01ba8926e59217ca13a7706ba03cf2ad95abbbc2e3bc',
'8a246b15bf185065d3f41a710f3eea0fbbe8754a8464501cf4a6d253d69c6423',
'8e5553ee4e95db51523640d1d03b00c8ab31dd69d787e169b0d958ef52e19a0a',
'92747340d922e9cda5bde17c46af8b409e47d6342f3b9c8fb058a34c6206d373',
'9b43c6791aebed5f7b38d91907539173aa278de88294b6422f3dd8282cd2c6e2',
'9e0bb9bd7ea2ec51efcdc98b432b6f0b055b2ed7973cfac9b44899d6e6c5deab',
'a0cf0835cb5af7bd03c56d12d846eed81c182eab36ea61c074961c115da9c8c4',
'a26dc4f6dea91e80d42e4736da7fe3c42f6275df9c4dca48be3ef96850192242',
'a4c945aea50ef321eabf8a3bcf0b8bb74288452c4638876ee4df5fb59be4a0ec',
'ae28c1043e79bdb95cae0e9919d4436157d14b183e24ab4572793812f3d89222',
'b0cf8be4e4d1d2ac5156c2298aa42edc7a74d25b7838d06e2279e94018a47f3f',
'b6d11705cb456768ff8271074dd8adcd78bd042a27fc205e9b52f9f945f42ea1',
'be478570584211521890fb65a41bded4d2bf29beb0b61ac3cc3b97f828a27b22',
'befde6d78ea1c996ae1d959342730220e274992097aeeae00c908a971314e6ae',
'bf5b9464d154ffeff57f05f90b223c1045f0754befd01a9a42c99382115e7833',
'd2239a7c92bc5c72a959a73ccee7727ba0966f4d88a33d71dbbc26e3f171555d',
'd3f9cf6e1711595b4588366a96b725fc039f72a8a28b2e4c1b630016dd2eed39',
'd9a9ab00bb79ce7dca7f6329ab627dcea2e53e5c16b85bf4aa8c3980b76fad57',
'dd73f7d1e721ef5cba95f454a3019521456c1b43b97084ad29e3e6509148d45d',
'eb30d55e90e1f9e5c4727d6fa2a5a8cd36ab9ae9738eb8f3faf53e2bee4861ee',
'ede4076aef07fd79502d14326c54ab3911558371baaf697a19d077f4f89de399',
'f481abe5700c6940ca996adfa9d07c9d9f07a486a7ce9cdb2e6ee0fa6450fe31',
'f7355ec52730c2e6ba02216da8501b4b6934cd363511e9d47ae5b97056d02703',
'fdf0ec118d98393a7702cf72e46fc87ad680b152f64b2aac59e093ac2d688fbb',
],
};
+3 -1
View File
@@ -63,7 +63,9 @@ export const CandlesChartContainer = ({
overlays, overlays,
studies, studies,
notEnoughDataText: ( notEnoughDataText: (
<span className="text-xs text-center">{t('No data')}</span> <span className="text-xs text-center">
{t('No open orders')}
</span>
), ),
initialNumCandlesToDisplay: candlesCount, initialNumCandlesToDisplay: candlesCount,
studySize: STUDY_SIZE, studySize: STUDY_SIZE,
@@ -14,16 +14,17 @@ export const MarketProductPill = ({
}: { }: {
productType?: ProductType; productType?: ProductType;
}) => { }) => {
if (!productType) {
return null;
}
return ( return (
productType && ( <Pill
<Pill size="xxs"
size="xxs" className="uppercase ml-0.5"
className="uppercase ml-0.5" title={ProductTypeMapping[productType]}
title={ProductTypeMapping[productType]} >
> {ProductTypeShortName[productType]}
{ProductTypeShortName[productType]} </Pill>
</Pill>
)
); );
}; };
@@ -37,6 +37,7 @@ import {
AccordionChevron, AccordionChevron,
AccordionPanel, AccordionPanel,
Intent, Intent,
ExternalLink,
Pill, Pill,
Tooltip, Tooltip,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
@@ -385,7 +386,21 @@ export const DealTicketMarginDetails = ({
value={liquidationPriceEstimateRange} value={liquidationPriceEstimateRange}
formattedValue={liquidationPriceEstimate} formattedValue={liquidationPriceEstimate}
symbol={quoteName} symbol={quoteName}
labelDescription={LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT} labelDescription={
<>
<span>{LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}</span>{' '}
<span>
{t('For full details please see ')}
<ExternalLink
href={
'https://github.com/vegaprotocol/specs/blob/master/non-protocol-specs/0012-NP-LIPE-liquidation-price-estimate.md'
}
>
{t('liquidation price estimate documentation.')}
</ExternalLink>
</span>
</>
}
/> />
{partyId && ( {partyId && (
<AccountBreakdownDialog <AccountBreakdownDialog
@@ -43,7 +43,7 @@ import {
REDUCE_ONLY_TOOLTIP, REDUCE_ONLY_TOOLTIP,
stopSubmit, stopSubmit,
getNotionalSize, getNotionalSize,
getAssetUnit, getBaseQuoteUnit,
} from './deal-ticket'; } from './deal-ticket';
import { TypeToggle } from './type-selector'; import { TypeToggle } from './type-selector';
import { import {
@@ -854,7 +854,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
}, [watch, market.id, updateStoredFormValues]); }, [watch, market.id, updateStoredFormValues]);
const quoteName = getQuoteName(market); const quoteName = getQuoteName(market);
const assetUnit = getAssetUnit( const assetUnit = getBaseQuoteUnit(
market.tradableInstrument.instrument.metadata.tags market.tradableInstrument.instrument.metadata.tags
); );
@@ -127,7 +127,7 @@ const getDefaultValues = (
...storedValues, ...storedValues,
}); });
export const getAssetUnit = (tags?: string[] | null) => export const getBaseQuoteUnit = (tags?: string[] | null) =>
tags tags
?.find((tag) => tag.startsWith('base:') || tag.startsWith('ticker:')) ?.find((tag) => tag.startsWith('base:') || tag.startsWith('ticker:'))
?.replace(/^[^:]*:/, ''); ?.replace(/^[^:]*:/, '');
@@ -273,9 +273,7 @@ export const DealTicket = ({
const assetSymbol = getAsset(market).symbol; const assetSymbol = getAsset(market).symbol;
const assetUnit = getAssetUnit( const assetUnit = getQuoteName(market);
market.tradableInstrument.instrument.metadata.tags
);
const summaryError = useMemo(() => { const summaryError = useMemo(() => {
if (!pubKey) { if (!pubKey) {
+1 -1
View File
@@ -60,7 +60,7 @@ export const EST_FEES_TOOLTIP_TEXT = t(
); );
export const LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT = t( export const LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT = t(
'This is an approximation (or a range) for the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.' 'This is an approximation for the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.'
); );
export const EST_SLIPPAGE = t( export const EST_SLIPPAGE = t(
+2 -6
View File
@@ -1,18 +1,14 @@
import { Networks, useEnvironment } from '@vegaprotocol/environment'; import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { AsyncRendererInline } from '@vegaprotocol/ui-toolkit'; import { AsyncRendererInline } from '@vegaprotocol/ui-toolkit';
import { DepositManager } from './deposit-manager'; import { DepositManager } from './deposit-manager';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useEnabledAssets } from '@vegaprotocol/assets';
import { enabledAssetsProvider } from '@vegaprotocol/assets';
/** /**
* Fetches data required for the Deposit page * Fetches data required for the Deposit page
*/ */
export const DepositContainer = ({ assetId }: { assetId?: string }) => { export const DepositContainer = ({ assetId }: { assetId?: string }) => {
const { VEGA_ENV } = useEnvironment(); const { VEGA_ENV } = useEnvironment();
const { data, loading, error } = useDataProvider({ const { data, error, loading } = useEnabledAssets();
dataProvider: enabledAssetsProvider,
variables: undefined,
});
return ( return (
<AsyncRendererInline data={data} loading={loading} error={error}> <AsyncRendererInline data={data} loading={loading} error={error}>
+1 -1
View File
@@ -148,7 +148,7 @@ describe('Deposit form', () => {
}); });
fireEvent.submit(screen.getByTestId('deposit-form')); fireEvent.submit(screen.getByTestId('deposit-form'));
// 1001-DEPO-004
expect( expect(
await screen.findByText( await screen.findByText(
"You can't deposit more than you have in your Ethereum wallet, 5" "You can't deposit more than you have in your Ethereum wallet, 5"
@@ -1,6 +1,4 @@
import { t } from '@vegaprotocol/i18n'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import { useNodeSwitcherStore } from '../../hooks/use-node-switcher-store';
export const AppFailure = ({ export const AppFailure = ({
title, title,
@@ -9,16 +7,10 @@ export const AppFailure = ({
title: string; title: string;
error?: string | null; error?: string | null;
}) => { }) => {
const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
return ( return (
<Splash> <Splash>
<div className="text-center"> <p className="mb-4 text-xl">{title}</p>
<p className="mb-4 text-xl">{title}</p> {error && <p className="mb-8 text-sm">{error}</p>}
{error && <p className="mb-8 text-sm">{error}</p>}
<Button onClick={() => setNodeSwitcher(true)}>
{t('Change node')}
</Button>
</div>
</Splash> </Splash>
); );
}; };
+1
View File
@@ -4,4 +4,5 @@ export * from './network-loader';
export * from './network-switcher'; export * from './network-switcher';
export * from './node-guard'; export * from './node-guard';
export * from './node-switcher'; export * from './node-switcher';
export * from './node-failure';
export * from './etherscan-link'; export * from './etherscan-link';
@@ -33,15 +33,14 @@ export function NetworkLoader({
return undefined; return undefined;
}, [VEGA_URL, status, cache]); }, [VEGA_URL, status, cache]);
const nonIdealWrapperClasses =
'h-full min-h-screen flex items-center justify-center';
if (status === 'failed') { if (status === 'failed') {
return <div className={nonIdealWrapperClasses}>{failure}</div>; // eslint-disable-next-line react/jsx-no-useless-fragment
return <>{failure}</>;
} }
if (status === 'default' || status === 'pending' || !client) { if (status === 'default' || status === 'pending' || !client) {
return <div className={nonIdealWrapperClasses}>{skeleton}</div>; // eslint-disable-next-line react/jsx-no-useless-fragment
return <>{skeleton}</>;
} }
return <ApolloProvider client={client}>{children}</ApolloProvider>; return <ApolloProvider client={client}>{children}</ApolloProvider>;
@@ -0,0 +1 @@
export { NodeFailure } from './node-failure';
@@ -0,0 +1,24 @@
import { t } from '@vegaprotocol/i18n';
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import { useNodeSwitcherStore } from '../../hooks/use-node-switcher-store';
export const NodeFailure = ({
title,
error,
}: {
title: string;
error?: string | null;
}) => {
const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
return (
<Splash>
<div className="text-center">
<p className="mb-4 text-xl">{title}</p>
{error && <p className="mb-8 text-sm">{error}</p>}
<Button onClick={() => setNodeSwitcher(true)}>
{t('Change node')}
</Button>
</div>
</Splash>
);
};
@@ -11,11 +11,10 @@ export const NodeGuard = ({
skeleton: ReactNode; skeleton: ReactNode;
}) => { }) => {
const { data, error, loading } = useNodeGuardQuery(); const { data, error, loading } = useNodeGuardQuery();
const wrapperClasses =
'h-full min-h-screen flex items-center justify-center text-black dark:text-white';
if (loading) { if (loading) {
return <div className={wrapperClasses}>{skeleton}</div>; // eslint-disable-next-line
return <>{skeleton}</>;
} }
// It is possible for nodes to have a functioning datanode, but not return // It is possible for nodes to have a functioning datanode, but not return
@@ -23,9 +22,10 @@ export const NodeGuard = ({
const netParamEdges = data?.networkParametersConnection.edges; const netParamEdges = data?.networkParametersConnection.edges;
if (error || !netParamEdges || !netParamEdges.length) { if (error || !netParamEdges || !netParamEdges.length) {
return <div className={wrapperClasses}>{failure}</div>; // eslint-disable-next-line react/jsx-no-useless-fragment
return <>{failure}</>;
} }
// eslint-disable-next-line // eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>; return <>{children}</>;
}; };
@@ -338,9 +338,6 @@ function compileEnvVars() {
'GITHUB_FEEDBACK_URL', 'GITHUB_FEEDBACK_URL',
process.env['NX_GITHUB_FEEDBACK_URL'] process.env['NX_GITHUB_FEEDBACK_URL']
), ),
MAINTENANCE_PAGE: parseBoolean(
windowOrDefault('MAINTENANCE_PAGE', process.env['NX_MAINTENANCE_PAGE'])
),
GIT_BRANCH: windowOrDefault( GIT_BRANCH: windowOrDefault(
'GIT_COMMIT_BRANCH', 'GIT_COMMIT_BRANCH',
process.env['GIT_COMMIT_BRANCH'] process.env['GIT_COMMIT_BRANCH']
@@ -524,10 +521,6 @@ function parseNetworks(value?: string) {
return {}; return {};
} }
function parseBoolean(value?: string) {
return ['true', '1', 'yes'].includes(value?.toLowerCase() || '');
}
/** /**
* Provides a fallback ethereum provider url for test purposes in some apps * Provides a fallback ethereum provider url for test purposes in some apps
*/ */
@@ -50,7 +50,6 @@ export const envSchema = z
message: 'The NX_ETHERSCAN_URL environment variable must be a valid url', message: 'The NX_ETHERSCAN_URL environment variable must be a valid url',
}), }),
HOSTED_WALLET_URL: z.optional(z.string()), HOSTED_WALLET_URL: z.optional(z.string()),
MAINTENANCE_PAGE: z.optional(z.boolean()),
ETH_LOCAL_PROVIDER_URL: z.optional(z.string()), ETH_LOCAL_PROVIDER_URL: z.optional(z.string()),
ETH_WALLET_MNEMONIC: z.optional(z.string()), ETH_WALLET_MNEMONIC: z.optional(z.string()),
ANNOUNCEMENTS_CONFIG_URL: z.optional(z.string()), ANNOUNCEMENTS_CONFIG_URL: z.optional(z.string()),
@@ -6,8 +6,12 @@ fragment FundingPaymentFields on FundingPayment {
timestamp timestamp
} }
query FundingPayments($partyId: ID!, $pagination: Pagination) { query FundingPayments($partyId: ID!, $pagination: Pagination, $marketId: ID) {
fundingPayments(partyId: $partyId, pagination: $pagination) { fundingPayments(
partyId: $partyId
pagination: $pagination
marketId: $marketId
) {
edges { edges {
node { node {
...FundingPaymentFields ...FundingPaymentFields
@@ -8,6 +8,7 @@ export type FundingPaymentFieldsFragment = { __typename?: 'FundingPayment', mark
export type FundingPaymentsQueryVariables = Types.Exact<{ export type FundingPaymentsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID']; partyId: Types.Scalars['ID'];
pagination?: Types.InputMaybe<Types.Pagination>; pagination?: Types.InputMaybe<Types.Pagination>;
marketId?: Types.InputMaybe<Types.Scalars['ID']>;
}>; }>;
@@ -23,8 +24,8 @@ export const FundingPaymentFieldsFragmentDoc = gql`
} }
`; `;
export const FundingPaymentsDocument = gql` export const FundingPaymentsDocument = gql`
query FundingPayments($partyId: ID!, $pagination: Pagination) { query FundingPayments($partyId: ID!, $pagination: Pagination, $marketId: ID) {
fundingPayments(partyId: $partyId, pagination: $pagination) { fundingPayments(partyId: $partyId, pagination: $pagination, marketId: $marketId) {
edges { edges {
node { node {
...FundingPaymentFields ...FundingPaymentFields
@@ -55,6 +56,7 @@ export const FundingPaymentsDocument = gql`
* variables: { * variables: {
* partyId: // value for 'partyId' * partyId: // value for 'partyId'
* pagination: // value for 'pagination' * pagination: // value for 'pagination'
* marketId: // value for 'marketId'
* }, * },
* }); * });
*/ */
@@ -8,12 +8,14 @@ import { fundingPaymentsWithMarketProvider } from './funding-payments-data-provi
interface FundingPaymentsManagerProps { interface FundingPaymentsManagerProps {
partyId: string; partyId: string;
marketId?: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void; onMarketClick?: (marketId: string, metaKey?: boolean) => void;
gridProps: ReturnType<typeof useDataGridEvents>; gridProps: ReturnType<typeof useDataGridEvents>;
} }
export const FundingPaymentsManager = ({ export const FundingPaymentsManager = ({
partyId, partyId,
marketId,
onMarketClick, onMarketClick,
gridProps, gridProps,
}: FundingPaymentsManagerProps) => { }: FundingPaymentsManagerProps) => {
@@ -27,7 +29,7 @@ export const FundingPaymentsManager = ({
} }
return false; return false;
}, },
variables: { partyId }, variables: { partyId, marketId },
}); });
return ( return (
+1
View File
@@ -1 +1,2 @@
export * from './lib/ledger-export-form'; export * from './lib/ledger-export-form';
export * from './lib/ledger-download-store';
@@ -0,0 +1,142 @@
import { create } from 'zustand';
import type { ReactNode } from 'react';
import { useCallback, useEffect } from 'react';
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { useToasts, Intent } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { subscribeWithSelector } from 'zustand/middleware';
type DownloadSettings = {
title: string;
link: string;
filename?: string;
isDownloaded?: boolean;
isChanged?: boolean;
isError?: boolean;
errorMessage?: string;
isDelayed?: boolean;
intent?: Intent;
blob?: Blob;
};
export type LedgerDownloadFileStore = {
queue: DownloadSettings[];
hasItem: (link: string) => boolean;
removeItem: (link: string) => void;
updateQueue: (item: DownloadSettings) => void;
};
export const useLedgerDownloadFile = create<LedgerDownloadFileStore>()(
subscribeWithSelector((set, get) => ({
queue: [],
hasItem: (link: string) =>
get().queue.findIndex((item) => item.link === link) > -1,
removeItem: (link: string) => {
const queue = get().queue;
const index = queue.findIndex((item) => item.link === link);
if (index > -1) {
queue.splice(index, 1);
set({ queue: [...queue] });
}
},
updateQueue: (newitem: DownloadSettings) => {
const queue = get().queue;
const index = queue.findIndex((item) => item.link === newitem.link);
if (index > -1) {
queue[index] = { ...queue[index], ...newitem };
set({ queue: [...queue] });
} else {
set({ queue: [newitem, ...queue] });
}
},
}))
);
const ErrorContent = ({ message }: { message?: string }) => (
<>
<h4 className="mb-1 text-sm">{t('Something went wrong')}</h4>
<p>{message || t('Try again later')}</p>
</>
);
const InfoContent = ({ progress = false }) => (
<>
<p>{t('Please note this can take several minutes.')}</p>
<p>{t('You will be notified here when your file is ready.')}</p>
<h4 className="my-2">
{progress ? t('Still in progress') : t('Download has been started')}
</h4>
</>
);
export const useLedgerDownloadManager = () => {
const queue = useLedgerDownloadFile((store) => store.queue);
const updateQueue = useLedgerDownloadFile((store) => store.updateQueue);
const removeItem = useLedgerDownloadFile((store) => store.removeItem);
const [setToast, updateToast, hasToast, removeToast] = useToasts((store) => [
store.setToast,
store.update,
store.hasToast,
store.remove,
]);
const onDownloadClose = useCallback(
(id: string) => {
removeToast(id);
removeItem(id);
},
[removeToast, removeItem]
);
const createToast = (item: DownloadSettings) => {
let content: ReactNode;
switch (true) {
case item.isError:
content = <ErrorContent message={item.errorMessage} />;
break;
case Boolean(item.blob):
content = (
<>
<h4 className="mb-1 text-sm">{t('Your file is ready')}</h4>
<a
onClick={() => onDownloadClose(item.link)}
href={URL.createObjectURL(item.blob as Blob)}
download={item.filename}
className="underline"
>
{t('Get file here')}
</a>
</>
);
break;
default:
content = <InfoContent progress={item.isDelayed} />;
}
const toast: Toast = {
id: item.link,
intent: item.intent || Intent.Primary,
content: (
<>
<h3 className="mb-1 text-md uppercase">{item.title}</h3>
{content}
</>
),
onClose: () => onDownloadClose(item.link),
loader: !item.isDownloaded && !item.isError,
};
if (hasToast(toast.id)) {
updateToast(toast.id, toast);
} else {
setToast(toast);
}
};
useEffect(() => {
queue.forEach((item) => {
if (item.isChanged) {
createToast(item);
updateQueue({ ...item, isChanged: false });
}
});
}, [queue, createToast, updateQueue]);
};
+56 -20
View File
@@ -1,10 +1,21 @@
import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { createDownloadUrl, LedgerExportForm } from './ledger-export-form'; import { createDownloadUrl, LedgerExportForm } from './ledger-export-form';
import { formatForInput, toNanoSeconds } from '@vegaprotocol/utils'; import { formatForInput, toNanoSeconds } from '@vegaprotocol/utils';
import {
useLedgerDownloadManager,
useLedgerDownloadFile,
} from './ledger-download-store';
import { Intent } from '@vegaprotocol/ui-toolkit';
const mockSetToast = jest.fn();
jest.mock('@vegaprotocol/ui-toolkit', () => ({
...jest.requireActual('@vegaprotocol/ui-toolkit'),
useToasts: jest.fn(() => [mockSetToast, jest.fn(), jest.fn(() => false)]),
}));
const vegaUrl = 'https://vega-url.co.uk/querystuff'; const vegaUrl = 'https://vega-url.co.uk/querystuff';
const mockResponse = { const mockResponse = {
ok: true,
headers: { get: jest.fn() }, headers: { get: jest.fn() },
blob: () => '', blob: () => '',
}; };
@@ -28,6 +39,7 @@ describe('LedgerExportForm', () => {
afterAll(() => { afterAll(() => {
jest.useRealTimers(); jest.useRealTimers();
jest.clearAllMocks();
}); });
it('should be properly rendered', async () => { it('should be properly rendered', async () => {
@@ -43,8 +55,6 @@ describe('LedgerExportForm', () => {
// userEvent does not work with faked timers // userEvent does not work with faked timers
fireEvent.click(screen.getByTestId('ledger-download-button')); fireEvent.click(screen.getByTestId('ledger-download-button'));
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
await waitFor(() => { await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith( expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${ `https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
@@ -52,9 +62,6 @@ describe('LedgerExportForm', () => {
}&dateRange.startTimestamp=1691057410000000000` }&dateRange.startTimestamp=1691057410000000000`
); );
}); });
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
}); });
it('assetID should be properly change request url', async () => { it('assetID should be properly change request url', async () => {
@@ -75,8 +82,6 @@ describe('LedgerExportForm', () => {
fireEvent.click(screen.getByTestId('ledger-download-button')); fireEvent.click(screen.getByTestId('ledger-download-button'));
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
await waitFor(() => { await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith( expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${ `https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
@@ -84,9 +89,6 @@ describe('LedgerExportForm', () => {
}&dateRange.startTimestamp=1691057410000000000` }&dateRange.startTimestamp=1691057410000000000`
); );
}); });
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
}); });
it('date-from should properly change request url', async () => { it('date-from should properly change request url', async () => {
@@ -110,8 +112,6 @@ describe('LedgerExportForm', () => {
fireEvent.click(screen.getByTestId('ledger-download-button')); fireEvent.click(screen.getByTestId('ledger-download-button'));
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
await waitFor(() => { await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith( expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${ `https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
@@ -119,10 +119,6 @@ describe('LedgerExportForm', () => {
}&dateRange.startTimestamp=${toNanoSeconds(newDate)}` }&dateRange.startTimestamp=${toNanoSeconds(newDate)}`
); );
}); });
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
}); });
it('date-to should properly change request url', async () => { it('date-to should properly change request url', async () => {
@@ -156,10 +152,6 @@ describe('LedgerExportForm', () => {
)}` )}`
); );
}); });
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
}); });
it('Time zone sentence should be properly displayed', () => { it('Time zone sentence should be properly displayed', () => {
@@ -205,6 +197,50 @@ describe('LedgerExportForm', () => {
screen.queryByText(/^The downloaded file uses the UTC/) screen.queryByText(/^The downloaded file uses the UTC/)
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it('A toast notification should be displayed', async () => {
useLedgerDownloadFile.setState({ queue: [] });
const TestWrapper = () => {
useLedgerDownloadManager();
return (
<LedgerExportForm
partyId={partyId}
vegaUrl={vegaUrl}
assets={assetsMock}
/>
);
};
render(<TestWrapper />);
expect(screen.getByText('symbol asset-id')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('ledger-download-button'));
const link = `https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
Object.keys(assetsMock)[0]
}&dateRange.startTimestamp=1691057410000000000`;
await waitFor(() => {
expect(mockSetToast).toHaveBeenCalledWith({
id: link,
content: expect.any(Object),
onClose: expect.any(Function),
intent: Intent.Primary,
loader: true,
});
expect(global.fetch).toHaveBeenCalledWith(link);
});
mockSetToast.mockClear();
(global.fetch as jest.Mock).mockClear();
fireEvent.click(screen.getByTestId('ledger-download-button')); // click again
await waitFor(() => {
expect(mockSetToast).toHaveBeenCalled();
expect(global.fetch).not.toHaveBeenCalled();
});
});
}); });
describe('createDownloadUrl', () => { describe('createDownloadUrl', () => {
+75 -31
View File
@@ -1,17 +1,22 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { z } from 'zod'; import { format, subDays } from 'date-fns';
import { import {
TradingButton, Intent,
Loader, Loader,
TradingButton,
TradingFormGroup, TradingFormGroup,
TradingInput, TradingInput,
TradingSelect, TradingSelect,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { toNanoSeconds, VEGA_ID_REGEX } from '@vegaprotocol/utils'; import { z } from 'zod';
import {
formatForInput,
toNanoSeconds,
VEGA_ID_REGEX,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { localLoggerFactory } from '@vegaprotocol/logger'; import { localLoggerFactory } from '@vegaprotocol/logger';
import { formatForInput } from '@vegaprotocol/utils'; import { useLedgerDownloadFile } from './ledger-download-store';
import { subDays } from 'date-fns';
const DEFAULT_EXPORT_FILE_NAME = 'ledger_entries.csv'; const DEFAULT_EXPORT_FILE_NAME = 'ledger_entries.csv';
@@ -73,10 +78,14 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
const maxFromDate = formatForInput(new Date(dateTo || now.current)); const maxFromDate = formatForInput(new Date(dateTo || now.current));
const maxToDate = formatForInput(now.current); const maxToDate = formatForInput(now.current);
const [isDownloading, setIsDownloading] = useState(false);
const [assetId, setAssetId] = useState(Object.keys(assets)[0]); const [assetId, setAssetId] = useState(Object.keys(assets)[0]);
const protohost = getProtoHost(vegaUrl); const protohost = getProtoHost(vegaUrl);
const disabled = Boolean(!assetId || isDownloading); const disabled = Boolean(!assetId);
const hasItem = useLedgerDownloadFile((store) => store.hasItem);
const updateDownloadQueue = useLedgerDownloadFile(
(store) => store.updateQueue
);
const assetDropDown = ( const assetDropDown = (
<TradingSelect <TradingSelect
@@ -87,7 +96,6 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
}} }}
className="w-full" className="w-full"
data-testid="select-ledger-asset" data-testid="select-ledger-asset"
disabled={isDownloading}
> >
{Object.keys(assets).map((assetKey) => ( {Object.keys(assets).map((assetKey) => (
<option key={assetKey} value={assetKey}> <option key={assetKey} value={assetKey}>
@@ -97,32 +105,78 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
</TradingSelect> </TradingSelect>
); );
const link = createDownloadUrl({
protohost,
partyId,
assetId,
dateFrom,
dateTo,
});
const startDownload = async (event: React.FormEvent<HTMLFormElement>) => { const startDownload = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
try {
const link = createDownloadUrl({ const title = t('Downloading for %s from %s till %s', [
protohost, assets[assetId],
partyId, format(new Date(dateFrom), 'dd MMMM yyyy HH:mm'),
assetId, format(new Date(dateTo || Date.now()), 'dd MMMM yyyy HH:mm'),
dateFrom, ]);
dateTo,
const downloadStoreItem = {
title,
link,
isChanged: true,
};
if (hasItem(link)) {
updateDownloadQueue(downloadStoreItem);
return;
}
const ts = setTimeout(() => {
updateDownloadQueue({
...downloadStoreItem,
intent: Intent.Warning,
isDelayed: true,
isChanged: true,
}); });
setIsDownloading(true); }, 1000 * 30);
try {
updateDownloadQueue(downloadStoreItem);
const resp = await fetch(link); const resp = await fetch(link);
if (!resp?.ok) {
if (resp?.status === 429) {
throw new Error('Too many requests. Try again later.');
}
throw new Error('Download of ledger entries failed');
}
const { headers } = resp; const { headers } = resp;
const nameHeader = headers.get('content-disposition'); const nameHeader = headers.get('content-disposition');
const filename = nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME; const filename = nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME;
updateDownloadQueue({
...downloadStoreItem,
filename,
});
const blob = await resp.blob(); const blob = await resp.blob();
if (blob) { if (blob) {
const link = document.createElement('a'); updateDownloadQueue({
link.href = URL.createObjectURL(blob); ...downloadStoreItem,
link.download = filename; blob,
link.click(); isDownloaded: true,
isChanged: true,
intent: Intent.Success,
});
} }
} catch (err) { } catch (err) {
localLoggerFactory({ application: 'ledger' }).error('Download file', err); localLoggerFactory({ application: 'ledger' }).error('Download file', err);
updateDownloadQueue({
...downloadStoreItem,
intent: Intent.Danger,
isError: true,
isChanged: true,
errorMessage: (err as Error).message || undefined,
});
} finally { } finally {
setIsDownloading(false); clearTimeout(ts);
} }
}; };
@@ -145,7 +199,6 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
id="date-from" id="date-from"
value={dateFrom} value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)} onChange={(e) => setDateFrom(e.target.value)}
disabled={disabled}
max={maxFromDate} max={maxFromDate}
/> />
</TradingFormGroup> </TradingFormGroup>
@@ -156,19 +209,10 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
id="date-to" id="date-to"
value={dateTo} value={dateTo}
onChange={(e) => setDateTo(e.target.value)} onChange={(e) => setDateTo(e.target.value)}
disabled={disabled}
max={maxToDate} max={maxToDate}
/> />
</TradingFormGroup> </TradingFormGroup>
<div className="relative text-sm" title={t('Download all to .csv file')}> <div className="relative text-sm" title={t('Download all to .csv file')}>
{isDownloading && (
<div
className="absolute flex items-center justify-center w-full h-full"
data-testid="download-spinner"
>
<Loader size="small" />
</div>
)}
<TradingButton <TradingButton
fill fill
disabled={disabled} disabled={disabled}
+1 -1
View File
@@ -238,7 +238,7 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
volumeFormat={volumeFormat} volumeFormat={volumeFormat}
priceFormat={priceFormat} priceFormat={priceFormat}
notEnoughDataText={ notEnoughDataText={
<span className="text-xs text-center">{t('No data')}</span> <span className="text-xs text-center">{t('No open orders')}</span>
} }
/> />
)} )}
@@ -11,14 +11,8 @@ import type {
MarketDepthQuery, MarketDepthQuery,
MarketDepthQueryVariables, MarketDepthQueryVariables,
MarketDepthUpdateSubscription, MarketDepthUpdateSubscription,
PriceLevelFieldsFragment,
} from './__generated__/MarketDepth'; } from './__generated__/MarketDepth';
export type OrderbookData = {
asks: PriceLevelFieldsFragment[];
bids: PriceLevelFieldsFragment[];
};
interface OrderbookManagerProps { interface OrderbookManagerProps {
marketId: string; marketId: string;
onClick: (args: { price?: string; size?: string }) => void; onClick: (args: { price?: string; size?: string }) => void;
+1 -1
View File
@@ -238,7 +238,7 @@ export const Orderbook = ({
</> </>
) : ( ) : (
<div className="absolute inset-0"> <div className="absolute inset-0">
<Splash>{t('No data')}</Splash> <Splash>{t('No open orders')}</Splash>
</div> </div>
)} )}
</div> </div>
+3 -2
View File
@@ -3,12 +3,12 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client'; import * as Apollo from '@apollo/client';
const defaultOptions = {} as const; const defaultOptions = {} as const;
export type MarketsDataFieldsFragment = { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } }; export type MarketsDataFieldsFragment = { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } };
export type MarketsDataQueryVariables = Types.Exact<{ [key: string]: never; }>; export type MarketsDataQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type MarketsDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } } | null } }> } | null }; export type MarketsDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } } | null } }> } | null };
export const MarketsDataFieldsFragmentDoc = gql` export const MarketsDataFieldsFragmentDoc = gql`
fragment MarketsDataFields on MarketData { fragment MarketsDataFields on MarketData {
@@ -20,6 +20,7 @@ export const MarketsDataFieldsFragmentDoc = gql`
markPrice markPrice
trigger trigger
staticMidPrice staticMidPrice
marketState
marketTradingMode marketTradingMode
indicativeVolume indicativeVolume
indicativePrice indicativePrice
@@ -126,6 +126,16 @@ export const marketTradingModeProvider = makeDerivedDataProvider<
(parts[0] as ReturnType<typeof getData>)?.marketTradingMode (parts[0] as ReturnType<typeof getData>)?.marketTradingMode
); );
export const marketStateProvider = makeDerivedDataProvider<
MarketDataFieldsFragment['marketState'] | undefined,
never,
MarketDataQueryVariables
>(
[marketDataProvider],
(parts, variables, prevData) =>
(parts[0] as ReturnType<typeof getData>)?.marketState
);
export const fundingRateProvider = makeDerivedDataProvider< export const fundingRateProvider = makeDerivedDataProvider<
string, string,
never, never,
@@ -176,3 +186,11 @@ export const useMarketTradingMode = (marketId?: string, skip?: boolean) => {
skip: skip || !marketId, skip: skip || !marketId,
}); });
}; };
export const useMarketState = (marketId?: string, skip?: boolean) => {
return useDataProvider({
dataProvider: marketStateProvider,
variables: { marketId: marketId || '' },
skip: skip || !marketId,
});
};
@@ -7,6 +7,7 @@ fragment MarketsDataFields on MarketData {
markPrice markPrice
trigger trigger
staticMidPrice staticMidPrice
marketState
marketTradingMode marketTradingMode
indicativeVolume indicativeVolume
indicativePrice indicativePrice

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