Compare commits

...
Author SHA1 Message Date
Madalina Raicu 4ac0d675f5 fix: revert sortable setup 2023-12-04 18:10:32 +00:00
m.ray 1e5c523bc4 chore(trading): disable trades table sorting (#5423) 2023-12-04 15:40:32 +00:00
m.ray 37cd69ba6e chore(trading): move rewards to portfolio part 1 (#5402) 2023-12-04 15:40:14 +00:00
Ben 2c11045dd9 feat(trading): perp market tests (#5426) 2023-12-04 14:29:00 +00:00
ArtandMadalina Raicu 9aef41a119 fix(governance): update asset proposal (#5417)
Co-authored-by: Madalina Raicu <madalina@raygroup.uk>
2023-12-04 15:20:07 +01:00
m.ray 80ab8821d0 fix(trading): live time fraction zero redundant check (#5420) 2023-12-02 11:36:58 +00:00
Art 7100b0e9fc chore(accounts): no assets avaiable in transfer form (#5358) 2023-12-01 17:05:22 +00:00
614a83b7d6 chore(trading): merge main back in develop (fees discounts, discount stats from prev epoch) (#5415)
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
Co-authored-by: asiaznik <artur@vegaprotocol.io>
2023-12-01 17:03:41 +00:00
m.ray 3dc77b0eff chore(trading): add close position button back to console (#5407) 2023-12-01 16:38:32 +00:00
Art 127e784ceb chore(trading): don't redirect to inactive markets (#5359) 2023-12-01 14:42:26 +01:00
EddandMadalina Raicu e06f4818fc feat(explorer): add signature viewer (#5264)
Co-authored-by: Madalina Raicu <madalina@raygroup.uk>
2023-11-30 11:54:18 +00:00
Bartłomiej Głownia 8182da3b31 feat(deal-ticket): fix postOnly mapping in mapFormValuesToOrderSubmission (#5375) 2023-11-30 11:25:25 +01:00
Ben c8c56307bb chore(trading): update vega version (#5396) 2023-11-30 09:52:05 +00:00
Ben a8cd7f157f chore(trading): migrate cypress tests to python (#5367) 2023-11-30 09:42:57 +00:00
Bartłomiej Głownia 4f18caa486 feat(trading): upgrade i18n, fix plurals (#5331) 2023-11-30 07:31:44 +00:00
Matthew Russell 5c7c626bbc Merge pull request #5393 from vegaprotocol/chore/sync-main
chore(trading, datagrid, liquidity, proposals, ui-toolkit): sync main
2023-11-29 18:08:50 -08:00
Matthew Russell e4c4c20631 fix: duplicate props in ag-grid-themed 2023-11-29 14:09:08 -08:00
Matthew Russell 0697302d07 Merge branch 'main' into chore/sync-main 2023-11-29 14:06:45 -08:00
ArtandMadalina Raicu 2d926c0ce0 fix(governance): sensible vote numbers (#5384)
Co-authored-by: Madalina Raicu <madalina@raygroup.uk>
2023-11-29 10:38:41 -08:00
Art f57d6a7c7b fix(trading): missing volume discount program issue, inverted tiers (#5378) 2023-11-29 17:23:14 +00:00
m.ray 15f905046f fix(trading): fees accrued tooltip (#5387) 2023-11-29 17:17:44 +00:00
Bartłomiej Głownia 4f7918f64e feat(trading): remove proposal warning from market header (#5385) 2023-11-29 17:05:53 +00:00
Bartłomiej Głownia 52ab0562b0 feat(trading): refactor ledger export form validation (#5379) 2023-11-29 15:27:00 +00:00
m.ray 4e2b0d1b1d fix(trading): fix ag-grid transparent filters (#5377) 2023-11-29 15:24:37 +00:00
m.ray 0b0bcad9b3 fix(trading): ag-grid compactness adjustment (#5382) 2023-11-29 14:29:02 +00:00
Bartłomiej GłowniaandMatthew Russell bcf17bb34e feat(trading): i18n language switcher (#5320)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-11-29 15:16:17 +01:00
Bartłomiej Głownia a2b9b0da05 feat(deal-ticket): enhance for fee discounts (#5353) 2023-11-29 12:59:42 +00:00
Bartłomiej Głownia 7588d0cd11 feat(trading): refactor ledger export form validation (#5362) 2023-11-29 13:28:31 +01:00
m.ray 5ee1748495 fix(trading): ag-grid styling updates and filter fixes (#5368) 2023-11-28 22:13:12 +00:00
Art 73a118978f feat(trading): referral code eligibility (#5325) 2023-11-28 16:08:21 +01:00
m.ray eac26c1966 fix(trading): empty connect wallet dialog (#5356) 2023-11-28 14:43:31 +00:00
Matthew Russell d615587564 Merge pull request #5361 from vegaprotocol/chore/backmerge-main
chore(trading): back merge main
2023-11-27 22:33:19 -08:00
Matthew Russell ba4ce1ce88 fix: botched merge in gitignore 2023-11-27 22:06:46 -08:00
Matthew Russell 3bbacc1aa0 fix: fix broken test after removal of formatting 2023-11-27 21:22:30 -08:00
Matthew Russell de5371435d Merge branch 'main' into chore/backmerge-main 2023-11-27 20:08:08 -08:00
m.rayandMatthew Russell 12cb5e10b6 chore(trading): merge main in develop (#5352)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-11-27 15:49:26 -08:00
Art ee2909cc84 fix(wallet): empty connect dialog (#5354) 2023-11-27 16:15:18 +00:00
Bartłomiej Głownia 068d6abf1b feat(trading): show percentage sign on y axis in funding rate chart (#5342) 2023-11-27 13:39:20 +00:00
m.ray e5d4d2b0b8 chore(trading): temporarily disable close position (#5350) 2023-11-27 10:48:29 +00:00
m.ray d4e801cfc6 feat(trading): reset console to default (#5344) 2023-11-27 10:42:47 +00:00
Bartłomiej Głownia 964deb2f23 feat(trading): update title on rewards and fees pages (#5347) 2023-11-27 08:18:59 +00:00
Art 6669125dd3 feat(trading): copy apply referral link from code tile (#5338) 2023-11-24 10:20:37 +00:00
m.ray 129b6c4e89 feat(trading): referral initial state when no sets are available (#5294) 2023-11-23 17:08:26 +00:00
Ben ca418cabfe chore(trading): migrate python tests to jest (#5339) 2023-11-23 15:56:29 +00:00
Ben 192af844c4 chore(trading): fix change keys edge case (#5337) 2023-11-23 09:12:45 +00:00
Ben 6a841f226a chore(ci): reduce number of runners for python tests (#5333) 2023-11-22 16:00:29 +00:00
Ben 76426baa2a chore(ci): create traces dir (#5327) 2023-11-22 15:52:59 +00:00
Ben 6fdac2419c chore(trading): move python stop order tests to jest (#5329) 2023-11-22 15:10:19 +00:00
Art 87807d2088 chore(governance): sensible vote numbers (#5332) 2023-11-22 15:30:16 +01:00
m.rayandbwallacee a5f2533a66 feat(trading): market list snags and disable column removal (#5324)
Co-authored-by: bwallacee <ben@vega.xyz>
2023-11-22 13:51:39 +00:00
Ben 1e9251c9c0 chore(trading): python tests update vega version (#5313) 2023-11-22 09:39:40 +00:00
Ben 2a27e124d7 chore(ci): increase number of tests in parallel (#5322) 2023-11-22 08:17:22 +00:00
Ben 4ee85720ab chore(ci): specific ganache image (#5321) 2023-11-22 08:02:47 +00:00
BenandBartłomiej Głownia a19da4c408 chore(trading): cypress to python tests (#5319)
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
2023-11-21 14:44:41 +00:00
BenandMatthew Russell 65ad4fffda chore(trading): migrate LP cypress tests (#5286)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-11-21 14:36:02 +00:00
Bartłomiej GłowniaandMatthew Russell 11317cdb2e feat(withdraws): use i18next (#5285)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-11-21 13:49:10 +00:00
Bartłomiej GłowniaandMatthew Russell c98870f27d feat(wallet): use i18next (#5299)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-11-21 13:48:53 +00:00
Bartłomiej Głowniaandbwallacee c0c56e4c48 feat(trading): fix markets i18n namespace, add missing label, preload all namespaces (#5312)
Co-authored-by: bwallacee <ben@vega.xyz>
2023-11-21 12:08:33 +01:00
Matthew Russell d9dc43b359 chore(deal-ticket): re-enable margin estimates for perps (#5317) 2023-11-20 13:57:18 -08:00
m.rayandMatthew Russell 06a6fe6d67 fix(accounts): remove account balance formatting in transfer form (#5315)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-11-20 13:07:17 -08:00
Bartłomiej GłowniaandMatthew Russell a9ca215276 feat(proposals): use i18next (#5284)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-11-19 21:49:39 -08:00
Bartłomiej GłowniaandMatthew Russell 61a9eb2f5c feat(ui-toolkit): use i18next (#5289)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-11-19 15:27:52 -08:00
Bartłomiej GłowniaandMatthew Russell bb47747501 feat(utils): use i18next (#5269)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-11-19 14:00:45 -08:00
260 changed files with 4777 additions and 3343 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ on:
inputs:
console-test-branch:
type: choice
description: 'main: v0.72.14, develop: v0.73.4'
description: 'main: v0.73.5, develop: v0.73.5'
options:
- main
- develop
@@ -215,7 +215,7 @@ jobs:
if: always()
with:
name: playwright-trace
path: ./traces/
path: apps/trading/e2e/traces/
retention-days: 15
#----------------------------------------------
# ----- upload logs -----
+1 -2
View File
@@ -59,5 +59,4 @@ apps/trading/e2e/logs/
apps/trading/e2e/.pytest_cache/
apps/trading/e2e/traces/
.nx/cache
.nx/
+19 -14
View File
@@ -1,3 +1,4 @@
import '../i18n';
import {
NetworkLoader,
NodeFailure,
@@ -28,20 +29,24 @@ function App() {
);
return (
<TendermintWebsocketProvider>
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<NodeGuard
skeleton={<div>{t('Loading')}</div>}
failure={<NodeFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
>
<Suspense fallback={splashLoading}>
<RouterProvider router={router} fallbackElement={splashLoading} />
</Suspense>
</NodeGuard>
<NodeSwitcherDialog
open={nodeSwitcherOpen}
setOpen={setNodeSwitcherOpen}
/>
</NetworkLoader>
<Suspense fallback={splashLoading}>
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<NodeGuard
skeleton={<div>{t('Loading')}</div>}
failure={
<NodeFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />
}
>
<Suspense fallback={splashLoading}>
<RouterProvider router={router} fallbackElement={splashLoading} />
</Suspense>
</NodeGuard>
<NodeSwitcherDialog
open={nodeSwitcherOpen}
setOpen={setNodeSwitcherOpen}
/>
</NetworkLoader>
</Suspense>
</TendermintWebsocketProvider>
);
}
@@ -12,7 +12,8 @@ import { type VegaICellRendererParams } from '@vegaprotocol/datagrid';
import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { useNavigate } from 'react-router-dom';
import { type RowClickedEvent, ColDef } from 'ag-grid-community';
import { type ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community';
type AssetsTableProps = {
data: AssetFieldsFragment[] | null;
@@ -8,7 +8,8 @@ import {
type VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { type RowClickedEvent, ColDef } from 'ag-grid-community';
import { type ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
@@ -0,0 +1,59 @@
import { useState } from 'react';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
import {
CopyWithTooltip,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
interface SignatureProps {
signature: BlockExplorerTransactionResult['signature'];
}
const valueClass =
'font-mono px-2.5 py-0.5 text-xs max-w-[200px] cursor-pointer';
const valueClassClosed = 'text-ellipsis overflow-hidden';
const valueClassOpen = 'break-words text-left';
/**
* Viewer component for a vega signature. Featuers copy and pasting, truncation
*
* @param signature
*/
export const Signature = ({ signature }: SignatureProps) => {
const [isOpen, setIsOpen] = useState(false);
if (!signature || !signature.value || !signature.version || !signature.algo) {
return null;
}
return (
<div className="inline-flex border rounded signature-component relative pr-[20px]">
<span
className="bg-gray-100 px-2.5 py-0.5 text-xs text-gray-500 select-none cursor-default"
title={`Version ${signature.version}`}
>
{signature.algo}
</span>
<div
className={
isOpen
? `${valueClass} ${valueClassOpen}`
: `${valueClass} ${valueClassClosed}`
}
>
<CopyWithTooltip text={signature.value}>
<span title={signature.value}>{signature.value}</span>
</CopyWithTooltip>
</div>
<button
onClick={() => setIsOpen(!isOpen)}
className="absolute top-[-3px] right-0 pr-2"
title={t('Show full signature')}
>
<VegaIcon name={isOpen ? VegaIconNames.EYE_OFF : VegaIconNames.EYE} />
</button>
</div>
);
};
@@ -9,6 +9,7 @@ import { Time } from '../../../time';
import { ChainResponseCode } from '../chain-response-code/chain-reponse.code';
import { TxDataView } from '../../tx-data-view';
import Hash from '../../../links/hash';
import { Signature } from '../../../signature/signature';
interface TxDetailsSharedProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -75,6 +76,12 @@ export const TxDetailsShared = ({
<BlockLink height={height} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Signature')}</TableCell>
<TableCell>
<Signature signature={txData.signature} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Time')}</TableCell>
<TableCell>
@@ -98,6 +98,8 @@ describe('TxDetailsTransfer', () => {
},
},
signature: {
version: '1',
algo: 'vega/ed25519',
value:
'610c2e196a7d4fed4413b9e82af267b1ff3e30e943df3a3d28096fd60604d430d752fbaf6dd4f84d496be78885bb6118f40560bff7832c06bd7a3d67b718b700',
},
@@ -20,6 +20,8 @@ const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
'4b782482f587d291e8614219eb9a5ee9280fa2c58982dee71d976782a9be1964',
type: 'Submit Order',
signature: {
version: '1',
algo: 'vega/ed25519',
value: '123',
},
code: 0,
@@ -23,6 +23,8 @@ const txData: BlockExplorerTransactionResult = {
type: 'type',
command: {} as ValidatorHeartbeat,
signature: {
version: '1',
algo: 'vega/ed25519',
value: '123',
},
};
@@ -11,6 +11,8 @@ export interface BlockExplorerTransactionResult {
cursor: string;
command: components['schemas']['blockexplorerv1transaction'];
signature: {
version: string;
algo: string;
value: string;
};
error?: string;
+14
View File
@@ -3,6 +3,9 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
import { locales } from '@vegaprotocol/i18n';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
Object.defineProperty(window, 'ResizeObserver', {
writable: false,
@@ -13,3 +16,14 @@ Object.defineProperty(window, 'ResizeObserver', {
disconnect: jest.fn(),
})),
});
// Set up i18n instance so that components have the correct default
// en translations
i18n.use(initReactI18next).init({
// we init with resources
resources: locales,
fallbackLng: 'en',
nsSeparator: false,
ns: ['explorer'],
defaultNS: 'explorer',
});
+1
View File
@@ -0,0 +1 @@
../../../../libs/i18n/src/locales
+45
View File
@@ -0,0 +1,45 @@
import type { Module } from 'i18next';
import i18n from 'i18next';
import HttpBackend from 'i18next-http-backend';
import LocizeBackend from 'i18next-locize-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
const isInDev = process.env.NODE_ENV === 'development';
const useLocize = isInDev && !!process.env.NX_USE_LOCIZE;
const backend = useLocize
? {
projectId: '96ac1231-4bdd-455a-b9d7-f5322a2e7430',
apiKey: process.env.NX_LOCIZE_API_KEY,
referenceLng: 'en',
}
: {
loadPath: '/assets/locales/{{lng}}/{{ns}}.json',
};
const Backend: Module = useLocize ? LocizeBackend : HttpBackend;
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
lng: 'en',
fallbackLng: 'en',
supportedLngs: ['en'],
load: 'languageOnly',
debug: isInDev,
// have a common namespace used around the full app
ns: ['explorer'],
defaultNS: 'explorer',
keySeparator: false, // we use content as keys
nsSeparator: false,
backend,
saveMissing: useLocize && !!process.env.NX_LOCIZE_API_KEY,
interpolation: {
escapeValue: false,
},
});
export default i18n;
@@ -23,10 +23,13 @@ export const Heading = ({
})}
>
<h1
className={classNames('font-alpha calt text-5xl break-words', {
'mt-0': !marginTop,
'mb-0': !marginBottom,
})}
className={classNames(
'font-alpha calt text-5xl [word-break:break-word]',
{
'mt-0': !marginTop,
'mb-0': !marginBottom,
}
)}
>
{title}
</h1>
@@ -7,8 +7,10 @@ import type { AssetFieldsFragment } from '@vegaprotocol/assets';
export const ProposalAssetDetails = ({
asset,
originalAsset,
}: {
asset: AssetFieldsFragment;
originalAsset?: AssetFieldsFragment;
}) => {
const { t } = useTranslation();
const [showAssetDetails, setShowAssetDetails] = useState(false);
@@ -27,6 +29,7 @@ export const ProposalAssetDetails = ({
<div className="mb-10 pb-4">
<AssetDetailsTable
asset={asset}
originalAsset={originalAsset}
omitRows={[
AssetDetail.STATUS,
AssetDetail.INFRASTRUCTURE_FEE_ACCOUNT_BALANCE,
@@ -65,10 +65,13 @@ export const Proposal = ({
? removePaginationWrapper(assetData.assetsConnection?.edges)[0]
: undefined;
const originalAsset = asset;
if (proposal.terms.change.__typename === 'UpdateAsset' && asset) {
asset = {
...asset,
quantum: proposal.terms.change.quantum,
source: { ...asset.source },
};
if (asset.source.__typename === 'ERC20') {
@@ -228,7 +231,7 @@ export const Proposal = ({
proposal.terms.change.__typename === 'UpdateAsset') &&
asset && (
<div className="mb-4">
<ProposalAssetDetails asset={asset} />
<ProposalAssetDetails asset={asset} originalAsset={originalAsset} />
</div>
)}
@@ -1,7 +1,7 @@
import {
getProposalDialogIcon,
getProposalDialogIntent,
getProposalDialogTitle,
useGetProposalDialogTitle,
} from '@vegaprotocol/proposals';
import type { ProposalEventFieldsFragment } from '@vegaprotocol/proposals';
import type { DialogProps } from '@vegaprotocol/proposals';
@@ -15,6 +15,7 @@ export const ProposalFormTransactionDialog = ({
finalizedProposal,
TransactionDialog,
}: ProposalFormTransactionDialogProps) => {
const title = useGetProposalDialogTitle(finalizedProposal?.state);
// Render a custom complete UI if the proposal was rejected otherwise
// pass undefined so that the default vega transaction dialog UI gets used
const completeContent = finalizedProposal?.rejectionReason ? (
@@ -24,7 +25,7 @@ export const ProposalFormTransactionDialog = ({
return (
<div data-testid="proposal-transaction-dialog">
<TransactionDialog
title={getProposalDialogTitle(finalizedProposal?.state)}
title={title}
intent={getProposalDialogIntent(finalizedProposal?.state)}
icon={getProposalDialogIcon(finalizedProposal?.state)}
content={{
@@ -8,7 +8,7 @@ import {
networkParamsQueryMock,
nextWeek,
} from '../../test-helpers/mocks';
import { VoteBreakdown } from './vote-breakdown';
import { CompactVotes, VoteBreakdown } from './vote-breakdown';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import {
@@ -346,3 +346,22 @@ describe('VoteBreakdown', () => {
expect(style.width).toBe(`${expectedProgress}%`);
});
});
describe('CompactVotes', () => {
it.each([
[0, '0'],
[1, '1'],
[12, '12'],
[123, '123'],
[1234, '1.2K'],
[12345, '12.3K'],
[123456, '123.5K'],
[1234567, '1.2M'],
[12345678, '12.3M'],
[123456789, '123.5M'],
[1234567890, '1.2B'],
])('compacts %s to %s', (input, output) => {
const { getByTestId } = render(<CompactVotes number={BigNumber(input)} />);
expect(getByTestId('compact-number').textContent).toBe(output);
});
});
@@ -3,11 +3,21 @@ import BigNumber from 'bignumber.js';
import { useTranslation } from 'react-i18next';
import { useVoteInformation } from '../../hooks';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { formatNumber, toBigNum } from '@vegaprotocol/utils';
import { formatNumber } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { CompactNumber } from '@vegaprotocol/react-helpers';
export const CompactVotes = ({ number }: { number: BigNumber }) => (
<CompactNumber
number={number}
decimals={number.isGreaterThan(1000) ? 1 : 0}
compactAbove={1000}
compactDisplay="short"
/>
);
interface VoteBreakdownProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
@@ -198,10 +208,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
)}
>
<button>
{yesEquityLikeShareWeight
.dividedBy(toBigNum(10 ** 6, 0))
.toFixed(1)}
M
<CompactVotes number={yesEquityLikeShareWeight} />
</button>
</Tooltip>
<span>
@@ -226,10 +233,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
)}
>
<button>
{noEquityLikeShareWeight
.dividedBy(toBigNum(10 ** 6, 0))
.toFixed(1)}
M
<CompactVotes number={noEquityLikeShareWeight} />
</button>
</Tooltip>
<span>
@@ -279,10 +283,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
)}
>
<button>
{totalEquityLikeShareWeight
.dividedBy(toBigNum(10 ** 6, 0))
.toFixed(1)}
M
<CompactVotes number={totalEquityLikeShareWeight} />
</button>
</Tooltip>
<span>
@@ -321,7 +322,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
<span>{t('tokenVotesFor')}:</span>
<Tooltip description={formatNumber(yesTokens, defaultDP)}>
<button data-testid="num-votes-for">
{yesTokens.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
<CompactVotes number={yesTokens} />
</button>
</Tooltip>
<span>
@@ -341,7 +342,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
<span>{t('tokenVotesAgainst')}:</span>
<Tooltip description={formatNumber(noTokens, defaultDP)}>
<button data-testid="num-votes-against">
{noTokens.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
<CompactVotes number={noTokens} />
</button>
</Tooltip>
<span>
@@ -384,7 +385,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
<span>{t('totalTokensVoted')}:</span>
<Tooltip description={formatNumber(totalTokensVoted, defaultDP)}>
<button data-testid="total-voted">
{totalTokensVoted.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
<CompactVotes number={totalTokensVoted} />
</button>
</Tooltip>
<span data-testid="total-voted-percentage">
@@ -8,7 +8,7 @@ import {
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import { useValidateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -41,6 +41,7 @@ export interface NewAssetProposalFormFields {
const DOCS_LINK = '/new-asset-proposal';
export const ProposeNewAsset = () => {
const validateJson = useValidateJson();
const {
params,
loading: networkParamsLoading,
@@ -7,7 +7,7 @@ import {
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import { useValidateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -39,6 +39,7 @@ export interface NewMarketProposalFormFields {
const DOCS_LINK = '/new-market-proposal';
export const ProposeNewMarket = () => {
const validateJson = useValidateJson();
const {
params,
loading: networkParamsLoading,
@@ -14,7 +14,7 @@ import {
RoundedWrapper,
TextArea,
} from '@vegaprotocol/ui-toolkit';
import { validateJson } from '@vegaprotocol/utils';
import { useValidateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -31,6 +31,7 @@ export interface RawProposalFormFields {
}
export const ProposeRaw = () => {
const validateJson = useValidateJson();
const {
params,
loading: networkParamsLoading,
@@ -7,7 +7,7 @@ import {
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import { useValidateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -39,6 +39,7 @@ export interface UpdateAssetProposalFormFields {
const DOCS_LINK = '/update-asset-proposal';
export const ProposeUpdateAsset = () => {
const validateJson = useValidateJson();
const {
params,
loading: networkParamsLoading,
@@ -8,7 +8,7 @@ import {
useProposalSubmit,
} from '@vegaprotocol/proposals';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import { useValidateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -53,6 +53,7 @@ export interface UpdateMarketProposalFormFields {
const DOCS_LINK = '/update-market-proposal';
export const ProposeUpdateMarket = () => {
const validateJson = useValidateJson();
const {
params,
loading: networkParamsLoading,
@@ -260,7 +261,7 @@ export const ProposeUpdateMarket = () => {
</FormGroup>
{selectedMarket && (
<div className="mt-[-20px] mb-6">
<div className="mb-6 mt-[-20px]">
<KeyValueTable data-testid="update-market-details">
<KeyValueTableRow>
{t('MarketName')}
@@ -1,204 +0,0 @@
const marketInfoBtn = 'Info';
const marketInfoSubtitle = 'accordion-title';
const marketSummaryBlock = 'header-summary';
const marketExpiry = 'market-expiry';
const marketPrice = 'market-price';
const marketChange = 'market-change';
const marketVolume = 'market-volume';
const marketMode = 'market-trading-mode';
const marketSettlement = 'market-settlement-asset';
const percentageValue = 'price-change-percentage';
const priceChangeValue = 'price-change';
const itemHeader = 'item-header';
const itemValue = 'item-value';
const marketListContent = 'popover-content';
describe(
'Console - market info - live env',
{ tags: '@live', testIsolation: true },
() => {
before(() => {
cy.visit('/');
cy.contains('Loading market data...').should('not.exist');
cy.getByTestId('link').should('be.visible');
cy.getByTestId('dialog-close').click();
cy.getByTestId(marketInfoBtn).click();
});
const titles = ['Market data', 'Market specification', 'Market governance'];
const subtitles = [
'Current fees',
'Market price',
'Market volume',
'Insurance pool',
'Key details',
'Instrument',
'Settlement asset',
'Metadata',
'Risk model',
'Risk parameters',
'Risk factors',
'Price monitoring bounds 1',
'Liquidity monitoring parameters',
'Liquidity',
'Liquidity price range',
'Oracle',
'Proposal',
];
it('market info titles are displayed', () => {
cy.getByTestId('split-view-view')
.find('.text-lg')
.each((element, index) => {
cy.wrap(element).should('have.text', titles[index]);
});
});
it('market info subtitles are displayed', () => {
cy.getByTestId('popover-trigger').click();
cy.contains('Loading market data...').should('not.exist');
cy.contains('[data-testid="link"]', 'AAVEDAI.MF21').click();
cy.getByTestId(marketInfoBtn).click();
cy.getByTestId(marketInfoSubtitle).each((element, index) => {
cy.wrap(element).should('have.text', subtitles[index]);
});
});
it('renders correctly liquidity in trading tab', () => {
cy.getByTestId('Liquidity').click();
cy.contains('Loading').should('not.exist');
cy.contains('Something went wrong').should('not.exist');
cy.contains('Application error').should('not.exist');
cy.getByTestId('tab-liquidity').within(() => {
cy.get('[col-id="partyId"]').eq(1).should('not.be.empty');
});
});
}
);
describe(
'Console - market summary - live env',
{ tags: '@live', testIsolation: true },
() => {
before(() => {
cy.visit('/');
cy.getByTestId('dialog-close').click();
cy.getByTestId(marketSummaryBlock).should('be.visible');
});
it('must display market name', () => {
cy.getByTestId('popover-trigger').should('not.be.empty');
});
it('must see market expiry', () => {
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketExpiry).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Expiry');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
it('must see market price', () => {
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketPrice).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Price');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
it('must see market change', () => {
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketChange).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Change (24h)');
cy.getByTestId(percentageValue).should('not.be.empty');
cy.getByTestId(priceChangeValue).should('not.be.empty');
});
});
});
it('must see market volume', () => {
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketVolume).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Volume (24h)');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
it('must see market mode', () => {
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketMode).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Trading mode');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
it('must see market settlement', () => {
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketSettlement).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Settlement asset');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
}
);
describe(
'Console - markets table - live env',
{ tags: '@live', testIsolation: true },
() => {
beforeEach(() => {
cy.visit('/');
});
it('renders markets correctly', () => {
cy.get('[data-testid^="market-link-"]').should('not.be.empty');
cy.getByTestId('price').invoke('text').should('not.be.empty');
cy.getByTestId('settlement-asset').should('not.be.empty');
cy.getByTestId('price-change-percentage').should('not.be.empty');
cy.getByTestId('price-change').should('not.be.empty');
cy.getByTestId('sparkline-svg').should('be.visible');
});
it('renders market list drop down', () => {
openMarketDropDown();
cy.getByTestId(marketListContent)
.find('[data-testid="price"]')
.invoke('text')
.should('not.be.empty');
cy.getByTestId(marketListContent)
.find('[data-testid="trading-mode-col"]')
.should('not.be.empty');
cy.getByTestId(marketListContent)
.find('[data-testid="taker-fee"]')
.should('contain.text', '%');
cy.getByTestId(marketListContent)
.find('[data-testid="market-volume"]')
.should('not.be.empty');
cy.getByTestId(marketListContent)
.find('[data-testid="market-name"]')
.should('not.be.empty');
});
it('Able to select market from dropdown', () => {
cy.getByTestId('popover-trigger')
.invoke('text')
.then((marketName) => {
openMarketDropDown();
cy.get('[data-testid^=market-link]').eq(1).click();
cy.getByTestId('popover-trigger').should('not.be.equal', marketName);
});
});
}
);
function openMarketDropDown() {
cy.contains('Loading...').should('not.exist');
cy.getByTestId('link').should('be.visible');
cy.getByTestId('dialog-close').click();
cy.getByTestId('popover-trigger').click();
cy.contains('Loading market data...').should('not.exist');
}
@@ -1,311 +0,0 @@
import { checkSorting } from '@vegaprotocol/cypress';
import * as Schema from '@vegaprotocol/types';
const liquidityTab = 'Liquidity';
const rowSelector =
'[data-testid="tab-liquidity"] .ag-center-cols-container .ag-row';
const rowSelectorLiquidityActive =
'[data-testid="tab-active"] .ag-center-cols-container .ag-row';
const rowSelectorLiquidityInactive =
'[data-testid="tab-inactive"] .ag-center-cols-container .ag-row';
const marketSummaryBlock = 'header-summary';
const itemValue = 'item-value';
const itemHeader = 'item-header';
const colCommitmentAmount = '[col-id="commitmentAmount"]';
const colEquityLikeShare = '[col-id="feeShare.equityLikeShare"]';
const colFee = '[col-id="fee"]';
const colCommitmentAmount_1 = '[col-id="commitmentAmount_1"]';
const colBalance = '[col-id="balance"]';
const colStatus = '[col-id="status"]';
const colCreatedAt = '[col-id="createdAt"] button';
const colUpdatedAt = '[col-id="updatedAt"] button';
const headers = [
'Party',
'Status',
'Commitment (tDAI)',
'Obligation',
'Fee',
'Adjusted stake share',
'Share',
'Live supplied liquidity',
'Fees accrued this epoch',
'Live time on book',
'Live liquidity quality score (%)',
'Last time on the book',
'Last fee penalty',
'Last bond penalty',
'Created',
'Updated',
];
describe('liquidity table - trading', { tags: '@smoke' }, () => {
before(() => {
cy.setOnBoardingViewed();
cy.mockSubscription();
cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.visit('/#/markets/market-0');
cy.wait('@MarketData');
cy.getByTestId(liquidityTab).click();
cy.wait('@LiquidityProvisions');
});
it('can see table headers', () => {
// 5002-LIQP-001
cy.getByTestId('tab-liquidity').within(($headers) => {
cy.wrap($headers)
.get('.ag-header-cell-text')
.each(($header, i) => {
cy.wrap($header).should('have.text', headers[i]);
});
});
});
it('renders liquidity table correctly', () => {
// 5002-LIQP-002
cy.get(rowSelector)
.first()
.find('[col-id="partyId"]')
.should('have.text', '69464e…dc6f');
cy.get(rowSelector)
.first()
.find(colCommitmentAmount)
.should('have.text', '4,000.00');
cy.get(rowSelector)
.first()
.find(colEquityLikeShare)
.should('have.text', '100.00%');
cy.get(rowSelector).first().find(colFee).should('have.text', '0.09%');
cy.get(rowSelector)
.first()
.find(colCommitmentAmount_1)
.should('have.text', '4,000.00');
cy.get(rowSelector)
.first()
.find(colBalance)
.scrollIntoView()
.should('have.text', '4,000.00');
cy.get(rowSelector).first().find(colStatus).should('have.text', 'Active');
cy.get(rowSelector).first().find(colCreatedAt).should('not.be.empty');
cy.get(rowSelector).first().find(colUpdatedAt).should('not.be.empty');
});
it('liquidity status column should be sorted properly', () => {
// 5002-LIQP-003
const liquidityColDefault = ['Active', 'Pending'];
const liquidityColAsc = ['Active', 'Pending'];
const liquidityColDesc = ['Pending', 'Active'];
checkSorting(
'status',
liquidityColDefault,
liquidityColAsc,
liquidityColDesc
);
});
});
describe('liquidity table view', { tags: '@smoke' }, () => {
before(() => {
cy.setOnBoardingViewed();
cy.mockSubscription();
cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.visit('/#/liquidity/market-0');
cy.wait('@LiquidityProvisions');
});
it('can see header title', () => {
// 5002-LIQP-004
// 5002-LIQP-005
cy.getByTestId('header-title').should(
'contain.text',
'BTCUSD.MF21 liquidity provision'
);
});
it('can see target stake', () => {
// 5002-LIQP-006
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('target-stake').within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Target stake');
cy.getByTestId(itemValue).should('have.text', '10.00 tDAI').realHover();
});
});
cy.getByTestId('tooltip-content').should(
'contain.text',
`The market's liquidity requirement which is derived from the maximum open interest observed over a rolling time window.The market's liquidity requirement which is derived from the maximum open interest observed over a rolling time window.`
);
});
it('can see supplied stake', () => {
// 5002-LIQP-007
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('supplied-stake').within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Supplied stake');
cy.getByTestId(itemValue).should('have.text', '0.01 tDAI').realHover();
});
});
cy.getByTestId('tooltip-content').should(
'contain.text',
'The current amount of liquidity supplied for this market.'
);
});
it('can see liquidity supplied', () => {
// 5002-LIQP-008
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('liquidity-supplied').within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Liquidity supplied');
cy.getByTestId('indicator').should('be.visible');
cy.getByTestId(itemValue).should('have.text', ' 0.10%').realHover();
});
});
});
it('can see market id', () => {
// 5002-LIQP-009
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('liquidity-market-id').within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Market ID');
cy.getByTestId(itemValue).should('have.text', 'market-0');
});
});
});
it('can see market id', () => {
// 5002-LIQP-010
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('liquidity-learn-more').within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Learn more');
cy.getByTestId(itemValue).should('have.text', 'Providing liquidity');
cy.getByTestId('external-link')
.should('have.attr', 'href')
.and(
'include',
'https://docs.vega.xyz/testnet/concepts/liquidity/provision'
);
});
});
});
describe('liquidity table view', { tags: '@smoke' }, () => {
it('can see table headers', () => {
cy.getByTestId('tab-active').within(($headers) => {
cy.wrap($headers)
.get('.ag-header-cell-text')
.each(($header, i) => {
cy.wrap($header).should('have.text', headers[i]);
});
});
});
it('renders liquidity active table correctly', () => {
// 5002-LIQP-011
cy.get(rowSelectorLiquidityActive)
.first()
.find('[col-id="partyId"]')
.should('have.text', '69464e…dc6f');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colCommitmentAmount)
.should('have.text', '4,000.00');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colEquityLikeShare)
.should('have.text', '100.00%');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colFee)
.should('have.text', '0.09%');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colCommitmentAmount_1)
.should('have.text', '4,000.00');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colBalance)
.should('have.text', '4,000.00');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colStatus)
.should('have.text', 'Active');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colCreatedAt)
.should('not.be.empty');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colUpdatedAt)
.should('not.be.empty');
});
it('renders liquidity inactive table correctly', () => {
// 5002-LIQP-012
cy.getByTestId('Inactive').click();
cy.get(rowSelectorLiquidityInactive)
.first()
.find('[col-id="partyId"]')
.should('have.text', 'cc464e…dc6f');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colCommitmentAmount)
.should('have.text', '4,000.00');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colEquityLikeShare)
.should('have.text', '100.00%');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colFee)
.should('have.text', '0.40%');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colCommitmentAmount_1)
.should('have.text', '4,000.00');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colBalance)
.should('have.text', '2,000.00');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colStatus)
.should('have.text', 'Pending');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colCreatedAt)
.should('not.be.empty');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colUpdatedAt)
.should('not.be.empty');
});
});
});
@@ -1,47 +0,0 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { proposalListQuery, marketUpdateProposal } from '@vegaprotocol/mock';
import * as Schema from '@vegaprotocol/types';
const marketSummaryBlock = 'header-summary';
describe('Market proposal notification', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.mockGQL((req) => {
aliasGQLQuery(
req,
'ProposalsList',
proposalListQuery({
proposalsConnection: {
edges: [{ node: marketUpdateProposal }],
},
})
);
});
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@MarketData');
cy.getByTestId(marketSummaryBlock).should('be.visible');
});
it('should display market proposal notification if proposal found', () => {
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('market-proposal-notification').should(
'contain.text',
'Changes have been proposed for this market'
);
cy.getByTestId('market-proposal-notification').within(() => {
cy.getByTestId('external-link').should(
'have.attr',
'href',
`${Cypress.env('VEGA_TOKEN_URL')}/proposals/123`
);
});
});
});
});
@@ -1,216 +0,0 @@
import * as Schema from '@vegaprotocol/types';
const expirtyTooltip = 'expiry-tooltip';
const externalLink = 'external-link';
const itemHeader = 'item-header';
const itemValue = 'item-value';
const link = 'link';
const liquidityLink = 'view-liquidity-link';
const liquiditySupplied = 'liquidity-supplied';
const liquiditySuppliedTooltip = 'liquidity-supplied-tooltip';
const marketChange = 'market-change';
const marketExpiry = 'market-expiry';
const marketMode = 'market-trading-mode';
const marketName = 'header-title';
const marketPrice = 'market-price';
const marketSettlement = 'market-settlement-asset';
const marketState = 'market-state';
const marketSummaryBlock = 'header-summary';
const marketVolume = 'market-volume';
const percentageValue = 'price-change-percentage';
const priceChangeValue = 'price-change';
const tradingModeTooltip = 'trading-mode-tooltip';
describe('Market trading page', () => {
before(() => {
cy.clearAllLocalStorage();
cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0');
cy.wait('@MarketData');
cy.getByTestId(marketSummaryBlock).should('be.visible');
});
describe('Market summary', { tags: '@smoke' }, () => {
// 7002-SORD-001
// 7002-SORD-002
it('must display market name', () => {
// 6002-MDET-001
cy.getByTestId(marketName).should('not.be.empty');
});
it('must see market expiry', () => {
// 6002-MDET-002
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketExpiry).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Expiry');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
it('must see market price', () => {
// 6002-MDET-003
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketPrice).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Mark Price');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
it('must see market change', () => {
// 6002-MDET-004
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketChange).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Change (24h)');
cy.getByTestId(percentageValue).should('not.be.empty');
cy.getByTestId(priceChangeValue).should('not.be.empty');
});
});
});
it('must see market volume', () => {
// 6002-MDET-005
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketVolume).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Volume (24h)');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
it('must see market mode', () => {
// 6002-MDET-006
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketMode).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Trading mode');
cy.getByTestId(itemValue).should(
'have.text',
'Monitoring auction - liquidity (target not met)'
);
});
});
});
it('must see market status', () => {
// 6002-MDET-007
// 7002-SORD-061
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketState).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Status');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
it('must see market settlement', () => {
// 6002-MDET-008
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketSettlement).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Settlement asset');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
it('must see market liquidity supplied', () => {
// 6002-MDET-009
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(liquiditySupplied).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Liquidity supplied');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
});
describe('Market tooltips', { tags: '@smoke' }, () => {
it('should see expiry tooltip', () => {
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketExpiry).within(() => {
cy.getByTestId(itemValue)
.should('have.text', 'Not time-based')
.realHover();
});
});
cy.getByTestId(expirtyTooltip)
.eq(0)
.should(
'contain.text',
'This market expires when triggered by its oracle, not on a set date.'
)
.within(() => {
cy.getByTestId(link)
.should('have.attr', 'href')
.and('include', Cypress.env('EXPLORER_URL'));
});
});
it('should see trading conditions tooltip', () => {
const toolTipLabel = 'tooltip-label';
const toolTipValue = 'tooltip-value';
const auctionToolTipLabels = [
'Auction start',
'Est. auction end',
'Target liquidity',
'Current liquidity',
'Est. uncrossing price',
'Est. uncrossing vol',
];
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketMode).within(() => {
cy.getByTestId(itemValue)
.should('contain.text', 'Monitoring auction')
.and('contain.text', 'liquidity')
.realHover();
});
});
cy.getByTestId(tradingModeTooltip)
.should(
'contain.text',
'This market is in auction until it reaches sufficient liquidity.'
)
.eq(0)
.within(() => {
cy.getByTestId(externalLink)
.should('have.attr', 'href')
.and('include', Cypress.env('TRADING_MODE_LINK'));
for (let i = 0; i < 6; i++) {
cy.getByTestId(toolTipLabel)
.eq(i)
.should('have.text', auctionToolTipLabels[i]);
cy.getByTestId(toolTipValue).eq(i).should('not.be.empty');
}
});
});
it('should see liquidity supplied tooltip', () => {
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(liquiditySupplied).within(() => {
cy.getByTestId(itemValue).realHover();
});
});
cy.getByTestId(liquiditySuppliedTooltip)
.should('contain.text', 'Supplied stake')
.and('contain.text', 'Target stake')
.first()
.within(() => {
cy.getByTestId(liquidityLink).should(
'have.text',
'View liquidity provision table'
);
cy.getByTestId(externalLink).should(
'have.text',
'Learn about providing liquidity'
);
});
});
});
});
@@ -1,103 +0,0 @@
import * as Schema from '@vegaprotocol/types';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { createOrder } from '../support/create-order';
describe(
'vega wallet - prompt',
{ tags: '@regression', testIsolation: true },
() => {
describe('must submit order', { tags: '@smoke' }, () => {
// 7002-SORD-039
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must see a prompt to check connected vega wallet to approve transaction', () => {
// 0003-WTXN-002
cy.mockVegaWalletTransaction(1000);
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_MARKET,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
size: '100',
};
createOrder(order);
cy.getByTestId('toast-content').should(
'contain.text',
'Please go to your Vega wallet application and approve or reject the transaction.'
);
});
it('must show error returned by wallet ', () => {
// 0003-WTXN-009
// 0003-WTXN-011
// 0002-WCON-016
// 0003-WTXN-008
//trigger error from the wallet
cy.intercept('POST', 'http://localhost:1789/api/v2/requests', (req) => {
req.on('response', (res) => {
res.send({
jsonrpc: '2.0',
id: '1',
});
});
});
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_MARKET,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
size: '100',
};
createOrder(order);
cy.getByTestId('toast-content').should(
'contain.text',
'The connection to your Vega Wallet has been lost.'
);
cy.getByTestId('connect-vega-wallet').click();
cy.getByTestId('dialog-content').should('be.visible');
});
it('must see that the order was rejected by the connected wallet', () => {
// 0003-WTXN-007
//trigger rejection error from the wallet
cy.intercept('POST', 'http://localhost:1789/api/v2/requests', (req) => {
req.alias = 'client.send_transaction';
req.reply({
statusCode: 400,
body: {
jsonrpc: '2.0',
error: {
code: 3001,
data: 'the user rejected the wallet connection',
message: 'User error',
},
id: '0',
},
});
});
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_MARKET,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
size: '100',
};
createOrder(order);
cy.getByTestId('toast-content').should(
'contain.text',
'Error occurredthe user rejected the wallet connection'
);
});
});
}
);
@@ -26,18 +26,6 @@ describe('ethereum wallet', { tags: '@smoke', testIsolation: true }, () => {
cy.getByTestId('tab-deposits').should('not.be.empty');
});
it.skip('should see QR code modal for WalletConnect', () => {
// 0004-EWAL-003
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
cy.getByTestId('connect-eth-wallet-btn').click();
cy.getByTestId('web3-connector-list').should('exist');
cy.getByTestId('web3-connector-WalletConnect').click();
// testing if exists rather than visible because of the long loading time
cy.get('#w3m-modal').should('exist');
});
it('able to disconnect eth wallet', () => {
// 0004-EWAL-004
// 0004-EWAL-005
@@ -1,91 +0,0 @@
import {
mockConnectWallet,
mockConnectWalletWithUserError,
} from '@vegaprotocol/cypress';
const connectVegaBtn = 'connect-vega-wallet';
const manageVegaBtn = 'manage-vega-wallet';
const dialogContent = 'dialog-content';
describe('connect vega wallet', { tags: '@smoke', testIsolation: true }, () => {
beforeEach(() => {
// Using portfolio page as it requires vega wallet connection
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/portfolio');
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
});
it('can connect', () => {
// 0002-WCON-002
// 0002-WCON-005
// 0002-WCON-007
// 0002-WCON-009
mockConnectWallet();
cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]')
.click();
cy.wait('@walletReq');
cy.getByTestId(dialogContent).should(
'contain.text',
'Approve the connection from your Vega wallet app.'
);
cy.getByTestId(dialogContent).should('not.exist');
cy.getByTestId(manageVegaBtn).should('exist');
});
it('can not connect', () => {
// 0002-WCON-002
// 0002-WCON-005
// 0002-WCON-007
// 0002-WCON-015
mockConnectWalletWithUserError();
cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]')
.click();
cy.getByTestId('dialog-content')
.should('contain.text', 'User error')
.and('contain.text', 'the user rejected the wallet connection');
});
it('can change selected public key and disconnect', () => {
// 0002-WCON-022
// 0002-WCON-023
// 0002-WCON-025
// 0002-WCON-026
// 0002-WCON-021
// 0002-WCON-027
// 0002-WCON-030
// 0002-WCON-029
// 0002-WCON-008
// 0002-WCON-035
// 0002-WCON-014
// 0002-WCON-010
// 0003-WTXN-004
mockConnectWallet();
const key2 = Cypress.env('VEGA_PUBLIC_KEY2');
const truncatedKey2 = Cypress.env('TRUNCATED_VEGA_PUBLIC_KEY2');
cy.connectVegaWallet();
cy.getByTestId('manage-vega-wallet').click();
cy.getByTestId('keypair-list').should('exist');
cy.getByTestId(`key-${key2}`).should('contain.text', truncatedKey2);
cy.getByTestId(`key-${key2}`)
.find('[data-testid="copy-vega-public-key"]')
.should('be.visible');
cy.get(`[data-testid="key-${key2}"] > .mr-2`).click();
cy.getByTestId('keypair-list')
.find('[data-state="checked"]')
.should('be.visible');
cy.getByTestId('disconnect').click();
cy.getByTestId('connect-vega-wallet').should('exist');
cy.getByTestId('manage-vega-wallet').should('not.exist');
cy.getByTestId('connect-vega-wallet').click();
cy.contains('Enter a custom wallet location');
});
});
+1
View File
@@ -28,3 +28,4 @@ NX_REFERRALS=false
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
NX_DISABLE_CLOSE_POSITION=true
+11 -1
View File
@@ -1,11 +1,21 @@
import { FeesContainer } from '../../components/fees-container';
import { useT } from '../../lib/use-t';
import { usePageTitleStore } from '../../stores';
import { titlefy } from '@vegaprotocol/utils';
import { useEffect } from 'react';
export const Fees = () => {
const t = useT();
const title = t('Fees');
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([title]));
}, [updateTitle, title]);
return (
<div className="container p-4 mx-auto">
<h1 className="px-4 pb-4 text-2xl">{t('Fees')}</h1>
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
<FeesContainer />
</div>
);
+2 -25
View File
@@ -1,34 +1,11 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { useGlobalStore } from '../../stores';
import { useTopTradedMarkets } from '../../lib/hooks/use-top-traded-markets';
import { Links } from '../../lib/links';
import { useNavigateToLastMarket } from '../../lib/hooks/use-navigate-to-last-market';
// The home pages only purpose is to redirect to the users last market,
// the top traded if they are new, or fall back to the list of markets.
// Thats why we just render a loader here
export const Home = () => {
const navigate = useNavigate();
const { data } = useTopTradedMarkets();
const marketId = useGlobalStore((store) => store.marketId);
useEffect(() => {
if (marketId) {
navigate(Links.MARKET(marketId), {
replace: true,
});
} else if (data) {
const marketDataId = data[0]?.id;
if (marketDataId) {
navigate(Links.MARKET(marketDataId), {
replace: true,
});
} else {
navigate(Links.MARKETS());
}
}
}, [marketId, data, navigate]);
useNavigateToLastMarket();
return (
<Splash>
@@ -1,7 +1,6 @@
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { DocsLinks, useEnvironment } from '@vegaprotocol/environment';
import { ButtonLink, ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
import { MarketProposalNotification } from '@vegaprotocol/proposals';
import type { Market } from '@vegaprotocol/markets';
import {
addDecimalsFormatNumber,
@@ -145,7 +144,6 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
/>
</HeaderStat>
)}
<MarketProposalNotification marketId={market.id} />
</>
);
};
+5 -4
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useMemo } from 'react';
import { addDecimalsFormatNumber, titlefy } from '@vegaprotocol/utils';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
import { ExternalLink, Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { Link, Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { getAsset, marketDataProvider, useMarket } from '@vegaprotocol/markets';
import { useGlobalStore, usePageTitleStore } from '../../stores';
import { TradeGrid } from './trade-grid';
@@ -114,15 +114,16 @@ export const MarketPage = () => {
</p>
<p className="justify-center text-sm">
<Trans
defaults="Please choose another market from the <0>market list<0>"
defaults="Please choose another market from the <0>market list</0>"
ns={ns}
components={[
<ExternalLink
<Link
className="underline underline-offset-4 "
onClick={() => navigate(Links.MARKETS())}
key="link"
>
market list
</ExternalLink>,
</Link>,
]}
/>
</p>
@@ -1,7 +1,14 @@
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import { AgGrid, PriceFlashCell } from '@vegaprotocol/datagrid';
import {
AgGrid,
PriceFlashCell,
useDataGridEvents,
} from '@vegaprotocol/datagrid';
import type { MarketMaybeWithData } from '@vegaprotocol/markets';
import { useColumnDefs } from './use-column-defs';
import type { DataGridStore } from '../../stores/datagrid-store-slice';
import { type StateCreator, create } from 'zustand';
import { persist } from 'zustand/middleware';
export const getRowId = ({ data }: { data: { id: string } }) => data.id;
@@ -18,8 +25,37 @@ const components = {
type Props = TypedDataAgGrid<MarketMaybeWithData>;
export type DataGridSlice = {
gridStore: DataGridStore;
updateGridStore: (gridStore: DataGridStore) => void;
};
export const createDataGridSlice: StateCreator<DataGridSlice> = (set) => ({
gridStore: {},
updateGridStore: (newStore) => {
set((curr) => ({
gridStore: {
...curr.gridStore,
...newStore,
},
}));
},
});
const useMarketsStore = create<DataGridSlice>()(
persist(createDataGridSlice, {
name: 'vega_market_list_store',
})
);
export const MarketListTable = (props: Props) => {
const columnDefs = useColumnDefs();
const gridStore = useMarketsStore((store) => store.gridStore);
const updateGridStore = useMarketsStore((store) => store.updateGridStore);
const gridStoreCallbacks = useDataGridEvents(gridStore, (colState) => {
updateGridStore(colState);
});
return (
<AgGrid
@@ -28,6 +64,7 @@ export const MarketListTable = (props: Props) => {
columnDefs={columnDefs}
components={components}
rowHeight={45}
{...gridStoreCallbacks}
{...props}
/>
);
@@ -43,7 +43,7 @@ export const OpenMarkets = () => {
if (!data) return;
// prevent navigating to the market page if any of the below cells are clicked
// event.preventDefault or event.stopPropagation dont seem to apply for aggird
// event.preventDefault or event.stopPropagation do not seem to apply for ag-grid
const colId = column.getColId();
if (
@@ -51,6 +51,29 @@ export const useColumnDefs = () => {
headerName: t('Description'),
field: 'tradableInstrument.instrument.name',
},
{
headerName: t('Settlement asset'),
field: 'tradableInstrument.instrument.product.settlementAsset.symbol',
cellRenderer: ({
data,
}: VegaICellRendererParams<
MarketMaybeWithData,
'tradableInstrument.instrument.product.settlementAsset.symbol'
>) => {
const value = data && getAsset(data);
return value ? (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value.id, e.target as HTMLElement);
}}
>
{value.symbol}
</ButtonLink>
) : (
''
);
},
},
{
headerName: t('Trading mode'),
field: 'tradingMode',
@@ -142,27 +165,21 @@ export const useColumnDefs = () => {
},
},
{
headerName: t('Settlement asset'),
field: 'tradableInstrument.instrument.product.settlementAsset.symbol',
cellRenderer: ({
headerName: t('Open Interest'),
field: 'data.openInterest',
type: 'rightAligned',
valueFormatter: ({
data,
}: VegaICellRendererParams<
}: VegaValueFormatterParams<
MarketMaybeWithData,
'tradableInstrument.instrument.product.settlementAsset.symbol'
>) => {
const value = data && getAsset(data);
return value ? (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value.id, e.target as HTMLElement);
}}
>
{value.symbol}
</ButtonLink>
) : (
''
);
},
'data.openInterest'
>) =>
data?.data?.openInterest === undefined
? '-'
: addDecimalsFormatNumber(
data?.data?.openInterest,
data?.positionDecimalPlaces
),
},
{
headerName: t('Spread'),
@@ -14,6 +14,7 @@ import { PositionsMenu } from '../../components/positions-menu';
import { WithdrawalsContainer } from '../../components/withdrawals-container';
import { OrdersContainer } from '../../components/orders-container';
import { LedgerContainer } from '../../components/ledger-container';
import { RewardsContainer } from '../../components/rewards-container';
import {
ResizableGrid,
ResizableGridPanel,
@@ -86,6 +87,9 @@ export const Portfolio = () => {
<Tab id="ledger-entries" name={t('Ledger entries')}>
<LedgerContainer />
</Tab>
<Tab id="rewards" name={t('Rewards')}>
<RewardsContainer />
</Tab>
</Tabs>
</PortfolioGridChild>
</ResizableGridPanel>
@@ -10,7 +10,7 @@ import { useForm } from 'react-hook-form';
import classNames from 'classnames';
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { RainbowButton } from './buttons';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { useReferral } from './hooks/use-referral';
@@ -32,6 +32,19 @@ const validateCode = (value: string, t: ReturnType<typeof useT>) => {
return true;
};
export const ApplyCodeFormContainer = () => {
const { pubKey } = useVegaWallet();
const { data: referee } = useReferral({ pubKey, role: 'referee' });
const { data: referrer } = useReferral({ pubKey, role: 'referrer' });
// go to main page if the current pubkey is already a referrer or referee
if (referee || referrer) {
return <Navigate to={Routes.REFERRALS} />;
}
return <ApplyCodeForm />;
};
export const ApplyCodeForm = () => {
const t = useT();
const program = useReferralProgram();
@@ -55,14 +68,29 @@ export const ApplyCodeForm = () => {
} = useForm();
const [params] = useSearchParams();
const { data: referee } = useReferral({ pubKey, role: 'referee' });
const { data: referrer } = useReferral({ pubKey, role: 'referrer' });
const codeField = watch('code');
const { data: previewData, loading: previewLoading } = useReferral({
code: validateCode(codeField, t) ? codeField : undefined,
});
/**
* Validates the set a user tries to apply to.
*/
const validateSet = useCallback(() => {
if (
codeField &&
!previewLoading &&
previewData &&
!previewData.isEligible
) {
return t('The code is no longer valid.');
}
if (codeField && !previewLoading && !previewData) {
return t('The code is invalid');
}
return true;
}, [codeField, previewData, previewLoading, t]);
useEffect(() => {
const code = params.get('code');
if (code) setValue('code', code);
@@ -144,16 +172,11 @@ export const ApplyCodeForm = () => {
}
}, [navigate, status]);
// go to main page if the current pubkey is already a referrer or referee
if (referee || referrer) {
return <Navigate to={Routes.REFERRALS} />;
}
// show "code applied" message when successfully applied
if (status === 'successful') {
return (
<div className="w-1/2 mx-auto">
<h3 className="mb-5 text-xl text-center uppercase calt flex flex-row gap-2 justify-center items-center">
<div className="mx-auto w-1/2">
<h3 className="calt mb-5 flex flex-row items-center justify-center gap-2 text-center text-xl uppercase">
<span className="text-vega-green-500">
<VegaIcon name={VegaIconNames.TICK} size={20} />
</span>{' '}
@@ -205,15 +228,18 @@ export const ApplyCodeForm = () => {
return (
<>
<div className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg">
<h3 className="mb-4 text-2xl text-center calt">
<div
data-testid="referral-apply-code-form"
className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto w-2/3 max-w-md rounded-lg p-8"
>
<h3 className="calt mb-4 text-center text-2xl">
{t('Apply a referral code')}
</h3>
<p className="mb-4 text-center text-base">
{t('Enter a referral code to get trading discounts.')}
</p>
<form
className={classNames('w-full flex flex-col gap-4', {
className={classNames('flex w-full flex-col gap-4', {
'animate-shake': Boolean(errors.code),
})}
onSubmit={handleSubmit(onSubmit)}
@@ -224,31 +250,37 @@ export const ApplyCodeForm = () => {
hasError={Boolean(errors.code)}
{...register('code', {
required: t('You have to provide a code to apply it.'),
validate: (value) => validateCode(value, t),
validate: (value) => {
const err = validateCode(value, t);
if (err !== true) return err;
return validateSet();
},
})}
placeholder="Enter a code"
className="mb-2 bg-vega-clight-900 dark:bg-vega-cdark-700"
className="bg-vega-clight-900 dark:bg-vega-cdark-700 mb-2"
/>
</label>
<RainbowButton variant="border" {...getButtonProps()} />
</form>
{errors.code && (
<InputError className="break-words overflow-auto">
<InputError className="overflow-auto break-words">
{errors.code.message?.toString()}
</InputError>
)}
</div>
{previewLoading && !previewData ? (
{validateCode(codeField, t) === true && previewLoading && !previewData ? (
<div className="mt-10">
<Loader />
</div>
) : null}
{previewData ? (
{/* TODO: Re-check plural forms once i18n is updated */}
{previewData && previewData.isEligible ? (
<div className="mt-10">
<h2 className="text-2xl mb-5">
<h2 className="mb-5 text-2xl">
{t(
'You are joining the group shown, but will not have access to benefits until you have completed at least %s epochs.',
[nextBenefitTierEpochsValue.toString()]
'youAreJoiningTheGroup',
'You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.',
{ count: nextBenefitTierEpochsValue }
)}
</h2>
<Statistics data={previewData} program={program} as="referee" />
@@ -39,7 +39,10 @@ export const CreateCodeForm = () => {
const { pubKey, isReadOnly } = useVegaWallet();
return (
<div className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg">
<div
data-testid="referral-create-code-form"
className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg"
>
<h3 className="mb-4 text-2xl text-center calt">
{t('Create a referral code')}
</h3>
@@ -95,6 +98,11 @@ const CreateCodeDialog = ({
const { stakeAvailable: currentStakeAvailable, requiredStake } =
useStakeAvailable();
const { data: referralSets } = useReferral({
pubKey,
role: 'referrer',
});
const onSubmit = () => {
if (isReadOnly || !pubKey) {
setErr('Not connected');
@@ -193,6 +201,68 @@ const CreateCodeDialog = ({
);
}
if (!referralSets) {
return (
<div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && (
<>
{
<p>
{t(
'There is currently no referral program active, are you sure you want to create a code?'
)}
</p>
}
</>
)}
{status === 'success' && code && (
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700">
<p className="overflow-hidden whitespace-nowrap text-ellipsis">
{code}
</p>
</div>
<CopyWithTooltip text={code}>
<TradingButton
className="text-sm no-underline"
icon={<VegaIcon name={VegaIconNames.COPY} />}
>
<span>{t('Copy')}</span>
</TradingButton>
</CopyWithTooltip>
</div>
)}
<TradingButton
fill={true}
intent={Intent.Primary}
onClick={() => onSubmit()}
{...getButtonProps()}
></TradingButton>
{status === 'idle' && (
<TradingButton
fill={true}
intent={Intent.Primary}
onClick={() => {
refetch();
setDialogOpen(false);
}}
>
{t('No')}
</TradingButton>
)}
{err && <InputError>{err}</InputError>}
<div className="flex justify-center pt-5 mt-2 text-sm border-t gap-4 text-default border-default">
<ExternalLink href={ABOUT_REFERRAL_DOCS_LINK}>
{t('About the referral program')}
</ExternalLink>
<ExternalLink href={DISCLAIMER_REFERRAL_DOCS_LINK}>
{t('Disclaimer')}
</ExternalLink>
</div>
</div>
);
}
return (
<div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && (
@@ -11,6 +11,7 @@ query ReferralSetStats($code: ID!, $epoch: Int) {
rewardsMultiplier
rewardsFactorMultiplier
referrerTakerVolume
wasEligible
}
}
}
@@ -0,0 +1,10 @@
query StakeAvailable($partyId: ID!) {
party(id: $partyId) {
stakingSummary {
currentStakeAvailable
}
}
networkParameter(key: "referralProgram.minStakedVegaTokens") {
value
}
}
@@ -9,7 +9,7 @@ export type ReferralSetStatsQueryVariables = Types.Exact<{
}>;
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string, referrerTakerVolume: string } } | null> } };
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string, referrerTakerVolume: string, wasEligible: boolean } } | null> } };
export const ReferralSetStatsDocument = gql`
@@ -26,6 +26,7 @@ export const ReferralSetStatsDocument = gql`
rewardsMultiplier
rewardsFactorMultiplier
referrerTakerVolume
wasEligible
}
}
}
@@ -0,0 +1,53 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type StakeAvailableQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type StakeAvailableQuery = { __typename?: 'Query', party?: { __typename?: 'Party', stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string } } | null, networkParameter?: { __typename?: 'NetworkParameter', value: string } | null };
export const StakeAvailableDocument = gql`
query StakeAvailable($partyId: ID!) {
party(id: $partyId) {
stakingSummary {
currentStakeAvailable
}
}
networkParameter(key: "referralProgram.minStakedVegaTokens") {
value
}
}
`;
/**
* __useStakeAvailableQuery__
*
* To run a query within a React component, call `useStakeAvailableQuery` and pass it any options that fit your needs.
* When your component renders, `useStakeAvailableQuery` 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 } = useStakeAvailableQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function useStakeAvailableQuery(baseOptions: Apollo.QueryHookOptions<StakeAvailableQuery, StakeAvailableQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<StakeAvailableQuery, StakeAvailableQueryVariables>(StakeAvailableDocument, options);
}
export function useStakeAvailableLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<StakeAvailableQuery, StakeAvailableQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<StakeAvailableQuery, StakeAvailableQueryVariables>(StakeAvailableDocument, options);
}
export type StakeAvailableQueryHookResult = ReturnType<typeof useStakeAvailableQuery>;
export type StakeAvailableLazyQueryHookResult = ReturnType<typeof useStakeAvailableLazyQuery>;
export type StakeAvailableQueryResult = Apollo.QueryResult<StakeAvailableQuery, StakeAvailableQueryVariables>;
@@ -75,22 +75,20 @@ export const useReferralProgram = () => {
const benefitTiers = sortBy(data.currentReferralProgram.benefitTiers, (t) =>
Number(t.referralRewardFactor)
)
.reverse()
.map((t, i) => {
return {
tier: i + 1,
rewardFactor: Number(t.referralRewardFactor),
commission: Number(t.referralRewardFactor) * 100 + '%',
discountFactor: Number(t.referralDiscountFactor),
discount: Number(t.referralDiscountFactor) * 100 + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: getNumberFormat(0).format(
Number(t.minimumRunningNotionalTakerVolume)
),
epochs: Number(t.minimumEpochs),
};
});
).map((t, i) => {
return {
tier: i + 1, // sorted in asc order, hence first is the lowest tier
rewardFactor: Number(t.referralRewardFactor),
commission: Number(t.referralRewardFactor) * 100 + '%',
discountFactor: Number(t.referralDiscountFactor),
discount: Number(t.referralDiscountFactor) * 100 + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: getNumberFormat(0).format(
Number(t.minimumRunningNotionalTakerVolume)
),
epochs: Number(t.minimumEpochs),
};
});
const stakingTiers = sortBy(
data.currentReferralProgram.stakingTiers,
@@ -0,0 +1,111 @@
import {
Intent,
type Toast,
useToasts,
ToastHeading,
Button,
} from '@vegaprotocol/ui-toolkit';
import { useReferral } from './use-referral';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useEffect } from 'react';
import { useT } from '../../../lib/use-t';
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
import { Routes } from '../../../lib/links';
import { useCurrentEpochInfoQuery } from './__generated__/Epoch';
const REFETCH_INTERVAL = 60 * 60 * 1000; // 1h
const NON_ELIGIBLE_REFERRAL_SET_TOAST_ID = 'non-eligible-referral-set';
const useNonEligibleReferralSet = () => {
const { pubKey } = useVegaWallet();
const { data, loading, refetch } = useReferral({ pubKey, role: 'referee' });
const {
data: epochData,
loading: epochLoading,
refetch: epochRefetch,
} = useCurrentEpochInfoQuery();
useEffect(() => {
const interval = setInterval(() => {
refetch();
epochRefetch();
}, REFETCH_INTERVAL);
return () => {
clearInterval(interval);
};
}, [epochRefetch, refetch]);
return { data, epoch: epochData?.epoch.id, loading: loading || epochLoading };
};
export const useReferralToasts = () => {
const navigate = useNavigate();
const { pathname } = useLocation();
const t = useT();
const [setToast, hasToast, updateToast] = useToasts((store) => [
store.setToast,
store.hasToast,
store.update,
]);
const { data, epoch, loading } = useNonEligibleReferralSet();
useEffect(() => {
if (
data &&
epoch &&
!loading &&
!data.isEligible &&
!hasToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch)
) {
const nonEligibleReferralToast: Toast = {
id: NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch,
intent: Intent.Warning,
content: (
<>
<ToastHeading>{t('Referral code no longer valid')}</ToastHeading>
<p>
{t(
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements.'
)}
</p>
<p className="mt-2">
<Button
data-testid="toast-apply-code"
size="xs"
onClick={() => {
const matched = matchPath(
Routes.REFERRALS_APPLY_CODE,
pathname
);
if (!matched) navigate(Routes.REFERRALS_APPLY_CODE);
updateToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch, {
hidden: true,
});
}}
>
{t('Apply a new code')}
</Button>
</p>
</>
),
onClose: () =>
updateToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch, {
hidden: true,
}),
};
setToast(nonEligibleReferralToast);
}
}, [
data,
epoch,
hasToast,
loading,
navigate,
pathname,
setToast,
t,
updateToast,
]);
};
@@ -4,6 +4,7 @@ import { useRefereesQuery } from './__generated__/Referees';
import compact from 'lodash/compact';
import type { ReferralSetsQueryVariables } from './__generated__/ReferralSets';
import { useReferralSetsQuery } from './__generated__/ReferralSets';
import { useStakeAvailable } from './use-stake-available';
export const DEFAULT_AGGREGATION_DAYS = 30;
@@ -62,6 +63,8 @@ export const useReferral = (args: UseReferralArgs) => {
? referralData.referralSets.edges[0]?.node
: undefined;
const { isEligible } = useStakeAvailable(referralSet?.referrer);
const {
data: refereesData,
loading: refereesLoading,
@@ -103,6 +106,7 @@ export const useReferral = (args: UseReferralArgs) => {
referee: referee,
referrerId: referralSet.referrer,
createdAt: referralSet.createdAt,
isEligible,
referees,
}
: undefined;
@@ -1,34 +1,35 @@
import { gql, useQuery } from '@apollo/client';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useStakeAvailableQuery } from './__generated__/StakeAvailable';
const STAKE_QUERY = gql`
query CreateCode($partyId: ID!) {
party(id: $partyId) {
stakingSummary {
currentStakeAvailable
}
}
networkParameter(key: "referralProgram.minStakedVegaTokens") {
value
}
}
`;
export const useStakeAvailable = () => {
const { pubKey } = useVegaWallet();
const { data } = useQuery(STAKE_QUERY, {
variables: { partyId: pubKey || '' },
skip: !pubKey,
/**
* Gets the current stake available for given public key and required stake for
* the referral program.
*
* (Uses currently connected public key if left empty)
*/
export const useStakeAvailable = (pubKey?: string) => {
const { pubKey: currentPubKey } = useVegaWallet();
const partyId = pubKey || currentPubKey;
const { data } = useStakeAvailableQuery({
variables: { partyId: partyId || '' },
skip: !partyId,
// TODO: remove when network params available
errorPolicy: 'ignore',
});
const stakeAvailable = data
? BigInt(data.party?.stakingSummary.currentStakeAvailable || '0')
: undefined;
const requiredStake = data
? BigInt(data.networkParameter?.value || '0')
: undefined;
return {
stakeAvailable: data
? BigInt(data.party?.stakingSummary.currentStakeAvailable || '0')
: undefined,
requiredStake: data
? BigInt(data.networkParameter?.value || '0')
: undefined,
stakeAvailable,
requiredStake,
isEligible:
stakeAvailable != null &&
requiredStake != null &&
stakeAvailable >= requiredStake,
};
};
@@ -0,0 +1,374 @@
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
import { render, waitFor } from '@testing-library/react';
import { type VegaWalletContextShape } from '@vegaprotocol/wallet';
import { ReferralStatistics } from './referral-statistics';
import {
ReferralProgramDocument,
type ReferralProgramQuery,
} from './hooks/__generated__/CurrentReferralProgram';
import {
ReferralSetsDocument,
type ReferralSetsQueryVariables,
type ReferralSetsQuery,
} from './hooks/__generated__/ReferralSets';
import {
StakeAvailableDocument,
type StakeAvailableQueryVariables,
type StakeAvailableQuery,
} from './hooks/__generated__/StakeAvailable';
import {
RefereesDocument,
type RefereesQueryVariables,
type RefereesQuery,
} from './hooks/__generated__/Referees';
import { MemoryRouter } from 'react-router-dom';
const MOCK_PUBKEY =
'1234567890123456789012345678901234567890123456789012345678901234';
const MOCK_STAKE_AVAILABLE: StakeAvailableQuery = {
networkParameter: {
__typename: 'NetworkParameter',
value: '1',
},
party: {
__typename: 'Party',
stakingSummary: {
__typename: 'StakingSummary',
currentStakeAvailable: '1',
},
},
};
const MOCK_NON_ELIGIBILE_STAKE_AVAILABLE: StakeAvailableQuery = {
networkParameter: {
__typename: 'NetworkParameter',
value: '1',
},
party: {
__typename: 'Party',
stakingSummary: {
__typename: 'StakingSummary',
currentStakeAvailable: '0',
},
},
};
const MOCK_REFERRAL_PROGRAM: ReferralProgramQuery = {
currentReferralProgram: {
__typename: 'CurrentReferralProgram',
benefitTiers: [
{
__typename: 'BenefitTier',
minimumEpochs: 1,
minimumRunningNotionalTakerVolume: '0',
referralDiscountFactor: '0.01',
referralRewardFactor: '0.01',
},
{
__typename: 'BenefitTier',
minimumEpochs: 2,
minimumRunningNotionalTakerVolume: '10',
referralDiscountFactor: '0.02',
referralRewardFactor: '0.02',
},
],
endOfProgramTimestamp: '202411012023-11-26T05:58:24.045158Z',
id: '123',
stakingTiers: [
{
__typename: 'StakingTier',
minimumStakedTokens: '100',
referralRewardMultiplier: '1',
},
{
__typename: 'StakingTier',
minimumStakedTokens: '1000',
referralRewardMultiplier: '2',
},
],
version: 2,
windowLength: 3,
endedAt: null,
},
};
const MOCK_REFERRER_SET: ReferralSetsQuery = {
referralSets: {
__typename: 'ReferralSetConnection',
edges: [
{
__typename: 'ReferralSetEdge',
node: {
__typename: 'ReferralSet',
createdAt: '2023-11-26T05:58:24.045158Z',
id: '3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
referrer: MOCK_PUBKEY,
updatedAt: '2023-11-26T05:58:24.045158Z',
},
},
],
},
};
const MOCK_REFERREE_SET: ReferralSetsQuery = {
referralSets: {
__typename: 'ReferralSetConnection',
edges: [
{
__typename: 'ReferralSetEdge',
node: {
__typename: 'ReferralSet',
createdAt: '2023-11-26T05:58:24.045158Z',
id: '3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
referrer:
'1111111111111111111111111111111111111111111111111111111111111111',
updatedAt: '2023-11-26T05:58:24.045158Z',
},
},
],
},
};
const MOCK_REFEREES: RefereesQuery = {
referralSetReferees: {
__typename: 'ReferralSetRefereeConnection',
edges: [
{
node: {
atEpoch: 1,
joinedAt: '2023-11-21T14:17:09.257235Z',
refereeId:
'0987654321098765432109876543210987654321098765432109876543219876',
referralSetId:
'3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
totalRefereeGeneratedRewards: '1234',
totalRefereeNotionalTakerVolume: '5678',
__typename: 'ReferralSetReferee',
},
},
],
},
};
const programMock: MockedResponse<ReferralProgramQuery> = {
request: {
query: ReferralProgramDocument,
},
result: { data: MOCK_REFERRAL_PROGRAM },
};
const referralSetAsReferrerMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referrer: MOCK_PUBKEY,
},
},
result: {
data: MOCK_REFERRER_SET,
},
};
const noReferralSetAsReferrerMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referrer: MOCK_PUBKEY,
},
},
result: {
data: { referralSets: { edges: [] } },
},
};
const referralSetAsRefereeMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referee: MOCK_PUBKEY,
},
},
result: {
data: MOCK_REFERREE_SET,
},
};
const noReferralSetAsRefereeMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referee: MOCK_PUBKEY,
},
},
result: {
data: { referralSets: { edges: [] } },
},
};
const stakeAvailableMock: MockedResponse<
StakeAvailableQuery,
StakeAvailableQueryVariables
> = {
request: {
query: StakeAvailableDocument,
variables: {
partyId: MOCK_PUBKEY,
},
},
result: {
data: MOCK_STAKE_AVAILABLE,
},
};
const nonEligibleStakeAvailableMock: MockedResponse<
StakeAvailableQuery,
StakeAvailableQueryVariables
> = {
request: {
query: StakeAvailableDocument,
variables: {
partyId: MOCK_PUBKEY,
},
},
result: {
data: MOCK_NON_ELIGIBILE_STAKE_AVAILABLE,
},
};
const refereesMock: MockedResponse<RefereesQuery, RefereesQueryVariables> = {
request: {
query: RefereesDocument,
variables: {
code: MOCK_REFERRER_SET.referralSets.edges[0]?.node.id as string,
aggregationEpochs:
MOCK_REFERRAL_PROGRAM.currentReferralProgram?.windowLength,
},
},
result: {
data: MOCK_REFEREES,
},
};
jest.mock('@vegaprotocol/wallet', () => {
return {
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: () => {
const ctx: Partial<VegaWalletContextShape> = {
pubKey: MOCK_PUBKEY,
};
return ctx;
},
};
});
describe('ReferralStatistics', () => {
it('displays create code when no data has been found for given pubkey', () => {
const { queryByTestId } = render(
<MockedProvider mocks={[]} showWarnings={false}>
<ReferralStatistics />
</MockedProvider>
);
expect(queryByTestId('referral-create-code-form')).toBeInTheDocument();
});
it('displays referrer stats when given pubkey is a referrer', async () => {
const { queryByTestId } = render(
<MockedProvider
mocks={[
programMock,
referralSetAsReferrerMock,
noReferralSetAsRefereeMock,
stakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
);
await waitFor(() => {
expect(
queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referrer'
);
});
});
it('displays referee stats when given pubkey is a referee', async () => {
const { queryByTestId } = render(
<MemoryRouter>
<MockedProvider
mocks={[
programMock,
noReferralSetAsReferrerMock,
referralSetAsRefereeMock,
stakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
await waitFor(() => {
expect(
queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee'
);
});
});
it('displays eligibility warning when the set is no longer valid due to the referrers stake', async () => {
const { queryByTestId } = render(
<MemoryRouter>
<MockedProvider
mocks={[
programMock,
noReferralSetAsReferrerMock,
referralSetAsRefereeMock,
nonEligibleStakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
await waitFor(() => {
expect(
queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee'
);
expect(queryByTestId('referral-eligibility-warning')).toBeInTheDocument();
expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument();
});
});
});
@@ -1,3 +1,4 @@
import minBy from 'lodash/minBy';
import { CodeTile, StatTile } from './tile';
import {
VegaIcon,
@@ -28,10 +29,10 @@ import sortBy from 'lodash/sortBy';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
import BigNumber from 'bignumber.js';
import maxBy from 'lodash/maxBy';
import { DocsLinks } from '@vegaprotocol/environment';
import { useT, ns } from '../../lib/use-t';
import { Trans } from 'react-i18next';
import { ApplyCodeForm } from './apply-code-form';
export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet();
@@ -50,11 +51,21 @@ export const ReferralStatistics = () => {
});
if (referee?.code) {
return <Statistics data={referee} program={program} as="referee" />;
return (
<>
<Statistics data={referee} program={program} as="referee" />;
{!referee.isEligible && <ApplyCodeForm />}
</>
);
}
if (referrer?.code) {
return <Statistics data={referrer} program={program} as="referrer" />;
return (
<>
<Statistics data={referrer} program={program} as="referrer" />;
<RefereesTable data={referrer} program={program} />
</>
);
}
return <CreateCodeContainer />;
@@ -116,8 +127,8 @@ export const useStats = ({
t.discountFactor === discountFactorValue
);
const nextBenefitTierValue = currentBenefitTierValue
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1)
: maxBy(benefitTiers, (bt) => bt.tier); // max tier number is lowest tier
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier + 1)
: minBy(benefitTiers, (bt) => bt.tier); // min tier number is lowest tier
const epochsValue =
!isNaN(currentEpoch) && refereeInfo?.atEpoch
? currentEpoch - refereeInfo?.atEpoch
@@ -174,7 +185,7 @@ export const Statistics = ({
const { benefitTiers } = useReferralProgram();
const { stakeAvailable } = useStakeAvailable();
const { stakeAvailable, isEligible } = useStakeAvailable();
const { details } = program;
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
@@ -200,12 +211,24 @@ export const Statistics = ({
{baseCommissionValue * 100}%
</StatTile>
);
const stakingMultiplierTile = (
<StatTile
title={t('Staking multiplier')}
description={t('{{amount}} $VEGA staked', {
amount: addDecimalsFormatNumber(stakeAvailable?.toString() || 0, 18),
})}
description={
<span
className={classNames({
'text-vega-red': !isEligible,
})}
>
{t('{{amount}} $VEGA staked', {
amount: addDecimalsFormatNumber(
stakeAvailable?.toString() || 0,
18
),
})}
</span>
}
>
{multiplier || t('None')}
</StatTile>
@@ -238,7 +261,7 @@ export const Statistics = ({
const referrerVolumeTile = (
<StatTile
title={t('My volume (last {{count}} epochs)', {
title={t('myVolume', 'My volume (last {{count}} epochs)', {
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
})}
>
@@ -251,7 +274,7 @@ export const Statistics = ({
.reduce((all, r) => all.plus(r), new BigNumber(0));
const totalCommissionTile = (
<StatTile
title={t('Total commission (last {{count}}} epochs)', {
title={t('totalCommission', 'Total commission (last {{count}}} epochs)', {
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
})}
description={<QUSDTooltip />}
@@ -294,9 +317,13 @@ export const Statistics = ({
);
const runningVolumeTile = (
<StatTile
title={t('Combined volume (last {{count}} epochs)', {
count: details?.windowLength,
})}
title={t(
'runningNotionalOverEpochs',
'Combined volume (last {{count}} epochs)',
{
count: details?.windowLength,
}
)}
>
{compactNumFormat.format(runningVolumeValue)}
</StatTile>
@@ -333,28 +360,60 @@ export const Statistics = ({
</>
);
const [collapsed, setCollapsed] = useState(false);
const tableRef = useRef<HTMLTableElement>(null);
useLayoutEffect(() => {
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
setCollapsed(true);
}
}, []);
const eligibilityWarning = as === 'referee' && !isEligible && (
<div
data-testid="referral-eligibility-warning"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
>
<h2 className="text-2xl mb-2">{t('Referral code no longer valid')}</h2>
<p>
{t(
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
)}
</p>
</div>
);
return (
<>
{/* Stats tiles */}
<div
data-testid="referral-statistics"
data-as={as}
className="relative mx-auto mb-20"
>
<div
className={classNames(
'grid grid-cols-1 grid-rows-1 gap-5 mx-auto mb-20'
)}
className={classNames('grid grid-cols-1 grid-rows-1 gap-5', {
'opacity-20 pointer-events-none': as === 'referee' && !isEligible,
})}
>
{as === 'referrer' && referrerTiles}
{as === 'referee' && refereeTiles}
</div>
{eligibilityWarning}
</div>
);
};
export const RefereesTable = ({
data,
program,
}: {
data: NonNullable<ReturnType<typeof useReferral>['data']>;
program: ReturnType<typeof useReferralProgram>;
}) => {
const t = useT();
const [collapsed, setCollapsed] = useState(false);
const tableRef = useRef<HTMLTableElement>(null);
const { details } = program;
useLayoutEffect(() => {
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
setCollapsed(true);
}
}, []);
return (
<>
{/* Referees (only for referrer view) */}
{as === 'referrer' && data.referees.length > 0 && (
{data.referees.length > 0 && (
<div className="mt-20 mb-20">
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
<div
@@ -384,15 +443,19 @@ export const Statistics = ({
{ name: 'joined', displayName: t('Date Joined') },
{
name: 'volume',
displayName: t('Volume (last {{count}} epochs)', {
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
}),
displayName: t(
'volumeLastEpochs',
'Volume (last {{count}} epochs)',
{
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
}
),
},
{
name: 'commission',
displayName: (
<Trans
i18nKey="referral-statistics-commission"
i18nKey="referralStatisticsCommission"
defaults="Commission earned in <0>qUSD</0> (last {{count}} epochs)"
values={{
count:
+27 -9
View File
@@ -6,7 +6,12 @@ import { BORDER_COLOR, GRADIENT } from './constants';
import { Tag } from './tag';
import type { ComponentProps, ReactNode } from 'react';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { DApp, TOKEN_PROPOSALS, useLinks } from '@vegaprotocol/environment';
import {
DApp,
DocsLinks,
TOKEN_PROPOSALS,
useLinks,
} from '@vegaprotocol/environment';
import { useT, ns } from '../../lib/use-t';
import { Trans } from 'react-i18next';
@@ -88,10 +93,19 @@ export const TiersContainer = () => {
return (
<div className="text-base px-5 py-10 text-center">
<Trans
defaults="We're sorry but we don't have an active referral programme currently running. You can propose a new programme <0>here</0>."
defaults="There are currently no active referral programs. Check the <0>Governance App</0> to see if there are any proposals in progress and vote."
components={[
<ExternalLink href={governanceLink(TOKEN_PROPOSALS)} key="link">
{t('here')}
{t('Governance App')}
</ExternalLink>,
]}
ns={ns}
/>
<Trans
defaults="You can propose a new program via the <0>Docs</0>."
components={[
<ExternalLink href={DocsLinks?.REFERRALS} key="link">
{t('Docs')}
</ExternalLink>,
]}
ns={ns}
@@ -194,9 +208,13 @@ const TiersTable = ({
{ name: 'discount', displayName: t('Referrer trading discount') },
{
name: 'volume',
displayName: t('Min. trading volume (last {{count}} epochs)', {
count: windowLength,
}),
displayName: t(
'minTradingVolume',
'Min. trading volume (last {{count}} epochs)',
{
count: windowLength,
}
),
},
{ name: 'epochs', displayName: t('Min. epochs') },
]}
@@ -204,13 +222,13 @@ const TiersTable = ({
...d,
className: classNames({
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
d.tier === 1,
d.tier >= 3,
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
d.tier === 2,
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
d.tier === 3,
d.tier === 1,
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
d.tier > 3,
d.tier == 0,
}),
}))}
/>
+26 -2
View File
@@ -8,6 +8,9 @@ import classNames from 'classnames';
import type { HTMLAttributes, ReactNode } from 'react';
import { Button } from './buttons';
import { useT } from '../../lib/use-t';
import { Routes } from '../../lib/links';
import { DApp, useLinks } from '@vegaprotocol/environment';
import truncate from 'lodash/truncate';
export const Tile = ({
className,
@@ -63,6 +66,10 @@ export const CodeTile = ({
className?: string;
}) => {
const t = useT();
const consoleLink = useLinks(DApp.Console);
const applyCodeLink = consoleLink(
`#${Routes.REFERRALS_APPLY_CODE}?code=${code}`
);
return (
<StatTile
title={t('Your referral code')}
@@ -89,10 +96,27 @@ export const CodeTile = ({
{code}
</div>
</Tooltip>
<CopyWithTooltip text={code}>
<CopyWithTooltip text={code} description={t('Copy referral code')}>
<Button className="text-sm no-underline !py-0 !px-0 h-fit !bg-transparent">
<span className="sr-only">{t('Copy')}</span>
<VegaIcon size={24} name={VegaIconNames.COPY} />
<VegaIcon size={20} name={VegaIconNames.COPY} />
</Button>
</CopyWithTooltip>
<CopyWithTooltip
text={applyCodeLink}
description={
<>
{t('Copy shareable apply code link')}
{': '}
<a className="text-vega-blue-500 underline" href={applyCodeLink}>
{truncate(applyCodeLink, { length: 32 })}
</a>
</>
}
>
<Button className="text-sm no-underline !py-0 !px-0 h-fit !bg-transparent">
<span className="sr-only">{t('Copy')}</span>
<VegaIcon size={20} name={VegaIconNames.OPEN_EXTERNAL} />
</Button>
</CopyWithTooltip>
</div>
+13 -2
View File
@@ -1,10 +1,21 @@
import { t } from '@vegaprotocol/i18n';
import { useT } from '../../lib/use-t';
import { RewardsContainer } from '../../components/rewards-container';
import { usePageTitleStore } from '../../stores';
import { titlefy } from '@vegaprotocol/utils';
import { useEffect } from 'react';
export const Rewards = () => {
const t = useT();
const title = t('Rewards');
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([title]));
}, [updateTitle, title]);
return (
<div className="container mx-auto p-4">
<h1 className="px-4 pb-4 text-2xl">{t('Rewards')}</h1>
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
<RewardsContainer />
</div>
);
@@ -16,18 +16,11 @@ query DiscountPrograms {
}
}
query Fees(
$partyId: ID!
$volumeDiscountEpochs: Int!
$referralDiscountEpochs: Int!
) {
query Fees($partyId: ID!) {
epoch {
id
}
volumeDiscountStats(
partyId: $partyId
pagination: { last: $volumeDiscountEpochs }
) {
volumeDiscountStats(partyId: $partyId, pagination: { last: 1 }) {
edges {
node {
atEpoch
@@ -59,10 +52,7 @@ query Fees(
}
}
}
referralSetStats(
partyId: $partyId
pagination: { last: $referralDiscountEpochs }
) {
referralSetStats(partyId: $partyId, pagination: { last: 1 }) {
edges {
node {
atEpoch
+3 -10
View File
@@ -10,8 +10,6 @@ export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgr
export type FeesQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
volumeDiscountEpochs: Types.Scalars['Int'];
referralDiscountEpochs: Types.Scalars['Int'];
}>;
@@ -65,14 +63,11 @@ export type DiscountProgramsQueryHookResult = ReturnType<typeof useDiscountProgr
export type DiscountProgramsLazyQueryHookResult = ReturnType<typeof useDiscountProgramsLazyQuery>;
export type DiscountProgramsQueryResult = Apollo.QueryResult<DiscountProgramsQuery, DiscountProgramsQueryVariables>;
export const FeesDocument = gql`
query Fees($partyId: ID!, $volumeDiscountEpochs: Int!, $referralDiscountEpochs: Int!) {
query Fees($partyId: ID!) {
epoch {
id
}
volumeDiscountStats(
partyId: $partyId
pagination: {last: $volumeDiscountEpochs}
) {
volumeDiscountStats(partyId: $partyId, pagination: {last: 1}) {
edges {
node {
atEpoch
@@ -104,7 +99,7 @@ export const FeesDocument = gql`
}
}
}
referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) {
referralSetStats(partyId: $partyId, pagination: {last: 1}) {
edges {
node {
atEpoch
@@ -129,8 +124,6 @@ export const FeesDocument = gql`
* const { data, loading, error } = useFeesQuery({
* variables: {
* partyId: // value for 'partyId'
* volumeDiscountEpochs: // value for 'volumeDiscountEpochs'
* referralDiscountEpochs: // value for 'referralDiscountEpochs'
* },
* });
*/
@@ -36,25 +36,25 @@ export const FeesContainer = () => {
const { data: markets, loading: marketsLoading } = useMarketList();
const { data: programData, loading: programLoading } =
useDiscountProgramsQuery();
useDiscountProgramsQuery({ errorPolicy: 'ignore' });
const volumeDiscountWindowLength =
programData?.currentVolumeDiscountProgram?.windowLength || 1;
const referralDiscountWindowLength =
programData?.currentReferralProgram?.windowLength || 1;
const { data: feesData, loading: feesLoading } = useFeesQuery({
variables: {
partyId: pubKey || '',
volumeDiscountEpochs: volumeDiscountWindowLength,
referralDiscountEpochs: referralDiscountWindowLength,
},
skip: !pubKey || !programData,
skip: !pubKey,
});
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
useVolumeStats(
feesData?.volumeDiscountStats,
previousEpoch,
feesData?.volumeDiscountStats.edges?.[0]?.node,
programData?.currentVolumeDiscountProgram
);
@@ -67,12 +67,12 @@ export const FeesContainer = () => {
code,
isReferrer,
} = useReferralStats(
feesData?.referralSetStats,
feesData?.referralSetReferees,
previousEpoch,
feesData?.referralSetStats.edges?.[0]?.node,
feesData?.referralSetReferees.edges?.[0]?.node,
programData?.currentReferralProgram,
feesData?.epoch,
feesData?.referrer,
feesData?.referee
feesData?.referrer.edges?.[0]?.node,
feesData?.referee.edges?.[0]?.node
);
const loading = paramsLoading || feesLoading || programLoading;
@@ -317,7 +317,7 @@ export const CurrentVolume = ({
<div className="flex flex-col gap-3 pt-4">
<CardStat
value={formatNumberRounded(new BigNumber(windowLengthVolume))}
text={t('Past {{count}} epochs', { count: windowLength })}
text={t('pastEpochs', 'Past {{count}} epochs', { count: windowLength })}
/>
{requiredForNextTier > 0 && (
<CardStat
@@ -344,9 +344,13 @@ const ReferralBenefits = ({
<CardStat
// all sets volume (not just current party)
value={formatNumber(setRunningNotionalTakerVolume)}
text={t('Combined running notional over the {{count}} epochs', {
count: epochs,
})}
text={t(
'runningNotionalOverEpochs',
'Combined running notional over the {{count}} epochs',
{
count: epochs,
}
)}
/>
<CardStat value={epochsInSet} text={t('epochs in referral set')} />
</div>
@@ -453,31 +457,27 @@ const VolumeTiers = ({
<Th>{t('Discount')}</Th>
<Th>{t('Min. trading volume')}</Th>
<Th>
{t('My volume (last {{count}} epochs)', { count: windowLength })}
{t('myVolume', 'My volume (last {{count}} epochs)', {
count: windowLength,
})}
</Th>
<Th />
</tr>
</THead>
<tbody>
{Array.from(tiers)
.reverse()
.map((tier, i) => {
const isUserTier = tiers.length - 1 - tierIndex === i;
{Array.from(tiers).map((tier, i) => {
const isUserTier = tierIndex === i;
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>
{formatPercentage(Number(tier.volumeDiscountFactor))}%
</Td>
<Td>
{formatNumber(tier.minimumRunningNotionalTakerVolume)}
</Td>
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
<Td>{isUserTier ? <YourTier /> : null}</Td>
</Tr>
);
})}
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>{formatPercentage(Number(tier.volumeDiscountFactor))}%</Td>
<Td>{formatNumber(tier.minimumRunningNotionalTakerVolume)}</Td>
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
<Td>{isUserTier ? <YourTier /> : null}</Td>
</Tr>
);
})}
</tbody>
</Table>
</div>
@@ -520,37 +520,33 @@ const ReferralTiers = ({
</tr>
</THead>
<tbody>
{Array.from(tiers)
.reverse()
.map((t, i) => {
const isUserTier = tiers.length - 1 - tierIndex === i;
{Array.from(tiers).map((t, i) => {
const isUserTier = tierIndex === i;
const requiredVolume = Number(
t.minimumRunningNotionalTakerVolume
const requiredVolume = Number(t.minimumRunningNotionalTakerVolume);
let unlocksIn = null;
if (
referralVolumeInWindow >= requiredVolume &&
epochsInSet < t.minimumEpochs
) {
unlocksIn = (
<span className="text-muted">
Unlocks in {t.minimumEpochs - epochsInSet} epochs
</span>
);
let unlocksIn = null;
}
if (
referralVolumeInWindow >= requiredVolume &&
epochsInSet < t.minimumEpochs
) {
unlocksIn = (
<span className="text-muted">
Unlocks in {t.minimumEpochs - epochsInSet} epochs
</span>
);
}
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
<Td>{t.minimumEpochs}</Td>
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
</Tr>
);
})}
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
<Td>{t.minimumEpochs}</Td>
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
</Tr>
);
})}
</tbody>
</Table>
</div>
@@ -2,46 +2,15 @@ import { renderHook } from '@testing-library/react';
import { useReferralStats } from './use-referral-stats';
describe('useReferralStats', () => {
const setStats = {
edges: [
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 9,
discountFactor: '0.2',
referralSetRunningNotionalTakerVolume: '100',
},
},
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: '200',
},
},
],
const stat = {
__typename: 'ReferralSetStats' as const,
atEpoch: 9,
discountFactor: '0.01',
referralSetRunningNotionalTakerVolume: '100',
};
const sets = {
edges: [
{
node: {
atEpoch: 3,
},
},
{
node: {
atEpoch: 4,
},
},
],
};
const epoch = {
id: '10',
const set = {
atEpoch: 4,
};
const program = {
@@ -78,102 +47,36 @@ describe('useReferralStats', () => {
});
});
it('returns formatted data and tiers', () => {
it('returns default values if set is not from previous epoch', () => {
const { result } = renderHook(() =>
useReferralStats(setStats, sets, program, epoch)
useReferralStats(10, stat, set, program)
);
// should use stats from latest epoch
const stats = setStats.edges[1].node;
const set = sets.edges[1].node;
expect(result.current).toEqual({
referralDiscount: Number(stats.discountFactor),
referralVolumeInWindow: Number(
stats.referralSetRunningNotionalTakerVolume
),
referralTierIndex: 1,
referralDiscount: 0,
referralVolumeInWindow: 0,
referralTierIndex: -1,
referralTiers: program.benefitTiers,
epochsInSet: Number(epoch.id) - set.atEpoch,
epochsInSet: 0,
code: undefined,
isReferrer: false,
});
});
it.each([
{ joinedAt: 2, index: -1 },
{ joinedAt: 3, index: -1 },
{ joinedAt: 4, index: 0 },
{ joinedAt: 5, index: 0 },
{ joinedAt: 6, index: 1 },
{ joinedAt: 7, index: 1 },
{ joinedAt: 8, index: 2 },
{ joinedAt: 9, index: 2 },
])('joined at epoch: $joinedAt should be index: $index', (obj) => {
const statsA = {
edges: [
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: '100000',
},
},
],
};
const setsA = {
edges: [
{
node: {
atEpoch: Number(epoch.id) - obj.joinedAt,
},
},
],
};
it('returns formatted data and tiers', () => {
const { result } = renderHook(() =>
useReferralStats(statsA, setsA, program, epoch)
useReferralStats(9, stat, set, program)
);
expect(result.current.referralTierIndex).toEqual(obj.index);
});
it.each([
{ volume: '50', index: -1 },
{ volume: '100', index: 0 },
{ volume: '150', index: 0 },
{ volume: '200', index: 1 },
{ volume: '250', index: 1 },
{ volume: '300', index: 2 },
{ volume: '999', index: 2 },
])('volume: $volume should be index: $index', (obj) => {
const statsA = {
edges: [
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: obj.volume,
},
},
],
};
const setsA = {
edges: [
{
node: {
atEpoch: 1,
},
},
],
};
const { result } = renderHook(() =>
useReferralStats(statsA, setsA, program, epoch)
);
expect(result.current.referralTierIndex).toEqual(obj.index);
expect(result.current).toEqual({
referralDiscount: Number(stat.discountFactor),
referralVolumeInWindow: Number(
stat.referralSetRunningNotionalTakerVolume
),
referralTierIndex: 0,
referralTiers: program.benefitTiers,
epochsInSet: stat.atEpoch - set.atEpoch,
code: undefined,
isReferrer: false,
});
});
});
@@ -1,20 +1,24 @@
import compact from 'lodash/compact';
import maxBy from 'lodash/maxBy';
import { getReferralBenefitTier } from './utils';
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
import { first } from 'lodash';
export const useReferralStats = (
setStats?: FeesQuery['referralSetStats'],
setReferees?: FeesQuery['referralSetReferees'],
previousEpoch?: number,
referralStats?: NonNullable<
FeesQuery['referralSetStats']['edges']['0']
>['node'],
setReferees?: NonNullable<
FeesQuery['referralSetReferees']['edges']['0']
>['node'],
program?: DiscountProgramsQuery['currentReferralProgram'],
epoch?: FeesQuery['epoch'],
setIfReferrer?: FeesQuery['referrer'],
setIfReferee?: FeesQuery['referee']
setIfReferrer?: NonNullable<FeesQuery['referrer']['edges']['0']>['node'],
setIfReferee?: NonNullable<FeesQuery['referee']['edges']['0']>['node']
) => {
const referralTiers = program?.benefitTiers || [];
if (!setStats || !setReferees || !program || !epoch) {
if (
!previousEpoch ||
referralStats?.atEpoch !== previousEpoch ||
!program ||
!setReferees
) {
return {
referralDiscount: 0,
referralVolumeInWindow: 0,
@@ -26,41 +30,22 @@ export const useReferralStats = (
};
}
const setIfReferrerData = first(
compact(setIfReferrer?.edges).map((e) => e.node)
);
const setIfRefereeData = first(
compact(setIfReferee?.edges).map((e) => e.node)
);
const referralSetsStats = compact(setStats.edges).map((e) => e.node);
const referralSets = compact(setReferees.edges).map((e) => e.node);
const referralSet = maxBy(referralSets, (s) => s.atEpoch);
const referralStats = maxBy(referralSetsStats, (s) => s.atEpoch);
const epochsInSet = referralSet ? Number(epoch.id) - referralSet.atEpoch : 0;
const referralDiscount = Number(referralStats?.discountFactor || 0);
const referralVolumeInWindow = Number(
referralStats?.referralSetRunningNotionalTakerVolume || 0
);
const referralTierIndex = referralStats
? getReferralBenefitTier(
epochsInSet,
Number(referralStats.referralSetRunningNotionalTakerVolume),
referralTiers
)
: -1;
const referralTierIndex = referralTiers.findIndex(
(tier) => tier.referralDiscountFactor === referralStats?.discountFactor
);
return {
referralDiscount,
referralVolumeInWindow,
referralTierIndex,
referralTiers,
epochsInSet,
code: (setIfReferrerData || setIfRefereeData)?.id,
isReferrer: Boolean(setIfReferrerData),
epochsInSet: referralStats.atEpoch - setReferees.atEpoch,
code: (setIfReferrer || setIfReferee)?.id,
isReferrer: Boolean(setIfReferrer),
};
};
@@ -2,27 +2,11 @@ import { renderHook } from '@testing-library/react';
import { useVolumeStats } from './use-volume-stats';
describe('useReferralStats', () => {
const statsList = {
edges: [
{
__typename: 'VolumeDiscountStatsEdge' as const,
node: {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 9,
discountFactor: '0.1',
runningVolume: '100',
},
},
{
__typename: 'VolumeDiscountStatsEdge' as const,
node: {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 10,
discountFactor: '0.3',
runningVolume: '200',
},
},
],
const stats = {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 10,
discountFactor: '0.05',
runningVolume: '200',
};
const program = {
@@ -44,7 +28,7 @@ describe('useReferralStats', () => {
};
it('returns correct default values', () => {
const { result } = renderHook(() => useVolumeStats());
const { result } = renderHook(() => useVolumeStats(10));
expect(result.current).toEqual({
volumeDiscount: 0,
volumeInWindow: 0,
@@ -53,11 +37,18 @@ describe('useReferralStats', () => {
});
});
it('returns formatted data and tiers', () => {
const { result } = renderHook(() => useVolumeStats(statsList, program));
it('returns default values if no stat is not from previous epoch', () => {
const { result } = renderHook(() => useVolumeStats(11, stats, program));
expect(result.current).toEqual({
volumeDiscount: 0,
volumeInWindow: 0,
volumeTierIndex: -1,
volumeTiers: program.benefitTiers,
});
});
// should use stats from latest epoch
const stats = statsList.edges[1].node;
it('returns formatted data and tiers', () => {
const { result } = renderHook(() => useVolumeStats(10, stats, program));
expect(result.current).toEqual({
volumeDiscount: Number(stats.discountFactor),
@@ -66,30 +57,4 @@ describe('useReferralStats', () => {
volumeTiers: program.benefitTiers,
});
});
it.each([
{ volume: '100', index: 0 },
{ volume: '150', index: 0 },
{ volume: '200', index: 1 },
{ volume: '250', index: 1 },
{ volume: '300', index: 2 },
{ volume: '350', index: 2 },
])('returns index: $index for the running volume: $volume', (obj) => {
const statsA = {
edges: [
{
__typename: 'VolumeDiscountStatsEdge' as const,
node: {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 10,
discountFactor: '0.3',
runningVolume: obj.volume,
},
},
],
};
const { result } = renderHook(() => useVolumeStats(statsA, program));
expect(result.current.volumeTierIndex).toBe(obj.index);
});
});
@@ -1,15 +1,15 @@
import compact from 'lodash/compact';
import maxBy from 'lodash/maxBy';
import { getVolumeTier } from './utils';
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
export const useVolumeStats = (
stats?: FeesQuery['volumeDiscountStats'],
previousEpoch: number,
lastEpochStats?: NonNullable<
FeesQuery['volumeDiscountStats']['edges']['0']
>['node'],
program?: DiscountProgramsQuery['currentVolumeDiscountProgram']
) => {
const volumeTiers = program?.benefitTiers || [];
if (!stats || !program) {
if (!lastEpochStats || lastEpochStats.atEpoch !== previousEpoch || !program) {
return {
volumeDiscount: 0,
volumeTierIndex: -1,
@@ -18,11 +18,11 @@ export const useVolumeStats = (
};
}
const volumeStats = compact(stats.edges).map((e) => e.node);
const lastEpochStats = maxBy(volumeStats, (s) => s.atEpoch);
const volumeDiscount = Number(lastEpochStats?.discountFactor || 0);
const volumeInWindow = Number(lastEpochStats?.runningVolume || 0);
const volumeTierIndex = getVolumeTier(volumeInWindow, volumeTiers);
const volumeTierIndex = volumeTiers.findIndex(
(tier) => tier.volumeDiscountFactor === lastEpochStats?.discountFactor
);
return {
volumeDiscount,
@@ -20,73 +20,6 @@ export const formatPercentage = (num: number) => {
return formatter.format(parseFloat(pct.toFixed(5)));
};
/**
* Return the index of the benefit tier for volume discounts. A user
* only needs to fulfill a minimum volume requirement for the tier
*/
export const getVolumeTier = (
volume: number,
tiers: Array<{
minimumRunningNotionalTakerVolume: string;
}>
) => {
return tiers.findIndex((tier, i) => {
const nextTier = tiers[i + 1];
const validVolume =
volume >= Number(tier.minimumRunningNotionalTakerVolume);
if (nextTier) {
return (
validVolume &&
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
);
}
return validVolume;
});
};
/**
* Return the index of the benefit tiers for referrals. A user must
* fulfill both the minimum epochs in the referral set, and the set
* must reach the combined total volume
*/
export const getReferralBenefitTier = (
epochsInSet: number,
volume: number,
tiers: Array<{
minimumRunningNotionalTakerVolume: string;
minimumEpochs: number;
}>
) => {
const indexByEpoch = tiers.findIndex((tier, i) => {
const nextTier = tiers[i + 1];
const validEpochs = epochsInSet >= tier.minimumEpochs;
if (nextTier) {
return validEpochs && epochsInSet < nextTier.minimumEpochs;
}
return validEpochs;
});
const indexByVolume = tiers.findIndex((tier, i) => {
const nextTier = tiers[i + 1];
const validVolume =
volume >= Number(tier.minimumRunningNotionalTakerVolume);
if (nextTier) {
return (
validVolume &&
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
);
}
return validVolume;
});
return Math.min(indexByEpoch, indexByVolume);
};
/**
* Given a set of fees and a set of discounts return
* the adjusted fee factor
@@ -65,7 +65,7 @@ export const FundingContainer = ({ marketId }: { marketId: string }) => {
if (edge.node.endTime) {
acc?.push({
endTime: fromNanoSeconds(edge.node.endTime),
fundingRate: Number(edge.node.fundingRate) * 100,
fundingRate: Number(edge.node.fundingRate),
});
}
return acc;
@@ -82,7 +82,8 @@ export const FundingContainer = ({ marketId }: { marketId: string }) => {
<LineChart
data={values}
theme={theme}
priceFormat={(fundingRate) => `${fundingRate.toFixed(4)}%`}
priceFormat={(fundingRate) => `${(fundingRate * 100).toFixed(4)}%`}
yAxisTickFormat="%"
/>
);
};
@@ -16,15 +16,12 @@ export const LedgerContainer = () => {
});
const assets = (data?.party?.accountsConnection?.edges ?? [])
.map<PartyAssetFieldsFragment>(
(item) => item?.node?.asset ?? ({} as PartyAssetFieldsFragment)
)
.reduce((aggr, item) => {
if ('id' in item && 'symbol' in item) {
aggr[item.id as string] = item.symbol as string;
}
return aggr;
}, {} as Record<string, string>);
.map((item) => item?.node?.asset)
.filter((asset): asset is PartyAssetFieldsFragment => !!asset?.id)
.reduce(
(aggr, item) => Object.assign(aggr, { [item.id]: item.symbol }),
{} as Record<string, string>
);
if (!pubKey) {
return (
+16 -2
View File
@@ -68,7 +68,6 @@ describe('Navbar', () => {
['/portfolio', 'Portfolio'],
['/referrals', 'Referrals'],
['/fees', 'Fees'],
['/rewards', 'Rewards'],
[expect.stringContaining('governance'), 'Governance'],
];
@@ -103,7 +102,6 @@ describe('Navbar', () => {
['/portfolio', 'Portfolio'],
['/referrals', 'Referrals'],
['/fees', 'Fees'],
['/rewards', 'Rewards'],
[expect.stringContaining('governance'), 'Governance'],
];
const links = menu.getAllByRole('link');
@@ -172,4 +170,20 @@ describe('Navbar', () => {
expect(mockDisconnect).toHaveBeenCalled();
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
});
it('does not render the language selector until we have more languages', () => {
renderComponent();
expect(screen.queryByTestId('icon-globe')).not.toBeInTheDocument();
});
it('renders the theme switcher', async () => {
renderComponent();
await userEvent.click(screen.getByTestId('icon-moon'));
expect(screen.queryByTestId('icon-moon')).not.toBeInTheDocument();
expect(screen.getByTestId('icon-sun')).toBeInTheDocument();
await userEvent.click(screen.getByTestId('icon-sun'));
expect(screen.queryByTestId('icon-sun')).not.toBeInTheDocument();
expect(screen.getByTestId('icon-moon')).toBeInTheDocument();
});
});
+19 -7
View File
@@ -11,7 +11,13 @@ import {
} from '@vegaprotocol/environment';
import { useGlobalStore } from '../../stores';
import { VegaWalletConnectButton } from '../vega-wallet-connect-button';
import { VegaIconNames, VegaIcon, VLogo } from '@vegaprotocol/ui-toolkit';
import {
VegaIconNames,
VegaIcon,
VLogo,
LanguageSelector,
ThemeSwitcher,
} from '@vegaprotocol/ui-toolkit';
import * as N from '@radix-ui/react-navigation-menu';
import * as D from '@radix-ui/react-dialog';
import { NavLink } from 'react-router-dom';
@@ -22,7 +28,8 @@ import { VegaWalletMenu } from '../vega-wallet';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { WalletIcon } from '../icons/wallet';
import { ProtocolUpgradeCountdown } from '@vegaprotocol/proposals';
import { useT } from '../../lib/use-t';
import { useT, useI18n } from '../../lib/use-t';
import { supportedLngs } from '../../lib/i18n';
type MenuState = 'wallet' | 'nav' | null;
type Theme = 'system' | 'yellow';
@@ -34,6 +41,7 @@ export const Navbar = ({
children?: ReactNode;
theme?: Theme;
}) => {
const i18n = useI18n();
const t = useT();
// menu state for small screens
const [menu, setMenu] = useState<MenuState>(null);
@@ -77,6 +85,15 @@ export const Navbar = ({
{/* Right section */}
<div className="ml-auto flex items-center justify-end gap-2">
<ProtocolUpgradeCountdown />
<div className="flex">
<ThemeSwitcher />
{supportedLngs.length > 1 ? (
<LanguageSelector
languages={supportedLngs}
onSelect={(language) => i18n.changeLanguage(language)}
/>
) : null}
</div>
<NavbarMobileButton
onClick={() => {
if (isConnected) {
@@ -196,11 +213,6 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
{t('Fees')}
</NavbarLink>
</NavbarItem>
<NavbarItem>
<NavbarLink to={Links.REWARDS()} onClick={onClick}>
{t('Rewards')}
</NavbarLink>
</NavbarItem>
<NavbarItem>
<NavbarLinkExternal to={useLinks(DApp.Governance)()}>
{t('Governance')}
@@ -1,7 +1,6 @@
import groupBy from 'lodash/groupBy';
import type { Account } from '@vegaprotocol/accounts';
import { useAccounts } from '@vegaprotocol/accounts';
import { t } from '@vegaprotocol/i18n';
import {
NetworkParams,
useNetworkParams,
@@ -31,8 +30,10 @@ import { addDecimalsFormatNumberQuantum } from '@vegaprotocol/utils';
import { ViewType, useSidebar } from '../sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { RewardsHistoryContainer } from './rewards-history';
import { useT } from '../../lib/use-t';
export const RewardsContainer = () => {
const t = useT();
const { pubKey } = useVegaWallet();
const { params, loading: paramsLoading } = useNetworkParams([
NetworkParams.reward_asset,
@@ -69,7 +70,7 @@ export const RewardsContainer = () => {
);
return (
<div className="grid auto-rows-min grid-cols-6 gap-3">
<div className="grid auto-rows-min grid-cols-6 gap-3 p-2">
{/* Always show reward information for vega */}
<Card
key={params.reward_asset}
@@ -121,7 +122,9 @@ export const RewardsContainer = () => {
return (
<Card
key={assetId}
title={t('%s Reward pot', asset.symbol)}
title={t('{{assetSymbol}} Reward pot', {
assetSymbol: asset.symbol,
})}
className="lg:col-span-3 xl:col-span-2"
loading={loading}
>
@@ -167,6 +170,7 @@ export const RewardPot = ({
assetId,
vestingBalancesSummary,
}: RewardPotProps) => {
const t = useT();
// TODO: Opening the sidebar for the first time works, but then clicking on redeem
// for a different asset does not update the form
const currentRouteId = useGetCurrentRouteId();
@@ -242,7 +246,9 @@ export const RewardPot = ({
<CardTable>
<tr>
<CardTableTH className="flex items-center gap-1">
{t(`Locked ${rewardAsset.symbol}`)}
{t('Locked {{assetSymbol}}', {
assetSymbol: rewardAsset.symbol,
})}
<VegaIcon name={VegaIconNames.LOCK} size={12} />
</CardTableTH>
<CardTableTD>
@@ -254,7 +260,11 @@ export const RewardPot = ({
</CardTableTD>
</tr>
<tr>
<CardTableTH>{t(`Vesting ${rewardAsset.symbol}`)}</CardTableTH>
<CardTableTH>
{t('Vesting {{assetSymbol}}', {
assetSymbol: rewardAsset.symbol,
})}
</CardTableTH>
<CardTableTD>
{addDecimalsFormatNumberQuantum(
totalVesting.toString(),
@@ -309,6 +319,7 @@ export const Vesting = ({
baseRate: string;
multiplier?: string;
}) => {
const t = useT();
const rate = new BigNumber(baseRate).times(multiplier);
const rateFormatted = formatPercentage(Number(rate));
const baseRateFormatted = formatPercentage(Number(baseRate));
@@ -341,6 +352,7 @@ export const Multipliers = ({
streakMultiplier?: string;
hoarderMultiplier?: string;
}) => {
const t = useT();
const combinedMultiplier = new BigNumber(streakMultiplier).times(
hoarderMultiplier
);
@@ -16,12 +16,12 @@ import {
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import {
useRewardsHistoryQuery,
type RewardsHistoryQuery,
} from './__generated__/Rewards';
import { useRewardsRowData } from './use-reward-row-data';
import { useT } from '../../lib/use-t';
export const RewardsHistoryContainer = ({
epoch,
@@ -140,6 +140,7 @@ export const RewardHistoryTable = ({
onEpochChange: (epochVariables: { from: number; to: number }) => void;
loading: boolean;
}) => {
const t = useT();
const [isParty, setIsParty] = useState(false);
const rowData = useRewardsRowData({
@@ -0,0 +1,12 @@
import { Settings } from './settings';
import { render, screen } from '@testing-library/react';
describe('Settings', () => {
it('should the settings component with all the options', () => {
render(<Settings />);
expect(screen.getByText('Dark mode')).toBeInTheDocument();
expect(screen.getByText('Share usage data')).toBeInTheDocument();
expect(screen.getByText('Toast location')).toBeInTheDocument();
expect(screen.getByText('Reset to default')).toBeInTheDocument();
});
});
+55 -2
View File
@@ -1,7 +1,13 @@
import { Switch, ToastPositionSetter } from '@vegaprotocol/ui-toolkit';
import {
Dialog,
Intent,
Switch,
ToastPositionSetter,
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
import type { ReactNode } from 'react';
import { useState, type ReactNode } from 'react';
import classNames from 'classnames';
import { useT } from '../../lib/use-t';
@@ -9,6 +15,7 @@ export const Settings = () => {
const t = useT();
const { theme, setTheme } = useThemeSwitcher();
const [isApproved, setIsApproved] = useTelemetryApproval();
const [open, setOpen] = useState(false);
return (
<div>
<SettingsGroup label={t('Dark mode')}>
@@ -33,6 +40,52 @@ export const Settings = () => {
<SettingsGroup label={t('Toast location')}>
<ToastPositionSetter />
</SettingsGroup>
<SettingsGroup label={t('Reset to default')}>
<TradingButton
name="reset-to-defaults"
size="small"
intent={Intent.None}
onClick={() => {
setOpen(true);
}}
>
{t('Reset')}
</TradingButton>
<Dialog open={open} title={t('Reset')}>
<div className="mb-4">
<p>
{t(
'You will lose all persisted settings and you will be logged out.'
)}
</p>
<p>
{t('Are you sure you want to reset all settings to default?')}
</p>
</div>
<div className="flex flex-col gap-4">
<TradingButton
name="reset-to-defaults-cancel"
intent={Intent.Primary}
onClick={() => {
localStorage.clear();
window.location.reload();
}}
>
{t('Yes, clear cache and refresh')}
</TradingButton>
<TradingButton
name="reset-to-defaults-cancel"
intent={Intent.None}
onClick={() => {
setOpen(false);
}}
>
{t('No, keep settings')}
</TradingButton>
</div>
</Dialog>
</SettingsGroup>
<SettingsGroup inline={false} label={t('App information')}>
<dl className="text-sm grid grid-cols-2 gap-1">
{process.env.GIT_TAG && (
+1 -1
View File
@@ -1,2 +1,2 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
VEGA_VERSION=v0.73.4
VEGA_VERSION=v0.73.6
+1 -1
View File
@@ -1,2 +1,2 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:develop
VEGA_VERSION=v0.73.4
VEGA_VERSION=v0.73.6
+1 -1
View File
@@ -1,2 +1,2 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:main
VEGA_VERSION=v0.73.4
VEGA_VERSION=v0.73.6
+12 -1
View File
@@ -34,4 +34,15 @@ def next_epoch(vega: VegaServiceNull):
"Epoch not started after forwarding the duration of two epochs."
)
vega.wait_fn(1)
vega.wait_for_total_catchup()
vega.wait_for_total_catchup()
def truncate_middle(market_id, start=6, end=4):
if len(market_id) < 11:
return market_id
return market_id[:start] + '\u2026' + market_id[-end:]
def change_keys(page: Page, vega:VegaServiceNull, key_name):
page.get_by_test_id("manage-vega-wallet").click()
page.get_by_test_id("key-" + vega.wallet.public_key(key_name)).click()
page.click(f'data-testid=key-{vega.wallet.public_key(key_name)} >> .inline-flex')
page.reload()
+7 -2
View File
@@ -12,6 +12,7 @@ from contextlib import contextmanager
from vega_sim.null_service import VegaServiceNull
from playwright.sync_api import Browser, Page
from config import console_image_name, vega_version
from datetime import datetime, timedelta
from fixtures.market import (
setup_simple_market,
setup_opening_auction_market,
@@ -78,6 +79,7 @@ def init_vega(request=None):
store_transactions=True,
transactions_per_block=1000,
seconds_per_block=seconds_per_block,
genesis_time= datetime.now() - timedelta(days=1),
) as vega:
try:
container = docker_client.containers.run(
@@ -136,8 +138,11 @@ def init_page(vega: VegaServiceNull, browser: Browser, request: pytest.FixtureRe
page.add_init_script(script=window_env)
yield page
finally:
if not os.path.exists("traces"):
os.makedirs("traces")
try:
if not os.path.exists("apps/trading/e2e/traces"):
os.makedirs("apps/trading/e2e/traces")
except OSError as e:
print(f"Failed to create directory '{'apps/trading/e2e/traces'}': {e}")
# Check whether this test failed or passed
outcome = request.config.cache.get(request.node.nodeid, None)
+1 -14
View File
@@ -1,26 +1,13 @@
from collections import namedtuple
from vega_sim.service import VegaService
from actions.vega import submit_multiple_orders, submit_order, submit_liquidity
from wallet_config import MM_WALLET, MM_WALLET2, TERMINATE_WALLET, wallets
import logging
logger = logging.getLogger()
# Defined namedtuples
WalletConfig = namedtuple("WalletConfig", ["name", "passphrase"])
# Wallet Configurations
MM_WALLET = WalletConfig("mm", "pin")
MM_WALLET2 = WalletConfig("mm2", "pin2")
TERMINATE_WALLET = WalletConfig("FJMKnwfZdd48C8NqvYrG", "bY3DxwtsCstMIIZdNpKs")
wallets = [MM_WALLET, MM_WALLET2, TERMINATE_WALLET]
mint_amount: float = 10e5
market_name = "BTC:DAI_2023"
def setup_simple_market(
vega: VegaService,
approve_proposal=True,
+1 -1
View File
@@ -1161,7 +1161,7 @@ profile = ["pytest-profiling", "snakeviz"]
type = "git"
url = "https://github.com/vegaprotocol/vega-market-sim.git"
reference = "HEAD"
resolved_reference = "e93f7dfa8463c59cfd0e299362b845511cebeef6"
resolved_reference = "fbcb974b2055bbc80169cdfd69987f087f9969fb"
[[package]]
name = "websocket-client"
+1 -1
View File
@@ -9,7 +9,7 @@ packages = [{include = "trading market-sim e2e"}]
[tool.poetry.dependencies]
python = ">=3.9,<3.11"
psutil = "^5.9.5"
vega-sim = {git = "https://github.com/vegaprotocol/vega-market-sim.git"}
vega-sim = {git = "https://github.com/vegaprotocol/vega-market-sim.git/", branch = "fix/genesis_panic"}
pytest-playwright = "^0.4.2"
docker = "^6.1.3"
pytest-xdist = "^3.3.1"
@@ -1,12 +0,0 @@
import pytest
from playwright.sync_api import expect, Page
@pytest.mark.usefixtures("page", "continuous_market", "risk_accepted")
def test_see_market_depth_chart(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
# Click on the 'Depth' tab
page.get_by_test_id("Depth").click()
# Check if the 'Depth' tab and the depth chart are visible
# 6006-DEPC-001
expect(page.get_by_test_id("tab-depth")).to_be_visible()
expect(page.locator('[class^="depth-chart-module_canvas__"]').first).to_be_visible()
@@ -29,11 +29,12 @@ def continuous_market(vega):
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_limit_buy_order_GTT(continuous_market, vega: VegaService, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(tif).select_option("Good 'til Time (GTT)")
page.get_by_test_id(order_size).fill("10")
page.get_by_test_id(order_price).fill("120")
page.get_by_test_id(tif).select_option("Good 'til Time (GTT)")
expires_at = datetime.now() + timedelta(days=1)
expires_at_input_value = expires_at.strftime("%Y-%m-%dT%H:%M:%S")
page.get_by_test_id("date-picker-field").clear()
page.get_by_test_id("date-picker-field").fill(expires_at_input_value)
# 7002-SORD-011
expect(page.get_by_test_id("place-order").locator("span").first).to_have_text(
@@ -1,16 +1,9 @@
import pytest
from collections import namedtuple
from playwright.sync_api import Page, expect
from vega_sim.service import VegaService
from actions.vega import submit_order
from actions.utils import wait_for_toast_confirmation
# Defined namedtuples
WalletConfig = namedtuple("WalletConfig", ["name", "passphrase"])
# Wallet Configurations
MM_WALLET = WalletConfig("mm", "pin")
notional = "deal-ticket-fee-notional"
fees = "deal-ticket-fee-fees"
margin_required = "deal-ticket-fee-margin-required"
@@ -1,5 +1,4 @@
import pytest
from collections import namedtuple
from playwright.sync_api import Page, expect
from vega_sim.service import VegaService
from actions.vega import submit_order
@@ -7,14 +6,6 @@ from datetime import datetime, timedelta
from conftest import init_vega
from fixtures.market import setup_continuous_market
# Defined namedtuples
WalletConfig = namedtuple("WalletConfig", ["name", "passphrase"])
# Wallet Configurations
MM_WALLET = WalletConfig("mm", "pin")
MM_WALLET2 = WalletConfig("mm2", "pin2")
TERMINATE_WALLET = WalletConfig("FJMKnwfZdd48C8NqvYrG", "bY3DxwtsCstMIIZdNpKs")
stop_order_btn = "order-type-Stop"
stop_limit_order_btn = "order-type-StopLimit"
stop_market_order_btn = "order-type-StopMarket"
@@ -1,19 +1,9 @@
import pytest
from collections import namedtuple
from playwright.sync_api import Page, expect
from vega_sim.service import VegaService
from actions.vega import submit_order
from conftest import init_vega
from fixtures.market import setup_continuous_market
from actions.utils import wait_for_toast_confirmation
# Defined namedtuples
WalletConfig = namedtuple("WalletConfig", ["name", "passphrase"])
# Wallet Configurations
MM_WALLET = WalletConfig("mm", "pin")
MM_WALLET2 = WalletConfig("mm2", "pin2")
TERMINATE_WALLET = WalletConfig("FJMKnwfZdd48C8NqvYrG", "bY3DxwtsCstMIIZdNpKs")
stop_order_btn = "order-type-Stop"
stop_limit_order_btn = "order-type-StopLimit"
@@ -336,98 +326,4 @@ def test_submit_stop_oco_limit_order_cancel(
page.locator(".ag-center-cols-container").locator('[col-id="status"]').last
).to_have_text("CancelledOCO")
class TestStopOcoValidation:
@pytest.fixture(scope="class")
def vega(self, request):
with init_vega(request) as vega:
yield vega
@pytest.fixture(scope="class")
def continuous_market(self, vega):
return setup_continuous_market(vega)
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_stop_market_order_oco_form_validation(self, continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(stop_order_btn).click()
page.get_by_test_id(stop_market_order_btn).is_visible()
page.get_by_test_id(stop_market_order_btn).click()
page.get_by_test_id(oco).click()
expect(
page.get_by_test_id("sidebar-content").get_by_text("Trigger").last
).to_be_visible()
# 7002-SORD-084
expect(page.locator('[for="triggerDirection-risesAbove-oco"]')).to_have_text(
"Rises above"
)
# 7002-SORD-085
expect(page.locator('[for="triggerDirection-fallsBelow-oco"]')).to_have_text(
"Falls below"
)
# 7002-SORD-087
expect(page.locator('[for="triggerType-price-oco"]')).to_have_text("Price")
expect(page.locator('[for="triggerType-price"]')).to_be_checked
# 7002-SORD-088
expect(
page.locator('[for="triggerType-trailingPercentOffset-oco"]')
).to_have_text("Trailing Percent Offset")
expect(page.locator('[for="order-size-oco"]')).to_have_text("Size")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_stop_limit_order_oco_form_validation(self, continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(stop_order_btn).click()
page.get_by_test_id(stop_market_order_btn).is_visible()
page.get_by_test_id(stop_limit_order_btn).click()
page.get_by_test_id(oco).click()
expect(
page.get_by_test_id("sidebar-content").get_by_text("Trigger").last
).to_be_visible()
# 7002-SORD-099
expect(page.locator('[for="triggerDirection-risesAbove-oco"]')).to_have_text(
"Rises above"
)
# 7002-SORD-091
expect(page.locator('[for="triggerDirection-fallsBelow-oco"]')).to_have_text(
"Falls below"
)
# 7002-SORD-095
expect(page.locator('[for="triggerType-price-oco"]')).to_have_text("Price")
expect(page.locator('[for="triggerType-price"]')).to_be_checked
# 7002-SORD-095
expect(
page.locator('[for="triggerType-trailingPercentOffset-oco"]')
).to_have_text("Trailing Percent Offset")
expect(page.locator('[for="order-size-oco"]')).to_have_text("Size")
expect(page.locator('[for="order-price-oco"]')).to_have_text("Price")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_maximum_number_of_active_stop_orders_oco(
self, continuous_market, vega: VegaService, page: Page
):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(stop_order_btn).click()
page.get_by_test_id(stop_limit_order_btn).is_visible()
page.get_by_test_id(stop_limit_order_btn).click()
page.get_by_test_id(order_side_sell).click()
page.locator("label").filter(has_text="Falls below").click()
page.get_by_test_id(trigger_price).fill("102")
page.get_by_test_id(order_size).fill("3")
page.get_by_test_id(order_price).fill("103")
page.get_by_test_id(oco).click()
page.get_by_test_id(trigger_price_oco).fill("120")
page.get_by_test_id(order_size_oco).fill("2")
page.get_by_test_id(order_limit_price_oco).fill("99")
for i in range(2):
page.get_by_test_id(submit_stop_order).click()
wait_for_toast_confirmation(page)
vega.wait_fn(1)
vega.forward("20s")
vega.wait_for_total_catchup()
if page.get_by_test_id(close_toast).is_visible():
page.get_by_test_id(close_toast).click()
# 7002-SORD-011
expect(page.get_by_test_id("stop-order-warning-limit")).to_have_text(
"There is a limit of 4 active stop orders per market. Orders submitted above the limit will be immediately rejected."
)
@@ -1,7 +1,7 @@
import pytest
from playwright.sync_api import Page, expect
from vega_sim.service import VegaService
from actions.utils import wait_for_toast_confirmation
from actions.utils import change_keys
from conftest import init_vega
from fixtures.market import setup_continuous_market
@@ -39,9 +39,7 @@ def test_should_display_info_and_button_for_deposit(continuous_market, vega: Veg
def test_should_show_an_error_if_your_balance_is_zero(continuous_market, vega: VegaService, page: Page):
page.goto(f"/#/markets/{continuous_market}")
vega.create_key("key_empty")
page.get_by_test_id("manage-vega-wallet").click()
page.locator('[role="menuitemradio"]').nth(4).click()
page.reload()
change_keys(page, vega, "key_empty")
page.get_by_test_id(order_size).fill("200")
page.get_by_test_id(order_price).fill("20")
# 7002-SORD-060
@@ -4,8 +4,8 @@ import json
from vega_sim.service import VegaService
from fixtures.market import setup_simple_market
from conftest import init_vega
from collections import namedtuple
from actions.vega import submit_order
from wallet_config import MM_WALLET, TERMINATE_WALLET, wallets
import logging
logger = logging.getLogger()
@@ -74,16 +74,6 @@ class TestGetStarted:
page.reload()
# Defined namedtuples
WalletConfig = namedtuple("WalletConfig", ["name", "passphrase"])
# Wallet Configurations
MM_WALLET = WalletConfig("mm", "pin")
MM_WALLET2 = WalletConfig("mm2", "pin2")
TERMINATE_WALLET = WalletConfig("FJMKnwfZdd48C8NqvYrG", "bY3DxwtsCstMIIZdNpKs")
wallets = [MM_WALLET, MM_WALLET2, TERMINATE_WALLET]
mint_amount: float = 10e5
for wallet in wallets:
@@ -1,22 +1,10 @@
import pytest
from collections import namedtuple
from playwright.sync_api import expect, Page
from vega_sim.service import VegaService
from actions.vega import submit_order
from conftest import init_vega
from fixtures.market import setup_continuous_market
# Defined namedtuples
WalletConfig = namedtuple("WalletConfig", ["name", "passphrase"])
# Wallet Configurations
MM_WALLET = WalletConfig("mm", "pin")
MM_WALLET2 = WalletConfig("mm2", "pin2")
TERMINATE_WALLET = WalletConfig("FJMKnwfZdd48C8NqvYrG", "bY3DxwtsCstMIIZdNpKs")
wallets = [MM_WALLET, MM_WALLET2, TERMINATE_WALLET]
from wallet_config import MM_WALLET2
def hover_and_assert_tooltip(page: Page, element_text):
element = page.get_by_text(element_text)
@@ -52,55 +40,13 @@ class TestIcebergOrdersValidations:
vega.wait_fn(1)
vega.wait_for_total_catchup()
expect(page.get_by_test_id("toast-content")).to_have_text(
"Order filledYour transaction has been confirmed View in block explorerSubmit order - filledBTC:DAI_2023+3 @ 107.00 tDAI"
"Order filledYour transaction has been confirmedView in block explorerSubmit order - filledBTC:DAI_2023+3 @ 107.00 tDAI"
)
page.get_by_test_id("All").click()
expect(
(page.get_by_role("row").locator('[col-id="type"]')).nth(1)
).to_have_text("Limit (Iceberg)")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_iceberg_tooltips(self, continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id("iceberg").hover()
expect(page.get_by_role("tooltip")).to_be_visible()
page.get_by_test_id("iceberg").click()
hover_and_assert_tooltip(page, "Peak size")
hover_and_assert_tooltip(page, "Minimum size")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_iceberg_validations(self, continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id("iceberg").click()
page.get_by_test_id("place-order").click()
expect(page.get_by_test_id("deal-ticket-peak-error-message")).to_be_visible()
expect(page.get_by_test_id("deal-ticket-peak-error-message")).to_have_text(
"You need to provide a peak size"
)
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_be_visible()
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_have_text(
"You need to provide a minimum visible size"
)
page.get_by_test_id("order-peak-size").clear()
page.get_by_test_id("order-peak-size").type("1")
page.get_by_test_id("order-minimum-size").clear()
page.get_by_test_id("order-minimum-size").type("2")
expect(page.get_by_test_id("deal-ticket-peak-error-message")).to_be_visible()
expect(page.get_by_test_id("deal-ticket-peak-error-message")).to_have_text(
"Peak size cannot be greater than the size (0)"
)
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_be_visible()
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_have_text(
"Minimum visible size cannot be greater than the peak size (1)"
)
page.get_by_test_id("order-minimum-size").clear()
page.get_by_test_id("order-minimum-size").type("0.1")
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_be_visible()
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_have_text(
"Minimum visible size cannot be lower than 1"
)
@pytest.mark.usefixtures("vega", "page", "continuous_market", "auth", "risk_accepted")
def test_iceberg_open_order(continuous_market, vega: VegaService, page: Page):
page.goto(f"/#/markets/{continuous_market}")
@@ -3,7 +3,8 @@ from playwright.sync_api import Page, expect
from vega_sim.service import VegaService
from conftest import init_vega
from fixtures.market import setup_continuous_market
from actions.utils import next_epoch
from actions.utils import next_epoch, truncate_middle, change_keys
@pytest.fixture(scope="module")
def vega(request):
@@ -20,19 +21,28 @@ def continuous_market(vega):
def test_liquidity_provision_amendment(continuous_market, vega: VegaService, page: Page):
# TODO Refactor asserting the grid
page.goto(f"/#/liquidity/{continuous_market}")
page.get_by_test_id("manage-vega-wallet").click()
#TODO Rename "mm" to "marketMaker" so that we don't have to specify where to click when switching wallets
# Currently the default click will click the middle of the element which will click the copy wallet key button
page.locator('[role="menuitemradio"] >> .mr-2.uppercase').nth(1).click(position={ "x": 0, "y": 0}, force=True)
page.reload()
change_keys(page, vega, "market_maker")
row = page.get_by_test_id("tab-myLP").locator(".ag-center-cols-container .ag-row").first
expect(row).to_contain_text(
"Active"
)
# 5002-LIQP-006
expect(page.get_by_test_id("target-stake")).to_have_text("Target stake5.82757 tDAI")
# 5002-LIQP-007
expect(page.get_by_test_id("supplied-stake")).to_have_text("Supplied stake10,000.00 tDAI")
# 5002-LIQP-008
expect(page.get_by_test_id("liquidity-supplied")).to_have_text("Liquidity supplied 171,598.11%")
expect(page.get_by_test_id("fees-paid")).to_have_text("Fees paid-")
# 5002-LIQP-009
expect(page.get_by_test_id("liquidity-market-id")).to_have_text("Market ID" + truncate_middle(continuous_market))
expect(page.get_by_test_id("liquidity-learn-more")).to_have_text("Learn moreProviding liquidity")
# 002-LIQP-010
expect(page.get_by_test_id("liquidity-learn-more").get_by_test_id("external-link")).to_have_attribute("href", "https://docs.vega.xyz/testnet/concepts/liquidity/provision")
vega.submit_simple_liquidity(
key_name="mm",
key_name="market_maker",
market_id=continuous_market,
commitment_amount=100,
commitment_amount=1,
fee=0.001,
is_amendment=True,
)
@@ -46,7 +56,30 @@ def test_liquidity_provision_amendment(continuous_market, vega: VegaService, pag
)
next_epoch(vega=vega)
page.reload()
expect(page.get_by_test_id("supplied-stake")).to_have_text("Supplied stake1.00001 tDAI")
expect(page.get_by_test_id("liquidity-supplied")).to_have_text("Liquidity supplied 17.16%")
row = page.get_by_test_id("tab-myLP").locator(".ag-center-cols-container .ag-row").first
expect(row).to_contain_text(
"Active"
)
)
@pytest.mark.skip("Waiting for the ability to cancel LP")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_liquidity_provision_inactive(continuous_market, vega: VegaService, page: Page):
# TODO Refactor asserting the grid
page.goto(f"/#/liquidity/{continuous_market}")
change_keys(page,vega, "market_maker")
row = page.get_by_test_id("tab-myLP").locator(".ag-center-cols-container .ag-row").first
expect(row).to_contain_text(
"Active"
)
vega.submit_simple_liquidity(
key_name="market_maker",
market_id=continuous_market,
commitment_amount=0,
fee=0,
is_amendment=False,
)
vega.wait_fn(1)
vega.wait_for_total_catchup()
@@ -58,7 +58,6 @@ class TestSettledMarket:
def test_settled_rows(self, page: Page, create_settled_market):
page.goto(f"/#/markets/all")
page.get_by_test_id("Closed markets").click()
row_selector = page.locator(
'[data-testid="tab-closed-markets"] .ag-center-cols-container .ag-row'
).first
@@ -72,7 +71,7 @@ class TestSettledMarket:
# 6001-MARK-009
# 6001-MARK-008
# 6001-MARK-010
pattern = r"(\d+)\s+months\s+ago"
pattern = r"(\d+)\s+(months|hours|days)\s+ago"
date_text = row_selector.locator('[col-id="settlementDate"]').inner_text()
assert re.match(pattern, date_text), f"Expected text to match pattern but got {date_text}"
+4 -14
View File
@@ -1,23 +1,12 @@
import pytest
from collections import namedtuple
from playwright.sync_api import Page, expect
from vega_sim.service import VegaService, PeggedOrder
from vega_sim.service import VegaService
from actions.vega import submit_order
from wallet_config import MM_WALLET, MM_WALLET2
import logging
logger = logging.getLogger()
# Wallet Configurations
WalletConfig = namedtuple("WalletConfig", ["name", "passphrase"])
MM_WALLET = WalletConfig("mm", "pin")
MM_WALLET2 = WalletConfig("mm2", "pin2")
TERMINATE_WALLET = WalletConfig("FJMKnwfZdd48C8NqvYrG", "bY3DxwtsCstMIIZdNpKs")
wallets = [MM_WALLET, MM_WALLET2, TERMINATE_WALLET]
table_row_selector = (
'[data-testid="tab-open-markets"] .ag-center-cols-container .ag-row'
)
@@ -85,7 +74,7 @@ def test_price_monitoring(simple_market, vega: VegaService, page: Page):
time_in_force="TIME_IN_FORCE_GTC",
volume=99,
)
#6002-MDET-009
expect(
page.get_by_test_id(liquidity_supplied).get_by_test_id(item_value)
).to_have_text("0.00 (0.00%)")
@@ -203,6 +192,7 @@ def test_price_monitoring(simple_market, vega: VegaService, page: Page):
# commented out because we have an issue #4233
# expect(page.get_by_text("Opening auction")).to_be_hidden()
#6002-MDET-009
expect(
page.get_by_test_id(liquidity_supplied).get_by_test_id(item_value)
).to_have_text("50.00 (>100%)")
@@ -25,15 +25,15 @@ def test_table_headers(page: Page, create_markets):
headers = [
"Market",
"Description",
"Settlement asset",
"Trading mode",
"Status",
"Mark price",
"24h volume",
"Settlement asset",
"Open Interest",
"Spread",
"",
]
page.wait_for_selector('[data-testid="tab-open-markets"]', state="visible")
page_headers = (
page.get_by_test_id("tab-open-markets").locator(".ag-header-cell-text").all()
@@ -157,4 +157,4 @@ def test_drag_and_drop_column(page: Page, create_markets):
page.locator(col_instrument_code).drag_to(
page.locator('.ag-header-row [col-id="data.bestBidPrice"]')
)
expect(page.locator(col_instrument_code)).to_have_attribute("aria-colindex", "8")
expect(page.locator(col_instrument_code)).to_have_attribute("aria-colindex", "9")
@@ -1,24 +1,11 @@
from math import exp
import pytest
import vega_sim.api.governance as governance
import re
from collections import namedtuple
from playwright.sync_api import Page, expect
from vega_sim.service import VegaService
from conftest import init_vega
from fixtures.market import setup_simple_market
# Defined namedtuples
WalletConfig = namedtuple("WalletConfig", ["name", "passphrase"])
# Wallet Configurations
MM_WALLET = WalletConfig("mm", "pin")
MM_WALLET2 = WalletConfig("mm2", "pin2")
TERMINATE_WALLET = WalletConfig("FJMKnwfZdd48C8NqvYrG", "bY3DxwtsCstMIIZdNpKs")
wallets = [MM_WALLET, MM_WALLET2, TERMINATE_WALLET]
from wallet_config import MM_WALLET, MM_WALLET2, TERMINATE_WALLET, wallets
row_selector = '[data-testid="tab-proposed-markets"] .ag-center-cols-container .ag-row'
col_market_id = '[col-id="market"] [data-testid="stack-cell-primary"]'
@@ -29,7 +16,6 @@ def vega(request):
with init_vega(request) as vega:
yield vega
@pytest.fixture(scope="module")
def proposed_market(vega: VegaService):
# setup market without liquidity provided
@@ -54,7 +40,6 @@ def test_can_see_table_headers(proposed_market, page: Page):
"Settlement asset",
"State",
"Parent market",
"Voting",
"Closing date",
"Enactment date",
"",
@@ -83,10 +68,6 @@ def test_renders_markets_correctly(proposed_market, page: Page):
row.locator('[col-id="terms.change.successorConfiguration.parentMarketId"]')
).to_have_text("-")
# 6001-MARK-054
# 6001-MARK-055
expect(row.get_by_test_id("vote-progress-bar-against")).to_be_visible()
# 6001-MARK-056
expect(row.locator('[col-id="closing-date"]')).not_to_be_empty()
@@ -124,8 +105,8 @@ def test_can_drag_and_drop_columns(proposed_market, page: Page):
page.goto("/#/markets/all")
page.click('[data-testid="Proposed markets"]')
col_market = page.locator('[col-id="market"]').first
col_vote = page.locator('[col-id="voting"]').first
col_market.drag_to(col_vote)
col_state = page.locator('[col-id="state"]').first
col_market.drag_to(col_state)
# Check the attribute of the dragged element
attribute_value = col_market.get_attribute("aria-colindex")
@@ -1,23 +1,11 @@
import pytest
from collections import namedtuple
from playwright.sync_api import Page, expect
from vega_sim.service import VegaService
from actions.vega import submit_order
from fixtures.market import setup_simple_market
from conftest import init_vega
from actions.utils import wait_for_toast_confirmation
# Defined namedtuples
WalletConfig = namedtuple("WalletConfig", ["name", "passphrase"])
# Wallet Configurations
MM_WALLET = WalletConfig("mm", "pin")
MM_WALLET2 = WalletConfig("mm2", "pin2")
TERMINATE_WALLET = WalletConfig("FJMKnwfZdd48C8NqvYrG", "bY3DxwtsCstMIIZdNpKs")
wallets = [MM_WALLET, MM_WALLET2, TERMINATE_WALLET]
from wallet_config import MM_WALLET, MM_WALLET2
@pytest.fixture(scope="module")
def vega(request):
@@ -1,31 +1,18 @@
import pytest
import vega_sim.api.governance as governance
import re
from collections import namedtuple
import vega_sim.api.governance as governance
from playwright.sync_api import Page, expect
from vega_sim.service import VegaService, PeggedOrder
import vega_sim.proto.vega as vega_protos
import vega_sim.api.governance as governance
from actions.vega import submit_order
from fixtures.market import setup_continuous_market
from datetime import datetime
from datetime import timedelta
from vega_sim.service import MarketStateUpdateType
from wallet_config import MM_WALLET, MM_WALLET2, GOVERNANCE_WALLET
# Defined namedtuples
WalletConfig = namedtuple("WalletConfig", ["name", "passphrase"])
# Wallet Configurations
MM_WALLET = WalletConfig("mm", "pin")
MM_WALLET2 = WalletConfig("mm2", "pin2")
GOVERNANCE_WALLET = WalletConfig("FJMKnwfZdd48C8NqvYrG", "bY3DxwtsCstMIIZdNpKs")
wallets = [MM_WALLET, MM_WALLET2, GOVERNANCE_WALLET]
@pytest.mark.usefixtures("vega", "page", "proposed_market", "risk_accepted")
def test_market_lifecycle(proposed_market, vega: VegaService, page: Page):
# 7002-SORD-001
# 7002-SORD-002
trading_mode = page.get_by_test_id("market-trading-mode").get_by_test_id(
"item-value"
)
@@ -34,9 +21,32 @@ def test_market_lifecycle(proposed_market, vega: VegaService, page: Page):
# setup market in proposed step, without liquidity provided
market_id = proposed_market
page.goto(f"/#/markets/{market_id}")
# 6002-MDET-001
expect(page.get_by_test_id("header-title")).to_have_text("BTC:DAI_2023Futr")
# 6002-MDET-002
expect(page.get_by_test_id("market-expiry")).to_have_text("ExpiryNot time-based")
page.get_by_test_id("market-expiry").hover()
expect(page.get_by_test_id("expiry-tooltip").first).to_have_text("This market expires when triggered by its oracle, not on a set date.View oracle specification")
expect(page.get_by_test_id("expiry-tooltip").first.get_by_test_id("link")).to_have_attribute("href", re.compile('.*'))
# 6002-MDET-003
expect(page.get_by_test_id("market-price")).to_have_text("Mark Price0.00")
# 6002-MDET-004
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
# 6002-MDET-005
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
# 6002-MDET-008
expect(page.get_by_test_id("market-settlement-asset")).to_have_text("Settlement assettDAI")
expect(page.get_by_test_id("liquidity-supplied")).to_have_text("Liquidity supplied 0.00 (0.00%)")
page.get_by_test_id("liquidity-supplied").hover()
expect(page.get_by_test_id("liquidity-supplied-tooltip").first).to_have_text("Supplied stake0.00Target stake0.00View liquidity provision tableLearn about providing liquidity")
expect(page.get_by_test_id("liquidity-supplied-tooltip").first.get_by_test_id("link").first).to_have_text("View liquidity provision table")
# check that market is in proposed state
# 6002-MDET-006
# 6002-MDET-007
# 7002-SORD-061
expect(trading_mode).to_have_text("No trading")
trading_mode.hover()
expect(page.get_by_test_id("trading-mode-tooltip").first).to_have_text("No trading enabled for this market.")
expect(market_state).to_have_text("Proposed")
# approve market
@@ -135,7 +145,7 @@ def test_market_closing_banners(page: Page, continuous_market, vega: VegaService
page.goto(f"/#/markets/{market_id}")
proposalID = vega.update_market_state(
continuous_market,
"mm",
"market_maker",
MarketStateUpdateType.Terminate,
approve_proposal=False,
vote_enactment_time = datetime.now() + timedelta(weeks=1),
@@ -148,7 +158,7 @@ def test_market_closing_banners(page: Page, continuous_market, vega: VegaService
vega.update_market_state(
continuous_market,
"mm",
"market_maker",
MarketStateUpdateType.Terminate,
approve_proposal=False,
vote_enactment_time = datetime.now() + timedelta(weeks=1),
@@ -161,7 +171,7 @@ def test_market_closing_banners(page: Page, continuous_market, vega: VegaService
governance.approve_proposal(
proposal_id=proposalID,
wallet=vega.wallet,
key_name="mm"
key_name="market_maker"
)
vega.forward("60s")
@@ -1,19 +1,10 @@
import pytest
from collections import namedtuple
from playwright.sync_api import Page, expect
from vega_sim.service import VegaService
from typing import List
from actions.vega import submit_order, submit_liquidity, submit_multiple_orders
from conftest import init_vega
from fixtures.market import setup_simple_market
# Defined namedtuples
WalletConfig = namedtuple("WalletConfig", ["name", "passphrase"])
# Wallet Configurations
MM_WALLET = WalletConfig("mm", "pin")
MM_WALLET2 = WalletConfig("mm2", "pin2")
from wallet_config import MM_WALLET, MM_WALLET2
@pytest.fixture(scope="module")
def vega():
@@ -0,0 +1,138 @@
import pytest
import re
from playwright.sync_api import Page, expect
from vega_sim.service import VegaService
from vega_sim.service import MarketStateUpdateType
from datetime import datetime, timedelta
from conftest import init_vega
from actions.utils import change_keys
from actions.vega import submit_multiple_orders
from fixtures.market import setup_perps_market
from wallet_config import MM_WALLET, MM_WALLET2, TERMINATE_WALLET
row_selector = '[data-testid="tab-funding-payments"] .ag-center-cols-container .ag-row'
col_amount = '[col-id="amount"]'
class TestPerpetuals:
@pytest.fixture(scope="class")
def vega(self, request):
with init_vega(request) as vega:
yield vega
@pytest.fixture(scope="class")
def perps_market(self, vega: VegaService):
perps_market = setup_perps_market(vega)
submit_multiple_orders(
vega, MM_WALLET.name, perps_market, "SIDE_SELL", [[1, 110], [1, 105]]
)
submit_multiple_orders(
vega, MM_WALLET2.name, perps_market, "SIDE_BUY", [[1, 90], [1, 95]]
)
vega.submit_settlement_data(
settlement_key=TERMINATE_WALLET.name,
settlement_price=110,
market_id=perps_market,
)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
submit_multiple_orders(
vega, MM_WALLET.name, perps_market, "SIDE_SELL", [[1, 110], [1, 105]]
)
submit_multiple_orders(
vega, MM_WALLET2.name, perps_market, "SIDE_BUY", [[1, 112], [1, 115]]
)
vega.submit_settlement_data(
settlement_key=TERMINATE_WALLET.name,
settlement_price=110,
market_id=perps_market,
)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
return perps_market
@pytest.mark.usefixtures("page","risk_accepted", "auth")
def test_funding_payment_profit(self, perps_market, page: Page):
page.goto(f"/#/markets/{perps_market}")
page.get_by_test_id("Funding payments").click()
row = page.locator(row_selector)
expect(row.locator(col_amount)).to_have_text("9.00 tDAI")
@pytest.mark.usefixtures("page","risk_accepted", "auth")
def test_funding_payment_loss(self, perps_market, page: Page, vega):
page.goto(f"/#/markets/{perps_market}")
change_keys(page, vega, "market_maker")
page.get_by_test_id("Funding payments").click()
row = page.locator(row_selector)
expect(row.locator(col_amount)).to_have_text("-27.00 tDAI")
@pytest.mark.usefixtures("page","risk_accepted", "auth")
def test_funding_header(self, perps_market, page: Page):
page.goto(f"/#/markets/{perps_market}")
expect(page.get_by_test_id("market-funding")).to_contain_text("Funding Rate / Countdown-8.1818%")
expect(page.get_by_test_id("index-price")).to_have_text("Index Price110.00")
@pytest.mark.skip("Skipped due to issue #5421")
@pytest.mark.usefixtures("page","risk_accepted", "auth")
def test_funding_payment_history(perps_market, page: Page, vega):
page.goto(f"/#/markets/{perps_market}")
change_keys(page, vega, "market_maker")
page.get_by_test_id("Funding history").click()
element = page.get_by_test_id("tab-funding-history")
# Get the bounding box of the element
bounding_box = element.bounding_box()
if bounding_box:
bottom_right_x = bounding_box["x"] + bounding_box["width"]
bottom_right_y = bounding_box["y"] + bounding_box["height"]
# Hover over the bottom-right corner of the element
element.hover(position={"x": bottom_right_x, "y": bottom_right_y})
else:
print("Bounding box not found for the element")
@pytest.mark.usefixtures("page","risk_accepted", "auth")
def test_perps_market_termination_proposed(page: Page, vega: VegaService):
perpetual_market = setup_perps_market(vega)
page.goto(f"/#/markets/{perpetual_market}")
vega.update_market_state(
proposal_key=MM_WALLET.name,
market_id=perpetual_market,
market_state=MarketStateUpdateType.Terminate,
price=100,
vote_closing_time = datetime.now() + timedelta(seconds=15),
vote_enactment_time = datetime.now() + timedelta(seconds=60),
approve_proposal = True,
forward_time_to_enactment = False,
)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
banner_text = page.get_by_test_id(f"termination-warning-banner-{perpetual_market}").text_content()
pattern = re.compile(
r"Trading on Market BTC:DAI_Perpetual may stop on \d{2} [A-Za-z]+\. There is open proposal to close this market\.Proposed final price is 100\.00 BTC\.View proposal"
)
assert pattern.search(banner_text), f"Text did not match pattern. Text was: {banner_text}"
@pytest.mark.usefixtures("page","risk_accepted", "auth" )
def test_perps_market_terminated(page: Page, vega: VegaService):
perpetual_market = setup_perps_market(vega)
page.goto(f"/#/markets/{perpetual_market}")
vega.update_market_state(
proposal_key=MM_WALLET.name,
market_id=perpetual_market,
market_state=MarketStateUpdateType.Terminate,
price=100,
approve_proposal = True,
forward_time_to_enactment = True,
)
expect(page.get_by_test_id("market-price")).to_have_text("Mark Price100.00")
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)-")
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
expect(page.get_by_test_id("market-trading-mode")).to_have_text("Trading modeNo trading")
expect(page.get_by_test_id("market-state")).to_have_text("StatusClosed")
expect(page.get_by_test_id("liquidity-supplied")).to_have_text("Liquidity supplied 0.00 (0.00%)")
expect(page.get_by_test_id("market-funding")).to_have_text("Funding Rate / Countdown-Unknown")
expect(page.get_by_test_id("index-price")).to_have_text("Index Price-")
expect(page.get_by_test_id("deal-ticket-error-message-summary")).to_have_text("This market is closed and not accepting orders")
+7 -9
View File
@@ -2,6 +2,7 @@ import pytest
from playwright.sync_api import Page
from vega_sim.service import VegaService
from actions.vega import submit_order
from actions.utils import change_keys
def check_pnl_color_value(element, expected_color, expected_value):
color = element.evaluate("element => getComputedStyle(element).color")
@@ -29,18 +30,16 @@ def test_pnl(continuous_market, vega: VegaService, page: Page):
check_pnl_color_value(unrealised_pnl, "rgb(236, 0, 60)", "-4.00")
# profit Trading unrealised
page.get_by_test_id("manage-vega-wallet").click()
page.locator('[role="menuitemradio"] >> .mr-2.uppercase').nth(1).click(position={ "x": 0, "y": 0}, force=True)
change_keys(page, vega, "market_maker")
check_pnl_color_value(realised_pnl, "rgb(0, 0, 0)", "0.00")
check_pnl_color_value(unrealised_pnl, "rgb(1, 145, 75)", "4.00")
# neutral Trading unrealised
page.locator('[role="menuitemradio"] >> .mr-2.uppercase').nth(2).click(position={ "x": 0, "y": 0}, force=True)
change_keys(page, vega, "market_maker_2")
check_pnl_color_value(realised_pnl, "rgb(0, 0, 0)", "0.00")
check_pnl_color_value(unrealised_pnl, "rgb(0, 0, 0)", "0.00")
# Portfolio Unrealised
page.get_by_test_id("manage-vega-wallet").click(force=True)
page.get_by_role("link", name="Portfolio").click()
page.get_by_test_id("Positions").click()
page.wait_for_selector(
@@ -52,10 +51,10 @@ def test_pnl(continuous_market, vega: VegaService, page: Page):
'//div[@role="row" and .//div[@col-id="partyId"]/div/span[text()="Key 1"]]'
)
key_mm = page.query_selector(
'//div[@role="row" and .//div[@col-id="partyId"]/div/span[text()="mm"]]'
'//div[@role="row" and .//div[@col-id="partyId"]/div/span[text()="market_maker"]]'
)
key_mm2 = page.query_selector(
'//div[@role="row" and .//div[@col-id="partyId"]/div/span[text()="mm2"]]'
'//div[@role="row" and .//div[@col-id="partyId"]/div/span[text()="market_maker_2"]]'
)
key_1_unrealised_pnl = key_1.query_selector('xpath=./div[@col-id="unrealisedPNL"]')
@@ -101,12 +100,11 @@ def test_pnl(continuous_market, vega: VegaService, page: Page):
check_pnl_color_value(unrealised_pnl, "rgb(0, 0, 0)", "0.00")
# profit trading realised
page.get_by_test_id("manage-vega-wallet").click()
page.locator('[role="menuitemradio"] >> .mr-2.uppercase').nth(1).click(position={ "x": 0, "y": 0}, force=True)
change_keys(page, vega, "market_maker")
check_pnl_color_value(realised_pnl, "rgb(1, 145, 75)", "8.00")
check_pnl_color_value(unrealised_pnl, "rgb(0, 0, 0)", "0.00")
# loss trading realised
page.locator('[role="menuitemradio"] >> .mr-2.uppercase').nth(0).click(position={ "x": 0, "y": 0}, force=True)
change_keys(page, vega, "Key 1")
check_pnl_color_value(realised_pnl, "rgb(236, 0, 60)", "-8.00")
check_pnl_color_value(unrealised_pnl, "rgb(0, 0, 0)", "0.00")
@@ -32,12 +32,12 @@ def test_share_usage_data(page: Page):
# Define a mapping of icon selectors to toast selectors
ICON_TO_TOAST = {
'aria-label="arrow-top-left icon"': 'class="group absolute z-20 top-0 left-0 max-w-full max-h-full overflow-x-hidden overflow-y-auto p-4"',
'aria-label="arrow-up icon"': 'class="group absolute z-20 top-0 left-[50%] translate-x-[-50%] max-w-full max-h-full overflow-x-hidden overflow-y-auto p-4"',
'aria-label="arrow-top-right icon"': 'class="group absolute z-20 top-0 right-0 max-w-full max-h-full overflow-x-hidden overflow-y-auto p-4"',
'aria-label="arrow-bottom-left icon"': 'class="group absolute z-20 bottom-0 left-0 max-w-full max-h-full overflow-x-hidden overflow-y-auto p-4"',
'aria-label="arrow-down icon"': 'class="group absolute z-20 bottom-0 left-[50%] translate-x-[-50%] max-w-full max-h-full overflow-x-hidden overflow-y-auto p-4"',
'aria-label="arrow-bottom-right icon"': 'class="group absolute z-20 bottom-0 right-0 max-w-full max-h-full overflow-x-hidden overflow-y-auto p-4"',
'aria-label="arrow-top-left icon"': 'class="relative flex-1 overflow-auto p-4 pr-[40px] [&>p]:mb-[2.5px]"',
'aria-label="arrow-up icon"': 'class="relative flex-1 overflow-auto p-4 pr-[40px] [&>p]:mb-[2.5px]"',
'aria-label="arrow-top-right icon"': 'class="relative flex-1 overflow-auto p-4 pr-[40px] [&>p]:mb-[2.5px]"',
'aria-label="arrow-bottom-left icon"': 'class="relative flex-1 overflow-auto p-4 pr-[40px] [&>p]:mb-[2.5px]"',
'aria-label="arrow-down icon"': 'class="relative flex-1 overflow-auto p-4 pr-[40px] [&>p]:mb-[2.5px]"',
'aria-label="arrow-bottom-right icon"': 'class="relative flex-1 overflow-auto p-4 pr-[40px] [&>p]:mb-[2.5px]"',
}
@@ -18,8 +18,8 @@
# page.goto(f"/#/markets/{continuous_market}")
# vega.forward("24h")
# vega.wait_for_total_catchup()
# submit_order(vega, "mm", continuous_market, "SIDE_SELL", 1, 101.50000)
# submit_order(vega, "mm2", continuous_market, "SIDE_SELL", 1, 101.50000)
# submit_order(vega, "market_maker", continuous_market, "SIDE_SELL", 1, 101.50000)
# submit_order(vega, "market_maker_2", continuous_market, "SIDE_SELL", 1, 101.50000)
# vega.forward("10s")
# vega.wait_for_total_catchup()

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