Compare commits
91
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2b0a60124 | ||
|
|
1970ea8cc2 | ||
|
|
5f57234e4e | ||
|
|
7d96d9bcd1 | ||
|
|
345be81142 | ||
|
|
2221e9faca | ||
|
|
7c0139e107 | ||
|
|
a157ff1e05 | ||
|
|
2e9fa0c52e | ||
|
|
c0790c1e93 | ||
|
|
13c1044f8e | ||
|
|
cf9f313e4c | ||
|
|
f56d34fe6e | ||
|
|
1379366caa | ||
|
|
8958c12fe3 | ||
|
|
fef13874db | ||
|
|
40f02ecf89 | ||
|
|
41cd6b1455 | ||
|
|
3cd393dac0 | ||
|
|
a52e60d6a2 | ||
|
|
bc13f1b359 | ||
|
|
eb81f4ae44 | ||
|
|
51ab02a2e2 | ||
|
|
ffada1b93d | ||
|
|
9dda3f712b | ||
|
|
df20dbeee0 | ||
|
|
cdfd8a2d00 | ||
|
|
1e5c523bc4 | ||
|
|
37cd69ba6e | ||
|
|
2c11045dd9 | ||
|
|
9aef41a119 | ||
|
|
80ab8821d0 | ||
|
|
8a3657a9b9 | ||
|
|
7100b0e9fc | ||
|
|
614a83b7d6 | ||
|
|
3dc77b0eff | ||
|
|
a59f7dfd29 | ||
|
|
61471228aa | ||
|
|
127e784ceb | ||
|
|
70d748fb15 | ||
|
|
e06f4818fc | ||
|
|
8182da3b31 | ||
|
|
c8c56307bb | ||
|
|
a8cd7f157f | ||
|
|
4f18caa486 | ||
|
|
5c7c626bbc | ||
|
|
e4c4c20631 | ||
|
|
0697302d07 | ||
|
|
2d926c0ce0 | ||
|
|
f57d6a7c7b | ||
|
|
15f905046f | ||
|
|
4f7918f64e | ||
|
|
52ab0562b0 | ||
|
|
4e2b0d1b1d | ||
|
|
0b0bcad9b3 | ||
|
|
bcf17bb34e | ||
|
|
a2b9b0da05 | ||
|
|
7588d0cd11 | ||
|
|
5ee1748495 | ||
|
|
73a118978f | ||
|
|
eac26c1966 | ||
|
|
d615587564 | ||
|
|
ba4ce1ce88 | ||
|
|
3bbacc1aa0 | ||
|
|
de5371435d | ||
|
|
12cb5e10b6 | ||
|
|
ee2909cc84 | ||
|
|
068d6abf1b | ||
|
|
e5d4d2b0b8 | ||
|
|
d4e801cfc6 | ||
|
|
964deb2f23 | ||
|
|
6669125dd3 | ||
|
|
129b6c4e89 | ||
|
|
ca418cabfe | ||
|
|
192af844c4 | ||
|
|
6a841f226a | ||
|
|
76426baa2a | ||
|
|
6fdac2419c | ||
|
|
87807d2088 | ||
|
|
a5f2533a66 | ||
|
|
1e9251c9c0 | ||
|
|
2a27e124d7 | ||
|
|
4ee85720ab | ||
|
|
a19da4c408 | ||
|
|
65ad4fffda | ||
|
|
11317cdb2e | ||
|
|
c98870f27d | ||
|
|
c0c56e4c48 | ||
|
|
d9dc43b359 | ||
|
|
06a6fe6d67 | ||
|
|
a9ca215276 |
@@ -77,6 +77,7 @@
|
||||
"fixStyle": "inline-type-imports"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-useless-constructor": 0,
|
||||
"curly": ["error", "multi-line"]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
@@ -59,5 +59,4 @@ apps/trading/e2e/logs/
|
||||
apps/trading/e2e/.pytest_cache/
|
||||
apps/trading/e2e/traces/
|
||||
|
||||
.nx/cache
|
||||
|
||||
.nx/
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
# Auto-format all files
|
||||
yarn nx format:write
|
||||
|
||||
# Lint all staged files
|
||||
yarn lint-staged
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
# Lint all staged files
|
||||
yarn nx format:check
|
||||
# Lint all staged files - this brings more value as pre-commit
|
||||
# yarn nx format:check
|
||||
|
||||
# Test all projects with changes
|
||||
yarn nx affected -t test --exclude trading
|
||||
# yarn nx affected -t test --exclude trading
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Time } from '../time';
|
||||
import { sideText, statusText, tifFull, tifShort } from './lib/order-labels';
|
||||
import SizeInMarket from '../size-in-market/size-in-market';
|
||||
import { TxOrderPeggedReference } from '../txs/details/order/tx-order-peg';
|
||||
import { OrderTypeMapping } from '@vegaprotocol/types';
|
||||
|
||||
export interface DeterministicOrderDetailsProps {
|
||||
id: string;
|
||||
@@ -69,7 +70,7 @@ const DeterministicOrderDetails = ({
|
||||
<span className="mx-5 text-base">@</span>
|
||||
<PriceInMarket price={o.price} marketId={o.market.id} />
|
||||
</h2>
|
||||
<p className="text-gray-200">
|
||||
<p className="text-gray-400 dark:text-gray-600">
|
||||
In <MarketLink id={o.market.id} /> at <Time date={o.createdAt} />.
|
||||
</p>
|
||||
{o.peggedOrder ? (
|
||||
@@ -83,13 +84,12 @@ const DeterministicOrderDetails = ({
|
||||
/>
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{o.reference ? (
|
||||
<p className="text-gray-500 mt-4">
|
||||
<span>{t('Reference')}</span>: {o.reference}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="grid md:grid-cols-4 gap-x-6 mt-4">
|
||||
<div className="grid md:grid-cols-5 gap-x-6 mt-4">
|
||||
<div className="mb-12 md:mb-0">
|
||||
<h2 className="text-2xl font-bold text-dark mb-4">
|
||||
{t('Status')}
|
||||
@@ -114,6 +114,16 @@ const DeterministicOrderDetails = ({
|
||||
{o.version}
|
||||
</h5>
|
||||
</div>
|
||||
{o.type ? (
|
||||
<div className="">
|
||||
<h2 className="text-2xl font-bold text-dark mb-4">
|
||||
{t('Type')}
|
||||
</h2>
|
||||
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
||||
{OrderTypeMapping[o.type]}
|
||||
</h5>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -80,6 +80,12 @@ export function getLabelForOrderType(
|
||||
if (command.orderSubmission.icebergOpts) {
|
||||
return 'Iceberg';
|
||||
}
|
||||
if (command.orderSubmission.type === 'TYPE_MARKET') {
|
||||
return 'Market order';
|
||||
}
|
||||
if (command.orderSubmission.type === 'TYPE_LIMIT') {
|
||||
return 'Limit order';
|
||||
}
|
||||
}
|
||||
return 'Order';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -49,12 +49,8 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
|
||||
? activeProvider
|
||||
: defaultProvider;
|
||||
|
||||
if (
|
||||
account &&
|
||||
activeProvider &&
|
||||
typeof activeProvider.getSigner === 'function'
|
||||
) {
|
||||
signer = provider.getSigner();
|
||||
if (account && provider && typeof provider.getSigner === 'function') {
|
||||
signer = provider.getSigner(account);
|
||||
}
|
||||
|
||||
const tokenVestingAddress =
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+2
-2
@@ -54,8 +54,8 @@ export const ProposalReferralProgramDetails = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const benefitTiers = proposal?.terms?.change?.benefitTiers;
|
||||
const stakingTiers = proposal?.terms?.change?.stakingTiers;
|
||||
const benefitTiers = proposal?.terms?.change?.benefitTiers.slice();
|
||||
const stakingTiers = proposal?.terms?.change?.stakingTiers.slice();
|
||||
const windowLength = proposal?.terms?.change?.windowLength;
|
||||
const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgram;
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
+3
-2
@@ -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={{
|
||||
|
||||
+20
-1
@@ -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">
|
||||
|
||||
@@ -104,6 +104,10 @@
|
||||
list-style: circle;
|
||||
}
|
||||
|
||||
.react-markdown-container a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.jsondiffpatch-delta,
|
||||
.jsondiffpatch-delta pre {
|
||||
font-family: 'Roboto Mono', monospace !important;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -22,9 +22,9 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_STOP_ORDERS=true
|
||||
NX_ICEBERG_ORDERS=true
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_REFERRALS=false
|
||||
NX_REFERRALS=true
|
||||
# NX_DISABLE_CLOSE_POSITION=false
|
||||
|
||||
NX_TENDERMINT_URL=https://be.vega.community
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||
|
||||
@@ -23,7 +23,6 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_STOP_ORDERS=true
|
||||
NX_ICEBERG_ORDERS=true
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_REFERRALS=true
|
||||
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
import { useEffect } from 'react';
|
||||
import { titlefy } from '@vegaprotocol/utils';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
import { FeesContainer } from '../../components/fees-container';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
|
||||
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>
|
||||
<FeesContainer />
|
||||
</div>
|
||||
<ErrorBoundary feature="fees">
|
||||
<div className="container p-4 mx-auto">
|
||||
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
|
||||
<FeesContainer />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { LiquidityContainer } from '../../components/liquidity-container';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
|
||||
const enum LiquidityTabs {
|
||||
Active = 'active',
|
||||
@@ -58,19 +59,28 @@ export const LiquidityViewContainer = ({
|
||||
name={t('My liquidity provision')}
|
||||
hidden={!pubKey}
|
||||
>
|
||||
<LiquidityContainer
|
||||
marketId={marketId}
|
||||
filter={{ partyId: pubKey || undefined }}
|
||||
/>
|
||||
<ErrorBoundary feature="liquidity-party">
|
||||
<LiquidityContainer
|
||||
marketId={marketId}
|
||||
filter={{ partyId: pubKey || undefined }}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id={LiquidityTabs.Active} name={t('Active')}>
|
||||
<LiquidityContainer marketId={marketId} filter={{ active: true }} />
|
||||
<ErrorBoundary feature="liquidity-active">
|
||||
<LiquidityContainer
|
||||
marketId={marketId}
|
||||
filter={{ active: true }}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id={LiquidityTabs.Inactive} name={t('Inactive')}>
|
||||
<LiquidityContainer
|
||||
marketId={marketId}
|
||||
filter={{ active: false }}
|
||||
/>
|
||||
<ErrorBoundary feature="liquidity-inactive">
|
||||
<LiquidityContainer
|
||||
marketId={marketId}
|
||||
filter={{ active: false }}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '../../components/market-banner';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
|
||||
interface TradeGridProps {
|
||||
market: Market | null;
|
||||
@@ -62,28 +63,38 @@ const MainGrid = memo(
|
||||
name={t('Chart')}
|
||||
menu={<TradingViews.candles.menu />}
|
||||
>
|
||||
<TradingViews.candles.component marketId={marketId} />
|
||||
<ErrorBoundary feature="chart">
|
||||
<TradingViews.candles.component marketId={marketId} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="depth" name={t('Depth')}>
|
||||
<TradingViews.depth.component marketId={marketId} />
|
||||
<ErrorBoundary feature="depth">
|
||||
<TradingViews.depth.component marketId={marketId} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="liquidity" name={t('Liquidity')}>
|
||||
<TradingViews.liquidity.component marketId={marketId} />
|
||||
<ErrorBoundary feature="liquidity">
|
||||
<TradingViews.liquidity.component marketId={marketId} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
{market &&
|
||||
market.tradableInstrument.instrument.product.__typename ===
|
||||
'Perpetual' ? (
|
||||
<Tab id="funding-history" name={t('Funding history')}>
|
||||
<TradingViews.funding.component marketId={marketId} />
|
||||
<ErrorBoundary feature="funding-history">
|
||||
<TradingViews.funding.component marketId={marketId} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
) : null}
|
||||
{market &&
|
||||
market.tradableInstrument.instrument.product.__typename ===
|
||||
'Perpetual' ? (
|
||||
<Tab id="funding-payments" name={t('Funding payments')}>
|
||||
<TradingViews.fundingPayments.component
|
||||
marketId={marketId}
|
||||
/>
|
||||
<ErrorBoundary feature="funding-payments">
|
||||
<TradingViews.fundingPayments.component
|
||||
marketId={marketId}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
) : null}
|
||||
</Tabs>
|
||||
@@ -96,10 +107,14 @@ const MainGrid = memo(
|
||||
<TradeGridChild>
|
||||
<Tabs storageKey="console-trade-grid-main-right">
|
||||
<Tab id="orderbook" name={t('Orderbook')}>
|
||||
<TradingViews.orderbook.component marketId={marketId} />
|
||||
<ErrorBoundary feature="orderbook">
|
||||
<TradingViews.orderbook.component marketId={marketId} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="trades" name={t('Trades')}>
|
||||
<TradingViews.trades.component marketId={marketId} />
|
||||
<ErrorBoundary feature="trades">
|
||||
<TradingViews.trades.component marketId={marketId} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</TradeGridChild>
|
||||
@@ -118,31 +133,43 @@ const MainGrid = memo(
|
||||
name={t('Positions')}
|
||||
menu={<TradingViews.positions.menu />}
|
||||
>
|
||||
<TradingViews.positions.component />
|
||||
<ErrorBoundary feature="positions">
|
||||
<TradingViews.positions.component />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab
|
||||
id="open-orders"
|
||||
name={t('Open')}
|
||||
menu={<TradingViews.activeOrders.menu />}
|
||||
>
|
||||
<TradingViews.activeOrders.component />
|
||||
<ErrorBoundary feature="activeOrders">
|
||||
<TradingViews.activeOrders.component />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="closed-orders" name={t('Closed')}>
|
||||
<TradingViews.closedOrders.component />
|
||||
<ErrorBoundary feature="closedOrders">
|
||||
<TradingViews.closedOrders.component />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="rejected-orders" name={t('Rejected')}>
|
||||
<TradingViews.rejectedOrders.component />
|
||||
<ErrorBoundary feature="rejectedOrders">
|
||||
<TradingViews.rejectedOrders.component />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab
|
||||
id="orders"
|
||||
name={t('All')}
|
||||
menu={<TradingViews.orders.menu />}
|
||||
>
|
||||
<TradingViews.orders.component />
|
||||
<ErrorBoundary feature="orders">
|
||||
<TradingViews.orders.component />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
{FLAGS.STOP_ORDERS ? (
|
||||
<Tab id="stop-orders" name={t('Stop orders')}>
|
||||
<TradingViews.stopOrders.component />
|
||||
<ErrorBoundary feature="stop-orders">
|
||||
<TradingViews.stopOrders.component />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
) : null}
|
||||
<Tab id="fills" name={t('Fills')}>
|
||||
@@ -153,7 +180,11 @@ const MainGrid = memo(
|
||||
name={t('Collateral')}
|
||||
menu={<TradingViews.collateral.menu />}
|
||||
>
|
||||
<TradingViews.collateral.component pinnedAsset={pinnedAsset} />
|
||||
<ErrorBoundary feature="collateral">
|
||||
<TradingViews.collateral.component
|
||||
pinnedAsset={pinnedAsset}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</TradeGridChild>
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import type { PinnedAsset } from '@vegaprotocol/accounts';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import { type PinnedAsset } from '@vegaprotocol/accounts';
|
||||
import { type Market } from '@vegaprotocol/markets';
|
||||
import { OracleBanner } from '@vegaprotocol/markets';
|
||||
import type { TradingView } from './trade-views';
|
||||
import { TradingViews } from './trade-views';
|
||||
import { useState } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import classNames from 'classnames';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import {
|
||||
MarketSuccessorBanner,
|
||||
MarketSuccessorProposalBanner,
|
||||
MarketTerminationBanner,
|
||||
} from '../../components/market-banner';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
import { type TradingView } from './trade-views';
|
||||
import { TradingViews } from './trade-views';
|
||||
|
||||
interface TradePanelsProps {
|
||||
market: Market | null;
|
||||
@@ -34,7 +35,11 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||
|
||||
// Watch out here, we don't know what component is being rendered
|
||||
// so watch out for clashes in props
|
||||
return <Component marketId={market?.id} pinnedAsset={pinnedAsset} />;
|
||||
return (
|
||||
<ErrorBoundary feature={view}>
|
||||
<Component marketId={market?.id} pinnedAsset={pinnedAsset} />;
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMenu = () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { act, render, screen, waitFor, within } from '@testing-library/react';
|
||||
// import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Closed } from './closed';
|
||||
import { MarketStateMapping, PropertyKeyType } from '@vegaprotocol/types';
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
marketsDataQuery,
|
||||
createMarketsDataFragment,
|
||||
} from '@vegaprotocol/mock';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('Closed', () => {
|
||||
let originalNow: typeof Date.now;
|
||||
@@ -168,14 +170,11 @@ describe('Closed', () => {
|
||||
Date.now = originalNow;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('renders correctly formatted and filtered rows', async () => {
|
||||
const renderComponent = async (mocks: MockedResponse[]) => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[marketsMock, marketsDataMock, oracleDataMock]}
|
||||
>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
@@ -185,6 +184,10 @@ describe('Closed', () => {
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
it('renders correct headers', async () => {
|
||||
await renderComponent([marketsMock, marketsDataMock, oracleDataMock]);
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
const expectedHeaders = [
|
||||
@@ -200,6 +203,10 @@ describe('Closed', () => {
|
||||
];
|
||||
expect(headers).toHaveLength(expectedHeaders.length);
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
|
||||
});
|
||||
|
||||
it('renders correctly formatted and filtered rows', async () => {
|
||||
await renderComponent([marketsMock, marketsDataMock, oracleDataMock]);
|
||||
|
||||
const assetSymbol = getAsset(market).symbol;
|
||||
|
||||
@@ -273,21 +280,8 @@ describe('Closed', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[mixedMarketsMock, marketsDataMock, oracleDataMock]}
|
||||
>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
<Closed />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
|
||||
await renderComponent([mixedMarketsMock, marketsDataMock, oracleDataMock]);
|
||||
|
||||
// check that the number of rows in datagrid is 2
|
||||
const container = within(
|
||||
@@ -319,8 +313,67 @@ describe('Closed', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('successor marked should be visible', async () => {
|
||||
it('display market actions', async () => {
|
||||
// Use market with a succcessor Id as the actions dropdown will optionally
|
||||
// show a link to the successor market
|
||||
const marketsWithSuccessorAndParent = [
|
||||
{
|
||||
__typename: 'MarketEdge' as const,
|
||||
node: createMarketFragment({
|
||||
id: 'include-0',
|
||||
state: MarketState.STATE_SETTLED,
|
||||
successorMarketID: 'successor',
|
||||
parentMarketID: 'parent',
|
||||
}),
|
||||
},
|
||||
];
|
||||
const mockWithSuccessorAndParent: MockedResponse<MarketsQuery> = {
|
||||
request: {
|
||||
query: MarketsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
__typename: 'MarketConnection',
|
||||
edges: marketsWithSuccessorAndParent,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await renderComponent([
|
||||
mockWithSuccessorAndParent,
|
||||
marketsDataMock,
|
||||
oracleDataMock,
|
||||
]);
|
||||
|
||||
const actionCell = screen
|
||||
.getAllByRole('gridcell')
|
||||
.find((el) => el.getAttribute('col-id') === 'market-actions');
|
||||
|
||||
await userEvent.click(
|
||||
within(actionCell as HTMLElement).getByTestId('dropdown-menu')
|
||||
);
|
||||
|
||||
expect(screen.getByRole('menu')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'Copy Market ID' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'View on Explorer' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'View settlement asset details' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'View parent market' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'View successor market' })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('successor market should be visible', async () => {
|
||||
const marketsWithSuccessorID = [
|
||||
{
|
||||
__typename: 'MarketEdge' as const,
|
||||
@@ -345,21 +398,11 @@ describe('Closed', () => {
|
||||
},
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[mockWithSuccessors, marketsDataMock, oracleDataMock]}
|
||||
>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
<Closed />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
await renderComponent([
|
||||
mockWithSuccessors,
|
||||
marketsDataMock,
|
||||
oracleDataMock,
|
||||
]);
|
||||
|
||||
const container = within(
|
||||
document.querySelector('.ag-center-cols-container') as HTMLElement
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
useLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
|
||||
export const MarketsPage = () => {
|
||||
const t = useT();
|
||||
@@ -34,7 +35,9 @@ export const MarketsPage = () => {
|
||||
<div className="h-full my-1 border rounded-sm border-default">
|
||||
<Tabs storageKey="console-markets">
|
||||
<Tab id="open-markets" name={t('Open markets')}>
|
||||
<OpenMarkets />
|
||||
<ErrorBoundary feature="markets-open">
|
||||
<OpenMarkets />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab
|
||||
id="proposed-markets"
|
||||
@@ -49,10 +52,14 @@ export const MarketsPage = () => {
|
||||
</TradingAnchorButton>
|
||||
}
|
||||
>
|
||||
<Proposed />
|
||||
<ErrorBoundary feature="markets-proposed">
|
||||
<Proposed />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="closed-markets" name={t('Closed markets')}>
|
||||
<Closed />
|
||||
<ErrorBoundary feature="markets-closed">
|
||||
<Closed />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { act, render, screen, within } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { OpenMarkets } from './open-markets';
|
||||
import { Interval } from '@vegaprotocol/types';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type {
|
||||
MarketsDataQuery,
|
||||
MarketsQuery,
|
||||
MarketCandlesQuery,
|
||||
MarketFieldsFragment,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
MarketsDataDocument,
|
||||
MarketsDocument,
|
||||
MarketsCandlesDocument,
|
||||
} from '@vegaprotocol/markets';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
marketsQuery,
|
||||
marketsDataQuery,
|
||||
marketsCandlesQuery,
|
||||
} from '@vegaprotocol/mock';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('Open', () => {
|
||||
let originalNow: typeof Date.now;
|
||||
const mockNowTimestamp = 1672531200000;
|
||||
const pubKey = 'pubKey';
|
||||
|
||||
const marketsQueryData = marketsQuery();
|
||||
const marketsMock: MockedResponse<MarketsQuery> = {
|
||||
request: {
|
||||
query: MarketsDocument,
|
||||
},
|
||||
result: {
|
||||
data: marketsQueryData,
|
||||
},
|
||||
};
|
||||
|
||||
const marketsCandlesQueryData = marketsCandlesQuery();
|
||||
const marketsCandlesMock: MockedResponse<MarketCandlesQuery> = {
|
||||
request: {
|
||||
query: MarketsCandlesDocument,
|
||||
variables: {
|
||||
interval: Interval.INTERVAL_I1H,
|
||||
since: '2022-12-31T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: marketsCandlesQueryData,
|
||||
},
|
||||
};
|
||||
|
||||
const marketsDataQueryData = marketsDataQuery();
|
||||
|
||||
const marketsDataMock: MockedResponse<MarketsDataQuery> = {
|
||||
request: {
|
||||
query: MarketsDataDocument,
|
||||
},
|
||||
result: {
|
||||
data: marketsDataQueryData,
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
originalNow = Date.now;
|
||||
Date.now = jest.fn().mockReturnValue(mockNowTimestamp);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Date.now = originalNow;
|
||||
});
|
||||
|
||||
const renderComponent = async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[marketsMock, marketsCandlesMock, marketsDataMock]}
|
||||
>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
<OpenMarkets />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
it('renders correct headers', async () => {
|
||||
await renderComponent();
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
const expectedHeaders = [
|
||||
'Market',
|
||||
'Description',
|
||||
'Settlement asset',
|
||||
'Trading mode',
|
||||
'Status',
|
||||
'Mark price',
|
||||
'24h volume',
|
||||
'Open Interest',
|
||||
'Spread',
|
||||
'', // Action row
|
||||
];
|
||||
expect(headers).toHaveLength(expectedHeaders.length);
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
|
||||
});
|
||||
|
||||
it('sort columns', async () => {
|
||||
await renderComponent();
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
const marketHeader = headers.find(
|
||||
(h) => h.getAttribute('col-id') === 'tradableInstrument.instrument.code'
|
||||
);
|
||||
if (!marketHeader) {
|
||||
throw new Error('No market header found');
|
||||
}
|
||||
expect(marketHeader).toHaveAttribute('aria-sort', 'none');
|
||||
await userEvent.click(within(marketHeader).getByText(/market/i));
|
||||
// 6001-MARK-064
|
||||
expect(marketHeader).toHaveAttribute('aria-sort', 'ascending');
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests, jest/expect-expect
|
||||
it('renders row', async () => {
|
||||
await renderComponent();
|
||||
|
||||
const container = within(
|
||||
document.querySelector('.ag-center-cols-container') as HTMLElement
|
||||
);
|
||||
|
||||
const markets = marketsQueryData.marketsConnection?.edges.map(
|
||||
(e) => e.node
|
||||
) as MarketFieldsFragment[];
|
||||
|
||||
const rows = container.getAllByRole('row');
|
||||
expect(rows).toHaveLength(markets.length);
|
||||
});
|
||||
});
|
||||
@@ -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'),
|
||||
|
||||
@@ -25,6 +25,7 @@ import { DepositsMenu } from '../../components/deposits-menu';
|
||||
import { WithdrawalsMenu } from '../../components/withdrawals-menu';
|
||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
|
||||
const WithdrawalsIndicator = () => {
|
||||
const { ready } = useIncompleteWithdrawals();
|
||||
@@ -72,19 +73,29 @@ export const Portfolio = () => {
|
||||
name={t('Positions')}
|
||||
menu={<PositionsMenu />}
|
||||
>
|
||||
<PositionsContainer allKeys />
|
||||
<ErrorBoundary feature="portfolio-positions">
|
||||
<PositionsContainer allKeys />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="orders" name={t('Orders')}>
|
||||
<OrdersContainer />
|
||||
<ErrorBoundary feature="portfolio-orders">
|
||||
<OrdersContainer />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="fills" name={t('Fills')}>
|
||||
<FillsContainer />
|
||||
<ErrorBoundary feature="portfolio-fills">
|
||||
<FillsContainer />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="funding-payments" name={t('Funding payments')}>
|
||||
<FundingPaymentsContainer />
|
||||
<ErrorBoundary feature="portfolio-funding-payments">
|
||||
<FundingPaymentsContainer />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="ledger-entries" name={t('Ledger entries')}>
|
||||
<LedgerContainer />
|
||||
<ErrorBoundary feature="portfolio-ledger">
|
||||
<LedgerContainer />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</PortfolioGridChild>
|
||||
@@ -101,10 +112,14 @@ export const Portfolio = () => {
|
||||
name={t('Collateral')}
|
||||
menu={<AccountsMenu />}
|
||||
>
|
||||
<AccountsContainer />
|
||||
<ErrorBoundary feature="portfolio-accounts">
|
||||
<AccountsContainer />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="deposits" name={t('Deposits')} menu={<DepositsMenu />}>
|
||||
<DepositsContainer />
|
||||
<ErrorBoundary feature="portfolio-deposit">
|
||||
<DepositsContainer />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab
|
||||
id="withdrawals"
|
||||
@@ -112,7 +127,9 @@ export const Portfolio = () => {
|
||||
indicator={<WithdrawalsIndicator />}
|
||||
menu={<WithdrawalsMenu />}
|
||||
>
|
||||
<WithdrawalsContainer />
|
||||
<ErrorBoundary feature="portfolio-deposit">
|
||||
<WithdrawalsContainer />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</PortfolioGridChild>
|
||||
|
||||
@@ -10,18 +10,43 @@ 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';
|
||||
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
|
||||
import { Routes } from '../../lib/links';
|
||||
import { useTransactionEventSubscription } from '@vegaprotocol/web3';
|
||||
import { Statistics, useStats } from './referral-statistics';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ns, useT } from '../../lib/use-t';
|
||||
import { useFundsAvailable } from './hooks/use-funds-available';
|
||||
import { ViewType, useSidebar } from '../../components/sidebar';
|
||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
import { QUSDTooltip } from './qusd-tooltip';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
const RELOAD_DELAY = 3000;
|
||||
|
||||
const SPAM_PROTECTION_ERR = 'SPAM_PROTECTION_ERR';
|
||||
const SpamProtectionErr = ({
|
||||
requiredFunds,
|
||||
}: {
|
||||
requiredFunds?: string | number | bigint;
|
||||
}) => {
|
||||
if (!requiredFunds) return null;
|
||||
// eslint-disable-next-line react/jsx-no-undef
|
||||
return (
|
||||
<Trans
|
||||
defaults="To protect the network from spam, you must have at least {{requiredFunds}} <0>qUSD</0> of any asset on the network to proceed."
|
||||
values={{
|
||||
requiredFunds,
|
||||
}}
|
||||
components={[<QUSDTooltip key="qusd" />]}
|
||||
ns={ns}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const validateCode = (value: string, t: ReturnType<typeof useT>) => {
|
||||
const number = +`0x${value}`;
|
||||
if (!value || value.length !== 64) {
|
||||
@@ -32,7 +57,23 @@ const validateCode = (value: string, t: ReturnType<typeof useT>) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const ApplyCodeForm = () => {
|
||||
export const ApplyCodeFormContainer = ({
|
||||
onSuccess,
|
||||
}: {
|
||||
onSuccess?: () => void;
|
||||
}) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const isInReferralSet = useIsInReferralSet(pubKey);
|
||||
|
||||
// Navigate to the index page when already in the referral set.
|
||||
if (isInReferralSet) {
|
||||
return <Navigate to={Routes.REFERRALS} />;
|
||||
}
|
||||
|
||||
return <ApplyCodeForm onSuccess={onSuccess} />;
|
||||
};
|
||||
|
||||
export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
const t = useT();
|
||||
const program = useReferralProgram();
|
||||
const navigate = useNavigate();
|
||||
@@ -41,10 +82,15 @@ export const ApplyCodeForm = () => {
|
||||
);
|
||||
|
||||
const [status, setStatus] = useState<
|
||||
'requested' | 'failed' | 'successful' | null
|
||||
'requested' | 'no-funds' | 'successful' | null
|
||||
>(null);
|
||||
const txHash = useRef<string | null>(null);
|
||||
const { isReadOnly, pubKey, sendTx } = useVegaWallet();
|
||||
const { isEligible, requiredFunds } = useFundsAvailable();
|
||||
|
||||
const currentRouteId = useGetCurrentRouteId();
|
||||
const setViews = useSidebar((s) => s.setViews);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@@ -55,19 +101,54 @@ 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 if a connected party can apply a code (min funds span protection)
|
||||
*/
|
||||
const validateFundsAvailable = useCallback(() => {
|
||||
if (requiredFunds && !isEligible) {
|
||||
const err = SPAM_PROTECTION_ERR;
|
||||
return err;
|
||||
}
|
||||
return true;
|
||||
}, [isEligible, requiredFunds]);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}, [params, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const err = validateFundsAvailable();
|
||||
if (err !== true) {
|
||||
setStatus('no-funds');
|
||||
} else {
|
||||
setStatus(null);
|
||||
}
|
||||
}, [isEligible, validateFundsAvailable]);
|
||||
|
||||
const onSubmit = ({ code }: FieldValues) => {
|
||||
if (isReadOnly || !pubKey || !code || code.length === 0) {
|
||||
return;
|
||||
@@ -139,21 +220,17 @@ export const ApplyCodeForm = () => {
|
||||
useEffect(() => {
|
||||
if (status === 'successful') {
|
||||
setTimeout(() => {
|
||||
if (onSuccess) onSuccess();
|
||||
navigate(Routes.REFERRALS);
|
||||
}, RELOAD_DELAY);
|
||||
}
|
||||
}, [navigate, status]);
|
||||
|
||||
// go to main page if the current pubkey is already a referrer or referee
|
||||
if (referee || referrer) {
|
||||
return <Navigate to={Routes.REFERRALS} />;
|
||||
}
|
||||
}, [navigate, onSuccess, status]);
|
||||
|
||||
// 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>{' '}
|
||||
@@ -184,6 +261,18 @@ export const ApplyCodeForm = () => {
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 'no-funds') {
|
||||
return {
|
||||
disabled: false,
|
||||
children: t('Deposit funds'),
|
||||
type: 'button' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
|
||||
onClick: ((event) => {
|
||||
event.preventDefault();
|
||||
setViews({ type: ViewType.Deposit }, currentRouteId);
|
||||
}) as MouseEventHandler,
|
||||
};
|
||||
}
|
||||
|
||||
if (status === 'requested') {
|
||||
return {
|
||||
disabled: true,
|
||||
@@ -205,15 +294,20 @@ 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.')}
|
||||
{t(
|
||||
'Apply a referral code to access the discount benefits of the current program.'
|
||||
)}
|
||||
</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 +318,55 @@ 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 codeErr = validateCode(value, t);
|
||||
if (codeErr !== true) return codeErr;
|
||||
const fundsErr = validateFundsAvailable();
|
||||
if (fundsErr !== true) return fundsErr;
|
||||
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">
|
||||
{errors.code.message?.toString()}
|
||||
{status === 'no-funds' ? (
|
||||
<InputError intent="warning" className="overflow-auto break-words">
|
||||
<span>
|
||||
<SpamProtectionErr requiredFunds={requiredFunds?.toString()} />
|
||||
</span>
|
||||
</InputError>
|
||||
) : (
|
||||
errors.code && (
|
||||
<InputError intent="warning" className="overflow-auto break-words">
|
||||
{errors.code.message === SPAM_PROTECTION_ERR ? (
|
||||
<span>
|
||||
<SpamProtectionErr
|
||||
requiredFunds={requiredFunds?.toString()}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
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" />
|
||||
|
||||
@@ -6,6 +6,8 @@ export const SKY_BACKGROUND =
|
||||
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[40%_0px] bg-[length:1440px] bg-no-repeat bg-local';
|
||||
|
||||
// TODO: Update the links to use the correct referral related pages
|
||||
export const REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
|
||||
export const ABOUT_REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
|
||||
export const REFERRAL_DOCS_LINK =
|
||||
'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program';
|
||||
export const ABOUT_REFERRAL_DOCS_LINK =
|
||||
'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program';
|
||||
export const DISCLAIMER_REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
|
||||
|
||||
@@ -19,14 +19,22 @@ import {
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
|
||||
import { useStakeAvailable } from './hooks/use-stake-available';
|
||||
import {
|
||||
ABOUT_REFERRAL_DOCS_LINK,
|
||||
DISCLAIMER_REFERRAL_DOCS_LINK,
|
||||
} from './constants';
|
||||
import { useReferral } from './hooks/use-referral';
|
||||
import { ABOUT_REFERRAL_DOCS_LINK } from './constants';
|
||||
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { Routes } from '../../lib/links';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
|
||||
export const CreateCodeContainer = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const isInReferralSet = useIsInReferralSet(pubKey);
|
||||
|
||||
// Navigate to the index page when already in the referral set.
|
||||
if (isInReferralSet) {
|
||||
return <Navigate to={Routes.REFERRALS} />;
|
||||
}
|
||||
|
||||
return <CreateCodeForm />;
|
||||
};
|
||||
|
||||
@@ -39,13 +47,16 @@ 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>
|
||||
<p className="mb-4 text-center text-base">
|
||||
{t(
|
||||
'Generate a referral code to share with your friends and start earning commission.'
|
||||
'Generate a referral code to share with your friends and access the commission benefits of the current program.'
|
||||
)}
|
||||
</p>
|
||||
|
||||
@@ -95,6 +106,8 @@ const CreateCodeDialog = ({
|
||||
const { stakeAvailable: currentStakeAvailable, requiredStake } =
|
||||
useStakeAvailable();
|
||||
|
||||
const { details: programDetails } = useReferralProgram();
|
||||
|
||||
const onSubmit = () => {
|
||||
if (isReadOnly || !pubKey) {
|
||||
setErr('Not connected');
|
||||
@@ -193,12 +206,73 @@ const CreateCodeDialog = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!programDetails) {
|
||||
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()}
|
||||
>
|
||||
{t('Yes')}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{(status === 'idle' || status === 'loading' || status === 'error') && (
|
||||
<p>
|
||||
{t(
|
||||
'Generate a referral code to share with your friends and start earning commission.'
|
||||
'Generate a referral code to share with your friends and access the commission benefits of the current program.'
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
@@ -229,9 +303,6 @@ const CreateCodeDialog = ({
|
||||
<ExternalLink href={ABOUT_REFERRAL_DOCS_LINK}>
|
||||
{t('About the referral program')}
|
||||
</ExternalLink>
|
||||
<ExternalLink href={DISCLAIMER_REFERRAL_DOCS_LINK}>
|
||||
{t('Disclaimer')}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -53,7 +53,7 @@ export const NotFound = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="pt-32">
|
||||
<LayoutWithSky className="pt-32">
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute top-64 right-[220px] md:right-[340px] max-sm:hidden"
|
||||
@@ -75,6 +75,6 @@ export const NotFound = () => {
|
||||
{t('Go back and try again')}
|
||||
</RainbowButton>
|
||||
</p>
|
||||
</div>
|
||||
</LayoutWithSky>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
query FundsAvailable($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
accountsConnection {
|
||||
edges {
|
||||
node {
|
||||
balance
|
||||
asset {
|
||||
decimals
|
||||
symbol
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
networkParameter(key: "spam.protection.applyReferral.min.funds") {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type FundsAvailableQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type FundsAvailableQuery = { __typename?: 'Query', party?: { __typename?: 'Party', accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', balance: string, asset: { __typename?: 'Asset', decimals: number, symbol: string, id: string } } } | null> | null } | null } | null, networkParameter?: { __typename?: 'NetworkParameter', key: string, value: string } | null };
|
||||
|
||||
|
||||
export const FundsAvailableDocument = gql`
|
||||
query FundsAvailable($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
accountsConnection {
|
||||
edges {
|
||||
node {
|
||||
balance
|
||||
asset {
|
||||
decimals
|
||||
symbol
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
networkParameter(key: "spam.protection.applyReferral.min.funds") {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useFundsAvailableQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useFundsAvailableQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useFundsAvailableQuery` 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 } = useFundsAvailableQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useFundsAvailableQuery(baseOptions: Apollo.QueryHookOptions<FundsAvailableQuery, FundsAvailableQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<FundsAvailableQuery, FundsAvailableQueryVariables>(FundsAvailableDocument, options);
|
||||
}
|
||||
export function useFundsAvailableLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FundsAvailableQuery, FundsAvailableQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<FundsAvailableQuery, FundsAvailableQueryVariables>(FundsAvailableDocument, options);
|
||||
}
|
||||
export type FundsAvailableQueryHookResult = ReturnType<typeof useFundsAvailableQuery>;
|
||||
export type FundsAvailableLazyQueryHookResult = ReturnType<typeof useFundsAvailableLazyQuery>;
|
||||
export type FundsAvailableQueryResult = Apollo.QueryResult<FundsAvailableQuery, FundsAvailableQueryVariables>;
|
||||
+2
-1
@@ -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>;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useFundsAvailableQuery } from './__generated__/FundsAvailable';
|
||||
import compact from 'lodash/compact';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
/**
|
||||
* Gets the funds for given public key and required min for
|
||||
* the referral program.
|
||||
*
|
||||
* (Uses currently connected public key if left empty)
|
||||
*/
|
||||
export const useFundsAvailable = (pubKey?: string) => {
|
||||
const { pubKey: currentPubKey } = useVegaWallet();
|
||||
const partyId = pubKey || currentPubKey;
|
||||
const { data, stopPolling } = useFundsAvailableQuery({
|
||||
variables: { partyId: partyId || '' },
|
||||
skip: !partyId,
|
||||
fetchPolicy: 'network-only',
|
||||
errorPolicy: 'ignore',
|
||||
pollInterval: 5000,
|
||||
});
|
||||
|
||||
const fundsAvailable = data
|
||||
? compact(data.party?.accountsConnection?.edges?.map((e) => e?.node))
|
||||
: undefined;
|
||||
const requiredFunds = data
|
||||
? BigNumber(data.networkParameter?.value || '0')
|
||||
: undefined;
|
||||
|
||||
const sumOfFunds =
|
||||
fundsAvailable
|
||||
?.filter((fa) => fa.balance)
|
||||
.reduce((sum, fa) => sum.plus(BigNumber(fa.balance)), BigNumber(0)) ||
|
||||
BigNumber(0);
|
||||
|
||||
if (requiredFunds && sumOfFunds.isGreaterThanOrEqualTo(requiredFunds)) {
|
||||
stopPolling();
|
||||
}
|
||||
|
||||
return {
|
||||
fundsAvailable,
|
||||
requiredFunds,
|
||||
isEligible:
|
||||
fundsAvailable != null &&
|
||||
requiredFunds != null &&
|
||||
sumOfFunds.isGreaterThanOrEqualTo(requiredFunds),
|
||||
};
|
||||
};
|
||||
@@ -1,14 +1,8 @@
|
||||
import { getNumberFormat } from '@vegaprotocol/utils';
|
||||
import { addDays } from 'date-fns';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import omit from 'lodash/omit';
|
||||
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
|
||||
|
||||
const STAKING_TIERS_MAPPING: Record<number, string> = {
|
||||
1: 'Tradestarter',
|
||||
2: 'Mid level degen',
|
||||
3: 'Reward hoarder',
|
||||
};
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const MOCK = {
|
||||
@@ -16,46 +10,76 @@ const MOCK = {
|
||||
currentReferralProgram: {
|
||||
id: 'abc',
|
||||
version: 1,
|
||||
endOfProgramTimestamp: addDays(new Date(), 10).toISOString(),
|
||||
windowLength: 10,
|
||||
benefitTiers: [
|
||||
{
|
||||
minimumEpochs: 5,
|
||||
minimumRunningNotionalTakerVolume: '30000',
|
||||
referralDiscountFactor: '0.01',
|
||||
referralRewardFactor: '0.01',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 5,
|
||||
minimumRunningNotionalTakerVolume: '20000',
|
||||
referralDiscountFactor: '0.05',
|
||||
minimumEpochs: 1,
|
||||
minimumRunningNotionalTakerVolume: '100000',
|
||||
referralDiscountFactor: '0.1',
|
||||
referralRewardFactor: '0.05',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 5,
|
||||
minimumRunningNotionalTakerVolume: '10000',
|
||||
referralDiscountFactor: '0.001',
|
||||
referralRewardFactor: '0.001',
|
||||
minimumEpochs: 1,
|
||||
minimumRunningNotionalTakerVolume: '1000000',
|
||||
referralDiscountFactor: '0.1',
|
||||
referralRewardFactor: '0.075',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 1,
|
||||
minimumRunningNotionalTakerVolume: '5000000',
|
||||
referralDiscountFactor: '0.1',
|
||||
referralRewardFactor: '0.1',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 1,
|
||||
minimumRunningNotionalTakerVolume: '25000000',
|
||||
referralDiscountFactor: '0.1',
|
||||
referralRewardFactor: '0.125',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 1,
|
||||
minimumRunningNotionalTakerVolume: '75000000',
|
||||
referralDiscountFactor: '0.1',
|
||||
referralRewardFactor: '0.15',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 1,
|
||||
minimumRunningNotionalTakerVolume: '150000000',
|
||||
referralDiscountFactor: '0.07',
|
||||
referralRewardFactor: '0.175',
|
||||
},
|
||||
],
|
||||
stakingTiers: [
|
||||
{
|
||||
minimumStakedTokens: '10000',
|
||||
referralRewardMultiplier: '1',
|
||||
minimumStakedTokens: '100000000000000000000',
|
||||
referralRewardMultiplier: '1.025',
|
||||
},
|
||||
{
|
||||
minimumStakedTokens: '20000',
|
||||
referralRewardMultiplier: '2',
|
||||
minimumStakedTokens: '1000000000000000000000',
|
||||
referralRewardMultiplier: '1.05',
|
||||
},
|
||||
{
|
||||
minimumStakedTokens: '30000',
|
||||
referralRewardMultiplier: '3',
|
||||
minimumStakedTokens: '5000000000000000000000',
|
||||
referralRewardMultiplier: '1.1',
|
||||
},
|
||||
{
|
||||
minimumStakedTokens: '50000000000000000000000',
|
||||
referralRewardMultiplier: '1.2',
|
||||
},
|
||||
{
|
||||
minimumStakedTokens: '250000000000000000000000',
|
||||
referralRewardMultiplier: '1.25',
|
||||
},
|
||||
{
|
||||
minimumStakedTokens: '500000000000000000000000',
|
||||
referralRewardMultiplier: '1.3',
|
||||
},
|
||||
],
|
||||
endOfProgramTimestamp: '2024-12-31T01:00:00Z',
|
||||
windowLength: 30,
|
||||
},
|
||||
loading: false,
|
||||
error: undefined,
|
||||
},
|
||||
loading: false,
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
export const useReferralProgram = () => {
|
||||
@@ -75,30 +99,26 @@ 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: BigNumber(t.referralRewardFactor).times(100).toFixed(2) + '%',
|
||||
discountFactor: Number(t.referralDiscountFactor),
|
||||
discount: BigNumber(t.referralDiscountFactor).times(100).toFixed(2) + '%',
|
||||
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
|
||||
volume: getNumberFormat(0).format(
|
||||
Number(t.minimumRunningNotionalTakerVolume)
|
||||
),
|
||||
epochs: Number(t.minimumEpochs),
|
||||
};
|
||||
});
|
||||
|
||||
const stakingTiers = sortBy(
|
||||
data.currentReferralProgram.stakingTiers,
|
||||
(t) => t.referralRewardMultiplier
|
||||
const stakingTiers = sortBy(data.currentReferralProgram.stakingTiers, (t) =>
|
||||
parseFloat(t.referralRewardMultiplier)
|
||||
).map((t, i) => {
|
||||
return {
|
||||
tier: i + 1,
|
||||
label: STAKING_TIERS_MAPPING[i + 1],
|
||||
...t,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
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, 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,
|
||||
]);
|
||||
};
|
||||
@@ -2,8 +2,12 @@ import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { useCallback } from 'react';
|
||||
import { useRefereesQuery } from './__generated__/Referees';
|
||||
import compact from 'lodash/compact';
|
||||
import type { ReferralSetsQueryVariables } from './__generated__/ReferralSets';
|
||||
import type {
|
||||
ReferralSetsQuery,
|
||||
ReferralSetsQueryVariables,
|
||||
} from './__generated__/ReferralSets';
|
||||
import { useReferralSetsQuery } from './__generated__/ReferralSets';
|
||||
import { useStakeAvailable } from './use-stake-available';
|
||||
|
||||
export const DEFAULT_AGGREGATION_DAYS = 30;
|
||||
|
||||
@@ -62,6 +66,8 @@ export const useReferral = (args: UseReferralArgs) => {
|
||||
? referralData.referralSets.edges[0]?.node
|
||||
: undefined;
|
||||
|
||||
const { isEligible } = useStakeAvailable(referralSet?.referrer);
|
||||
|
||||
const {
|
||||
data: refereesData,
|
||||
loading: refereesLoading,
|
||||
@@ -103,6 +109,7 @@ export const useReferral = (args: UseReferralArgs) => {
|
||||
referee: referee,
|
||||
referrerId: referralSet.referrer,
|
||||
createdAt: referralSet.createdAt,
|
||||
isEligible,
|
||||
referees,
|
||||
}
|
||||
: undefined;
|
||||
@@ -114,3 +121,36 @@ export const useReferral = (args: UseReferralArgs) => {
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
const retrieveReferralSetData = (data: ReferralSetsQuery | undefined) =>
|
||||
data?.referralSets.edges && data.referralSets.edges.length > 0
|
||||
? data.referralSets.edges[0]?.node
|
||||
: undefined;
|
||||
|
||||
export const useIsInReferralSet = (pubKey: string | null) => {
|
||||
const [asRefereeVariables, asRefereeSkip] = prepareVariables({
|
||||
pubKey,
|
||||
role: 'referee',
|
||||
});
|
||||
const [asReferrerVariables, asReferrerSkip] = prepareVariables({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
});
|
||||
|
||||
const { data: asRefereeData } = useReferralSetsQuery({
|
||||
variables: asRefereeVariables,
|
||||
skip: asRefereeSkip,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
const { data: asReferrerData } = useReferralSetsQuery({
|
||||
variables: asReferrerVariables,
|
||||
skip: asReferrerSkip,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
return Boolean(
|
||||
retrieveReferralSetData(asRefereeData) ||
|
||||
retrieveReferralSetData(asReferrerData)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
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,
|
||||
// TODO: remove when network params available
|
||||
/**
|
||||
* 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,
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,11 +15,16 @@ export const LandingBanner = () => {
|
||||
</div>
|
||||
<div className="pt-20 sm:w-[50%]">
|
||||
<h1 className="text-6xl font-alpha calt mb-10">
|
||||
{t('Earn commission & stake rewards')}
|
||||
{t('Vega community referrals')}
|
||||
</h1>
|
||||
<p className="text-lg mb-1">
|
||||
{t(
|
||||
'Referral programs can be proposed and created via community governance.'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-lg mb-10">
|
||||
{t(
|
||||
'Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.'
|
||||
'Once live, users can generate referral codes to share with their friends and earn commission on their trades, while referred traders can access fee discounts based on the running volume of the group.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { ExternalLink, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../lib/use-t';
|
||||
|
||||
export const QUSDTooltip = () => {
|
||||
const t = useT();
|
||||
return (
|
||||
<Tooltip
|
||||
description={
|
||||
<>
|
||||
<p className="mb-1">
|
||||
{t(
|
||||
'qUSD provides a rough USD equivalent of balances across all assets using the value of "Quantum" for that asset'
|
||||
)}
|
||||
</p>
|
||||
{DocsLinks && (
|
||||
<ExternalLink href={DocsLinks.QUANTUM}>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
underline={true}
|
||||
>
|
||||
<span>{t('qUSD')}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,378 @@
|
||||
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 apply code when no data has been found for given pubkey', () => {
|
||||
const { queryByTestId } = render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider mocks={[]} showWarnings={false}>
|
||||
<ReferralStatistics />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays referrer stats when given pubkey is a referrer', async () => {
|
||||
const { queryByTestId } = render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[
|
||||
programMock,
|
||||
referralSetAsReferrerMock,
|
||||
noReferralSetAsRefereeMock,
|
||||
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(
|
||||
'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,15 +1,13 @@
|
||||
import minBy from 'lodash/minBy';
|
||||
import { CodeTile, StatTile } from './tile';
|
||||
import {
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
truncateMiddle,
|
||||
ExternalLink,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { DEFAULT_AGGREGATION_DAYS, useReferral } from './hooks/use-referral';
|
||||
import { CreateCodeContainer } from './create-code-form';
|
||||
import classNames from 'classnames';
|
||||
import { Table } from './table';
|
||||
import {
|
||||
@@ -25,39 +23,54 @@ import compact from 'lodash/compact';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
import { useStakeAvailable } from './hooks/use-stake-available';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, 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, ApplyCodeFormContainer } from './apply-code-form';
|
||||
import { QUSDTooltip } from './qusd-tooltip';
|
||||
|
||||
export const ReferralStatistics = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const program = useReferralProgram();
|
||||
|
||||
const { data: referee } = useReferral({
|
||||
const { data: referee, refetch: refereeRefetch } = useReferral({
|
||||
pubKey,
|
||||
role: 'referee',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
});
|
||||
const { data: referrer } = useReferral({
|
||||
const { data: referrer, refetch: referrerRefetch } = useReferral({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
});
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
refereeRefetch();
|
||||
referrerRefetch();
|
||||
}, [refereeRefetch, referrerRefetch]);
|
||||
|
||||
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 />;
|
||||
return <ApplyCodeFormContainer onSuccess={refetch} />;
|
||||
};
|
||||
|
||||
export const useStats = ({
|
||||
@@ -70,7 +83,9 @@ export const useStats = ({
|
||||
as?: 'referrer' | 'referee';
|
||||
}) => {
|
||||
const { benefitTiers } = program;
|
||||
const { data: epochData } = useCurrentEpochInfoQuery();
|
||||
const { data: epochData } = useCurrentEpochInfoQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
const { data: statsData } = useReferralSetStatsQuery({
|
||||
variables: {
|
||||
code: data?.code || '',
|
||||
@@ -104,7 +119,7 @@ export const useStats = ({
|
||||
: 1;
|
||||
const finalCommissionValue = isNaN(multiplier)
|
||||
? baseCommissionValue
|
||||
: multiplier * baseCommissionValue;
|
||||
: new BigNumber(multiplier).times(baseCommissionValue).toNumber();
|
||||
|
||||
const discountFactorValue = refereeStats?.discountFactor
|
||||
? Number(refereeStats.discountFactor)
|
||||
@@ -116,8 +131,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
|
||||
@@ -163,6 +178,7 @@ export const Statistics = ({
|
||||
discountFactorValue,
|
||||
currentBenefitTierValue,
|
||||
epochsValue,
|
||||
nextBenefitTierValue,
|
||||
nextBenefitTierVolumeValue,
|
||||
nextBenefitTierEpochsValue,
|
||||
} = useStats({ data, program, as });
|
||||
@@ -174,7 +190,7 @@ export const Statistics = ({
|
||||
|
||||
const { benefitTiers } = useReferralProgram();
|
||||
|
||||
const { stakeAvailable } = useStakeAvailable();
|
||||
const { stakeAvailable, isEligible } = useStakeAvailable();
|
||||
const { details } = program;
|
||||
|
||||
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
||||
@@ -196,32 +212,51 @@ export const Statistics = ({
|
||||
).toString(),
|
||||
}
|
||||
)}
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{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>
|
||||
}
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{multiplier || t('None')}
|
||||
</StatTile>
|
||||
);
|
||||
const baseCommissionFormatted = BigNumber(baseCommissionValue)
|
||||
.times(100)
|
||||
.toString();
|
||||
const finalCommissionFormatted = new BigNumber(finalCommissionValue)
|
||||
.times(100)
|
||||
.toString();
|
||||
const finalCommissionTile = (
|
||||
<StatTile
|
||||
title={t('Final commission rate')}
|
||||
description={
|
||||
!isNaN(multiplier)
|
||||
? `(${baseCommissionValue * 100}% ⨉ ${multiplier} = ${
|
||||
finalCommissionValue * 100
|
||||
}%)`
|
||||
? `(${baseCommissionFormatted}% ⨉ ${multiplier} = ${finalCommissionFormatted}%)`
|
||||
: undefined
|
||||
}
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{finalCommissionValue * 100}%
|
||||
{finalCommissionFormatted}%
|
||||
</StatTile>
|
||||
);
|
||||
const numberOfTradersValue = data.referees.length;
|
||||
@@ -238,9 +273,10 @@ 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,
|
||||
})}
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{compactNumFormat.format(referrerVolumeValue)}
|
||||
</StatTile>
|
||||
@@ -251,7 +287,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 />}
|
||||
@@ -278,15 +314,25 @@ export const Statistics = ({
|
||||
);
|
||||
|
||||
const currentBenefitTierTile = (
|
||||
<StatTile title={t('Current tier')}>
|
||||
<StatTile
|
||||
title={t('Current tier')}
|
||||
description={
|
||||
nextBenefitTierValue?.tier
|
||||
? t('(Next tier: {{nextTier}})', {
|
||||
nextTier: nextBenefitTierValue?.tier,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{isApplyCodePreview
|
||||
? currentBenefitTierValue?.tier || benefitTiers[0]?.tier || 'None'
|
||||
: currentBenefitTierValue?.tier || 'None'}
|
||||
</StatTile>
|
||||
);
|
||||
const discountFactorTile = (
|
||||
<StatTile title={t('Discount')}>
|
||||
{isApplyCodePreview
|
||||
<StatTile title={t('Discount')} overrideWithNoProgram={!details}>
|
||||
{isApplyCodePreview && benefitTiers.length >= 1
|
||||
? benefitTiers[0].discountFactor * 100
|
||||
: discountFactorValue * 100}
|
||||
%
|
||||
@@ -294,9 +340,14 @@ 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,
|
||||
}
|
||||
)}
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{compactNumFormat.format(runningVolumeValue)}
|
||||
</StatTile>
|
||||
@@ -305,14 +356,14 @@ export const Statistics = ({
|
||||
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
|
||||
);
|
||||
const nextTierVolumeTile = (
|
||||
<StatTile title={t('Volume to next tier')}>
|
||||
<StatTile title={t('Volume to next tier')} overrideWithNoProgram={!details}>
|
||||
{nextBenefitTierVolumeValue <= 0
|
||||
? '0'
|
||||
: compactNumFormat.format(nextBenefitTierVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
const nextTierEpochsTile = (
|
||||
<StatTile title={t('Epochs to next tier')}>
|
||||
<StatTile title={t('Epochs to next tier')} overrideWithNoProgram={!details}>
|
||||
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
|
||||
</StatTile>
|
||||
);
|
||||
@@ -333,28 +384,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,20 +467,25 @@ 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:
|
||||
details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
||||
}}
|
||||
components={[<QUSDTooltip key="qusd" />]}
|
||||
ns={ns}
|
||||
/>
|
||||
),
|
||||
@@ -429,28 +517,3 @@ export const Statistics = ({
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const QUSDTooltip = () => {
|
||||
const t = useT();
|
||||
return (
|
||||
<Tooltip
|
||||
description={
|
||||
<>
|
||||
<p className="mb-1">
|
||||
{t(
|
||||
'qUSD provides a rough USD equivalent of balances across all assets using the value of "Quantum" for that asset'
|
||||
)}
|
||||
</p>
|
||||
{DocsLinks && (
|
||||
<ExternalLink href={DocsLinks.QUANTUM}>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
underline={true}
|
||||
>
|
||||
<span>{t('qUSD')}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,11 +4,10 @@ import {
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { HowItWorksTable } from './how-it-works-table';
|
||||
import { LandingBanner } from './landing-banner';
|
||||
import { TiersContainer } from './tiers';
|
||||
import { TabLink } from './buttons';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Outlet, useMatch } from 'react-router-dom';
|
||||
import { Routes } from '../../lib/links';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useReferral } from './hooks/use-referral';
|
||||
@@ -18,15 +17,17 @@ import { usePageTitleStore } from '../../stores';
|
||||
import { useEffect } from 'react';
|
||||
import { titlefy } from '@vegaprotocol/utils';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
|
||||
const Nav = () => {
|
||||
const t = useT();
|
||||
const match = useMatch(Routes.REFERRALS_APPLY_CODE);
|
||||
return (
|
||||
<div className="flex justify-center border-b border-vega-cdark-500">
|
||||
<TabLink end to={Routes.REFERRALS}>
|
||||
{t('I want a code')}
|
||||
<TabLink end to={match ? Routes.REFERRALS_APPLY_CODE : Routes.REFERRALS}>
|
||||
{t('Apply code')}
|
||||
</TabLink>
|
||||
<TabLink to={Routes.REFERRALS_APPLY_CODE}>{t('I have a code')}</TabLink>
|
||||
<TabLink to={Routes.REFERRALS_CREATE_CODE}>{t('Create code')}</TabLink>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -65,7 +66,7 @@ export const Referrals = () => {
|
||||
}, [updateTitle, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ErrorBoundary feature="referrals">
|
||||
<LandingBanner />
|
||||
|
||||
{showNav && <Nav />}
|
||||
@@ -95,18 +96,16 @@ export const Referrals = () => {
|
||||
<h2 className="text-2xl">{t('How it works')}</h2>
|
||||
</div>
|
||||
<div className="md:w-[60%] mx-auto">
|
||||
<HowItWorksTable />
|
||||
<div className="mt-5">
|
||||
<TradingAnchorButton
|
||||
className="mx-auto w-max"
|
||||
href={REFERRAL_DOCS_LINK}
|
||||
target="_blank"
|
||||
>
|
||||
{t('Read the terms')}{' '}
|
||||
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
{t('Read the docs')} <VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
</TradingAnchorButton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,8 +14,10 @@ export const Tag = ({
|
||||
className={classNames(
|
||||
'w-max border rounded-[1rem] py-[0.125rem] px-2 text-xs',
|
||||
{
|
||||
'border-vega-yellow-500 text-vega-yellow-500': color === 'yellow',
|
||||
'border-vega-green-500 text-vega-green-500': color === 'green',
|
||||
'border-vega-yellow-550 text-vega-yellow-550 dark:border-vega-yellow-500 dark:text-vega-yellow-500':
|
||||
color === 'yellow',
|
||||
'border-vega-green-550 text-vega-green-550 dark:border-vega-green-500 dark:text-vega-green-500':
|
||||
color === 'green',
|
||||
'border-vega-blue-500 text-vega-blue-500': color === 'blue',
|
||||
'border-vega-purple-500 text-vega-purple-500': color === 'purple',
|
||||
'border-vega-pink-500 text-vega-pink-500': color === 'pink',
|
||||
|
||||
@@ -1,15 +1,43 @@
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
getDateTimeFormat,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
import { Table } from './table';
|
||||
import classNames from 'classnames';
|
||||
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 { ExternalLink, truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
DApp,
|
||||
DocsLinks,
|
||||
TOKEN_PROPOSAL,
|
||||
TOKEN_PROPOSALS,
|
||||
useLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { useT, ns } from '../../lib/use-t';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
// rainbow-ish order
|
||||
const TIER_COLORS: Array<ComponentProps<typeof Tag>['color']> = [
|
||||
'pink',
|
||||
'orange',
|
||||
'yellow',
|
||||
'green',
|
||||
'blue',
|
||||
'purple',
|
||||
];
|
||||
|
||||
const getTierColor = (tier: number) => {
|
||||
const tiers = Object.keys(TIER_COLORS).length;
|
||||
let index = Math.abs(tier - 1);
|
||||
if (tier >= tiers) {
|
||||
index = index % tiers;
|
||||
}
|
||||
return TIER_COLORS[index];
|
||||
};
|
||||
|
||||
const Loading = ({ variant }: { variant: 'large' | 'inline' }) => (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -23,51 +51,63 @@ const Loading = ({ variant }: { variant: 'large' | 'inline' }) => (
|
||||
|
||||
const StakingTier = ({
|
||||
tier,
|
||||
label,
|
||||
referralRewardMultiplier,
|
||||
minimumStakedTokens,
|
||||
}: {
|
||||
tier: number;
|
||||
label: string;
|
||||
referralRewardMultiplier: string;
|
||||
minimumStakedTokens: string;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const color: Record<number, ComponentProps<typeof Tag>['color']> = {
|
||||
1: 'green',
|
||||
2: 'blue',
|
||||
3: 'pink',
|
||||
};
|
||||
const minimum = addDecimalsFormatNumber(minimumStakedTokens, 18);
|
||||
|
||||
// TODO: Decide what to do with the multiplier images
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const multiplierImage = (
|
||||
<div
|
||||
aria-hidden
|
||||
className={classNames(
|
||||
'w-full max-w-[80px] h-full min-h-[80px]',
|
||||
'bg-cover bg-right-bottom',
|
||||
{
|
||||
"bg-[url('/1x.png')]": tier === 1,
|
||||
"bg-[url('/2x.png')]": tier === 2,
|
||||
"bg-[url('/3x.png')]": tier === 3,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">{`${referralRewardMultiplier}x multiplier`}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'overflow-hidden',
|
||||
'border rounded-md w-full',
|
||||
'flex flex-row',
|
||||
'bg-white dark:bg-vega-cdark-900',
|
||||
GRADIENT,
|
||||
BORDER_COLOR
|
||||
)}
|
||||
>
|
||||
<div aria-hidden className="max-w-[120px]">
|
||||
{tier < 4 && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={`/${tier}x.png`}
|
||||
alt={`${referralRewardMultiplier}x multiplier`}
|
||||
width={240}
|
||||
height={240}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
'p-3 flex flex-row min-h-[80px] h-full items-center'
|
||||
)}
|
||||
</div>
|
||||
<div className={classNames('p-3')}>
|
||||
<Tag color={color[tier]}>Multiplier {referralRewardMultiplier}x</Tag>
|
||||
<h3 className="mt-1 mb-1 text-base">{label}</h3>
|
||||
<p className="text-sm text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
{t('Stake a minimum of {{minimumStakedTokens}} $VEGA tokens', {
|
||||
minimumStakedTokens,
|
||||
})}
|
||||
</p>
|
||||
>
|
||||
<div>
|
||||
<Tag color={getTierColor(tier)}>
|
||||
{t('Multiplier')} {referralRewardMultiplier}x
|
||||
</Tag>
|
||||
<p className="mt-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
<Trans
|
||||
defaults="Stake a minimum of <0>{{minimum}}</0> $VEGA tokens"
|
||||
values={{ minimum }}
|
||||
components={[<b key={minimum}></b>]}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -86,12 +126,29 @@ export const TiersContainer = () => {
|
||||
|
||||
if ((!loading && !details) || error) {
|
||||
return (
|
||||
<div className="text-base px-5 py-10 text-center">
|
||||
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white rounded-lg p-6 mt-1 mb-20 text-sm 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')}
|
||||
<ExternalLink
|
||||
href={governanceLink(TOKEN_PROPOSALS)}
|
||||
key="link"
|
||||
className="underline"
|
||||
>
|
||||
{t('Governance App')}
|
||||
</ExternalLink>,
|
||||
]}
|
||||
ns={ns}
|
||||
/>{' '}
|
||||
<Trans
|
||||
defaults="Use the <0>docs</0> tutorial to propose a new program."
|
||||
components={[
|
||||
<ExternalLink
|
||||
href={DocsLinks?.REFERRALS}
|
||||
key="link"
|
||||
className="underline"
|
||||
>
|
||||
{t('docs')}
|
||||
</ExternalLink>,
|
||||
]}
|
||||
ns={ns}
|
||||
@@ -102,47 +159,93 @@ export const TiersContainer = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Benefit tiers */}
|
||||
<div className="flex flex-col items-baseline justify-between mt-10 mb-5">
|
||||
<h2 className="text-2xl">{t('Referral tiers')}</h2>
|
||||
<h2 className="text-3xl mt-10">{t('Current program details')}</h2>
|
||||
{details?.id && (
|
||||
<p>
|
||||
<Trans
|
||||
defaults="As a result of governance proposal <0>{{proposal}}</0> the program below is currently active on the Vega network."
|
||||
values={{ proposal: truncateMiddle(details.id) }}
|
||||
components={[
|
||||
<ExternalLink
|
||||
key="referral-program-proposal-link"
|
||||
href={governanceLink(TOKEN_PROPOSAL.replace(':id', details.id))}
|
||||
className="underline"
|
||||
>
|
||||
proposal
|
||||
</ExternalLink>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Meta */}
|
||||
<div className="mt-10 flex flex-row items-baseline justify-between text-xs text-vega-clight-100 dark:text-vega-cdark-100 font-alpha calt">
|
||||
{details?.id && (
|
||||
<span>
|
||||
{t('Proposal ID:')}{' '}
|
||||
<ExternalLink
|
||||
href={governanceLink(TOKEN_PROPOSAL.replace(':id', details.id))}
|
||||
>
|
||||
<span>{truncateMiddle(details.id)}</span>
|
||||
</ExternalLink>
|
||||
</span>
|
||||
)}
|
||||
{ends && (
|
||||
<span className="text-sm text-vega-clight-200 dark:text-vega-cdark-200">
|
||||
<span>
|
||||
{t('Program ends:')} {ends}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-20">
|
||||
{loading || !benefitTiers || benefitTiers.length === 0 ? (
|
||||
<Loading variant="large" />
|
||||
) : (
|
||||
<TiersTable
|
||||
windowLength={details?.windowLength}
|
||||
data={benefitTiers.map((bt) => ({
|
||||
...bt,
|
||||
tierElement: (
|
||||
<div className="rounded-full bg-vega-clight-900 dark:bg-vega-cdark-900 p-1 w-8 h-8 text-center">
|
||||
{bt.tier}
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Staking tiers */}
|
||||
<div className="flex flex-row items-baseline justify-between mb-5">
|
||||
<h2 className="text-2xl">{t('Staking multipliers')}</h2>
|
||||
</div>
|
||||
<div className="mb-20 flex flex-col justify-items-stretch lg:flex-row gap-5">
|
||||
{loading || !stakingTiers || stakingTiers.length === 0 ? (
|
||||
<>
|
||||
{/* Container */}
|
||||
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white rounded-lg p-6 mt-1 mb-20">
|
||||
{/* Benefit tiers */}
|
||||
<div className="flex flex-col mb-5">
|
||||
<h3 className="text-2xl calt">{t('Benefit tiers')}</h3>
|
||||
<p className="text-sm text-vega-clight-200 dark:text-vega-cdark-200">
|
||||
{t(
|
||||
'Members of a referral group can access the increasing commission and discount benefits defined in the program based on their combined running volume.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-10">
|
||||
{loading || !benefitTiers || benefitTiers.length === 0 ? (
|
||||
<Loading variant="large" />
|
||||
<Loading variant="large" />
|
||||
<Loading variant="large" />
|
||||
</>
|
||||
) : (
|
||||
<StakingTiers data={stakingTiers} />
|
||||
)}
|
||||
) : (
|
||||
<TiersTable
|
||||
windowLength={details?.windowLength}
|
||||
data={benefitTiers.map((bt) => ({
|
||||
...bt,
|
||||
tierElement: (
|
||||
<div className="rounded-full bg-vega-clight-900 dark:bg-vega-cdark-900 p-1 w-8 h-8 text-center">
|
||||
{bt.tier}
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Staking tiers */}
|
||||
<div className="flex flex-col mb-5">
|
||||
<h3 className="text-2xl calt">{t('Staking multipliers')}</h3>
|
||||
<p className="text-sm text-vega-clight-200 dark:text-vega-cdark-200">
|
||||
{t(
|
||||
'Referrers can access the commission multipliers defined in the program by staking VEGA tokens in the amounts shown.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="gap-5 grid lg:grid-cols-3">
|
||||
{loading || !stakingTiers || stakingTiers.length === 0 ? (
|
||||
<>
|
||||
<Loading variant="large" />
|
||||
<Loading variant="large" />
|
||||
<Loading variant="large" />
|
||||
</>
|
||||
) : (
|
||||
<StakingTiers data={stakingTiers} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -154,17 +257,14 @@ const StakingTiers = ({
|
||||
data: ReturnType<typeof useReferralProgram>['stakingTiers'];
|
||||
}) => (
|
||||
<>
|
||||
{data.map(
|
||||
({ tier, label, referralRewardMultiplier, minimumStakedTokens }, i) => (
|
||||
<StakingTier
|
||||
key={i}
|
||||
tier={tier}
|
||||
label={label}
|
||||
referralRewardMultiplier={referralRewardMultiplier}
|
||||
minimumStakedTokens={minimumStakedTokens}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{data.map(({ tier, referralRewardMultiplier, minimumStakedTokens }, i) => (
|
||||
<StakingTier
|
||||
key={i}
|
||||
tier={tier}
|
||||
referralRewardMultiplier={referralRewardMultiplier}
|
||||
minimumStakedTokens={minimumStakedTokens}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -189,28 +289,54 @@ const TiersTable = ({
|
||||
{
|
||||
name: 'commission',
|
||||
displayName: t('Referrer commission'),
|
||||
tooltip: t('A percentage of commission earned by the referrer'),
|
||||
tooltip: t(
|
||||
"The proportion of the referee's taker fees to be rewarded to the referrer"
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'discount',
|
||||
displayName: t('Referee trading discount'),
|
||||
tooltip: t(
|
||||
"The proportion of the referee's taker fees to be discounted"
|
||||
),
|
||||
},
|
||||
{ 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,
|
||||
}
|
||||
),
|
||||
tooltip: t('The minimum running notional for the given benefit tier'),
|
||||
},
|
||||
{
|
||||
name: 'epochs',
|
||||
displayName: t('Min. epochs'),
|
||||
tooltip: t(
|
||||
'The minimum number of epochs the party needs to be in the referral set to be eligible for the benefit'
|
||||
),
|
||||
},
|
||||
{ name: 'epochs', displayName: t('Min. epochs') },
|
||||
]}
|
||||
className="bg-white dark:bg-vega-cdark-900"
|
||||
data={data.map((d) => ({
|
||||
...d,
|
||||
className: classNames({
|
||||
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
|
||||
d.tier === 1,
|
||||
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
|
||||
d.tier === 2,
|
||||
'from-vega-yellow-400 dark:from-vega-yellow-600 to-20% bg-highlight':
|
||||
'yellow' === getTierColor(d.tier),
|
||||
'from-vega-green-400 dark:from-vega-green-600 to-20% bg-highlight':
|
||||
'green' === getTierColor(d.tier),
|
||||
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
|
||||
d.tier === 3,
|
||||
'blue' === getTierColor(d.tier),
|
||||
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
|
||||
'purple' === getTierColor(d.tier),
|
||||
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
|
||||
'pink' === getTierColor(d.tier),
|
||||
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
|
||||
d.tier > 3,
|
||||
'orange' === getTierColor(d.tier),
|
||||
'from-vega-clight-200 dark:from-vega-cdark-200 to-20% bg-highlight':
|
||||
'none' === getTierColor(d.tier),
|
||||
}),
|
||||
}))}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
@@ -31,8 +34,17 @@ type StatTileProps = {
|
||||
title: string;
|
||||
description?: ReactNode;
|
||||
children?: ReactNode;
|
||||
overrideWithNoProgram?: boolean;
|
||||
};
|
||||
export const StatTile = ({ title, description, children }: StatTileProps) => {
|
||||
export const StatTile = ({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
overrideWithNoProgram = false,
|
||||
}: StatTileProps) => {
|
||||
if (overrideWithNoProgram) {
|
||||
return <NoProgramTile title={title} />;
|
||||
}
|
||||
return (
|
||||
<Tile>
|
||||
<h3 className="mb-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100 calt">
|
||||
@@ -48,6 +60,20 @@ export const StatTile = ({ title, description, children }: StatTileProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const NoProgramTile = ({ title }: Pick<StatTileProps, 'title'>) => {
|
||||
const t = useT();
|
||||
return (
|
||||
<Tile title={title}>
|
||||
<h3 className="mb-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100 calt">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="text-xs text-vega-clight-300 dark:text-vega-cdark-300 leading-[3rem]">
|
||||
{t('No active program')}
|
||||
</div>
|
||||
</Tile>
|
||||
);
|
||||
};
|
||||
|
||||
const FADE_OUT_STYLE = classNames(
|
||||
'after:w-5 after:h-full after:absolute after:top-0 after:right-0',
|
||||
'after:bg-gradient-to-l after:from-vega-clight-800 after:dark:from-vega-cdark-800 after:to-transparent'
|
||||
@@ -63,6 +89,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 +119,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>
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useEffect } from 'react';
|
||||
import { titlefy } from '@vegaprotocol/utils';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { RewardsContainer } from '../../components/rewards-container';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
|
||||
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>
|
||||
<RewardsContainer />
|
||||
</div>
|
||||
<ErrorBoundary feature="rewards">
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
|
||||
<RewardsContainer />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ErrorBoundary } from './error-boundary';
|
||||
import { localLoggerFactory } from '@vegaprotocol/logger';
|
||||
|
||||
jest.mock('@vegaprotocol/logger', () => ({
|
||||
localLoggerFactory: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('ErrorBoundary', () => {
|
||||
const mockLogError = jest.fn();
|
||||
const originalConsoleError = console.error;
|
||||
const mockLoggerFactory = localLoggerFactory as jest.Mock;
|
||||
|
||||
beforeAll(() => {
|
||||
console.error = () => {};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockLoggerFactory.mockImplementation(() => ({
|
||||
error: mockLogError,
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockLogError.mockClear();
|
||||
});
|
||||
|
||||
it('renders children', () => {
|
||||
render(
|
||||
<ErrorBoundary feature="feature">
|
||||
<div data-testid="child" />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('child')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders fallback ui and logs an error', () => {
|
||||
const error = new Error('bork!');
|
||||
const BorkedComponent = () => {
|
||||
throw error;
|
||||
};
|
||||
|
||||
render(
|
||||
<ErrorBoundary feature="test-feature">
|
||||
<BorkedComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
||||
expect(mockLogError).toHaveBeenCalledTimes(1);
|
||||
expect(mockLogError).toHaveBeenCalledWith(
|
||||
error.message,
|
||||
expect.stringContaining('componentStack')
|
||||
);
|
||||
});
|
||||
|
||||
it('renders fallback render prop if error', () => {
|
||||
const error = new Error('bork!');
|
||||
const BorkedComponent = () => {
|
||||
throw error;
|
||||
};
|
||||
|
||||
render(
|
||||
<ErrorBoundary
|
||||
feature="test-feature"
|
||||
fallback={<div data-testid="custom-ui" />}
|
||||
>
|
||||
<BorkedComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('custom-ui')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { localLoggerFactory, type LocalLogger } from '@vegaprotocol/logger';
|
||||
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
||||
import { useT } from '../../lib/use-t';
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
feature: string;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<
|
||||
ErrorBoundaryProps,
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
logger: LocalLogger | null = null;
|
||||
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
|
||||
this.logger = localLoggerFactory({ application: props.feature });
|
||||
|
||||
this.state = {
|
||||
hasError: false,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
if (this.logger) {
|
||||
this.logger.error(error.message, JSON.stringify(info));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return this.props.fallback || <DefaultFallback />;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const DefaultFallback = () => {
|
||||
const t = useT();
|
||||
return <p className="text-xs">{t('Something went wrong')}</p>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { ErrorBoundary } from './error-boundary';
|
||||
@@ -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
@@ -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;
|
||||
@@ -310,16 +310,25 @@ export const CurrentVolume = ({
|
||||
const t = useT();
|
||||
const nextTier = tiers[tierIndex + 1];
|
||||
const requiredForNextTier = nextTier
|
||||
? Number(nextTier.minimumRunningNotionalTakerVolume) - windowLengthVolume
|
||||
: 0;
|
||||
? new BigNumber(nextTier.minimumRunningNotionalTakerVolume).minus(
|
||||
windowLengthVolume
|
||||
)
|
||||
: new BigNumber(0);
|
||||
const currentVolume = new BigNumber(windowLengthVolume);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 pt-4">
|
||||
<CardStat
|
||||
value={formatNumberRounded(new BigNumber(windowLengthVolume))}
|
||||
text={t('Past {{count}} epochs', { count: windowLength })}
|
||||
value={
|
||||
currentVolume.isZero()
|
||||
? `<${formatNumberRounded(requiredForNextTier)}`
|
||||
: formatNumberRounded(currentVolume)
|
||||
}
|
||||
text={t('pastEpochs', 'Past {{count}} epochs', {
|
||||
count: windowLength,
|
||||
})}
|
||||
/>
|
||||
{requiredForNextTier > 0 && (
|
||||
{requiredForNextTier.isGreaterThan(0) && (
|
||||
<CardStat
|
||||
value={formatNumber(requiredForNextTier)}
|
||||
text={t('Required for next tier')}
|
||||
@@ -344,9 +353,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 +466,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 +529,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
|
||||
|
||||
@@ -40,6 +40,9 @@ const DateRange = {
|
||||
RANGE_ALL: 'All',
|
||||
};
|
||||
|
||||
const priceFormat = (fundingRate: number) =>
|
||||
`${(fundingRate * 100).toFixed(4)}%`;
|
||||
|
||||
export const FundingContainer = ({ marketId }: { marketId: string }) => {
|
||||
const t = useT();
|
||||
const { theme } = useThemeSwitcher();
|
||||
@@ -65,7 +68,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 +85,8 @@ export const FundingContainer = ({ marketId }: { marketId: string }) => {
|
||||
<LineChart
|
||||
data={values}
|
||||
theme={theme}
|
||||
priceFormat={(fundingRate) => `${fundingRate.toFixed(4)}%`}
|
||||
priceFormat={priceFormat}
|
||||
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 (
|
||||
|
||||
@@ -172,4 +172,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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import uniq from 'lodash/uniq';
|
||||
import type { Account } from '@vegaprotocol/accounts';
|
||||
import { useAccounts } from '@vegaprotocol/accounts';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
@@ -31,42 +31,83 @@ 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';
|
||||
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
||||
|
||||
const ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA = [
|
||||
'bf1e88d19db4b3ca0d1d5bdb73718a01686b18cf731ca26adedf3c8b83802bba', // USDT mainnet
|
||||
'8ba0b10971f0c4747746cd01ff05a53ae75ca91eba1d4d050b527910c983e27e', // USDT testnet
|
||||
];
|
||||
|
||||
export const RewardsContainer = () => {
|
||||
const t = useT();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { params, loading: paramsLoading } = useNetworkParams([
|
||||
NetworkParams.reward_asset,
|
||||
NetworkParams.rewards_activityStreak_benefitTiers,
|
||||
NetworkParams.rewards_vesting_baseRate,
|
||||
]);
|
||||
|
||||
const { data: accounts, loading: accountsLoading } = useAccounts(pubKey);
|
||||
|
||||
const { data: assetMap } = useAssetsMapProvider();
|
||||
|
||||
const { data: epochData } = useRewardsEpochQuery();
|
||||
|
||||
// No need to specify the fromEpoch as it will by default give you the last
|
||||
// Note activityStreak in query will fail
|
||||
const { data: rewardsData, loading: rewardsLoading } = useRewardsPageQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
},
|
||||
// Inclusion of activity streak in query currently fails
|
||||
errorPolicy: 'ignore',
|
||||
});
|
||||
|
||||
if (!epochData?.epoch) return null;
|
||||
if (!epochData?.epoch || !assetMap) return null;
|
||||
|
||||
const loading = paramsLoading || accountsLoading || rewardsLoading;
|
||||
|
||||
const rewardAccounts = accounts
|
||||
? accounts.filter((a) =>
|
||||
[
|
||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
AccountType.ACCOUNT_TYPE_VESTING_REWARDS,
|
||||
].includes(a.type)
|
||||
)
|
||||
? accounts
|
||||
.filter((a) =>
|
||||
[
|
||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
AccountType.ACCOUNT_TYPE_VESTING_REWARDS,
|
||||
].includes(a.type)
|
||||
)
|
||||
.filter((a) => new BigNumber(a.balance).isGreaterThan(0))
|
||||
: [];
|
||||
|
||||
const rewardAssetsMap = groupBy(
|
||||
rewardAccounts.filter((a) => a.asset.id !== params.reward_asset),
|
||||
'asset.id'
|
||||
);
|
||||
const rewardAccountsAssetMap = groupBy(rewardAccounts, 'asset.id');
|
||||
|
||||
const lockedBalances = rewardsData?.party?.vestingBalancesSummary
|
||||
.lockedBalances
|
||||
? rewardsData.party.vestingBalancesSummary.lockedBalances.filter((b) =>
|
||||
new BigNumber(b.balance).isGreaterThan(0)
|
||||
)
|
||||
: [];
|
||||
const lockedAssetMap = groupBy(lockedBalances, 'asset.id');
|
||||
|
||||
const vestingBalances = rewardsData?.party?.vestingBalancesSummary
|
||||
.vestingBalances
|
||||
? rewardsData.party.vestingBalancesSummary.vestingBalances.filter((b) =>
|
||||
new BigNumber(b.balance).isGreaterThan(0)
|
||||
)
|
||||
: [];
|
||||
const vestingAssetMap = groupBy(vestingBalances, 'asset.id');
|
||||
|
||||
// each asset reward pot is made up of:
|
||||
// available to withdraw - ACCOUNT_TYPE_VESTED_REWARDS
|
||||
// vesting - vestingBalancesSummary.vestingBalances
|
||||
// locked - vestingBalancesSummary.lockedBalances
|
||||
//
|
||||
// there can be entires for the same asset in each list so we need a uniq list of assets
|
||||
const assets = uniq([
|
||||
...Object.keys(rewardAccountsAssetMap),
|
||||
...Object.keys(lockedAssetMap),
|
||||
...Object.keys(vestingAssetMap),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="grid auto-rows-min grid-cols-6 gap-3">
|
||||
@@ -116,26 +157,72 @@ export const RewardsContainer = () => {
|
||||
</Card>
|
||||
|
||||
{/* Show all other reward pots, most of the time users will not have other rewards */}
|
||||
{Object.keys(rewardAssetsMap).map((assetId) => {
|
||||
const asset = rewardAssetsMap[assetId][0].asset;
|
||||
return (
|
||||
<Card
|
||||
key={assetId}
|
||||
title={t('%s Reward pot', asset.symbol)}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={assetId}
|
||||
vestingBalancesSummary={
|
||||
rewardsData?.party?.vestingBalancesSummary
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
{assets
|
||||
.filter((assetId) => assetId !== params.reward_asset)
|
||||
.map((assetId) => {
|
||||
const asset = assetMap ? assetMap[assetId] : null;
|
||||
|
||||
if (!asset) return null;
|
||||
|
||||
// Following code is for mitigating an issue due to a core bug where locked and vesting
|
||||
// balances were incorrectly increased for infrastructure rewards for USDT on mainnet
|
||||
//
|
||||
// We don't want to incorrectly show the wring locked/vesting values, but we DO want to
|
||||
// show the user that they have rewards available to withdraw
|
||||
if (ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA.includes(asset.id)) {
|
||||
const accountsForAsset = rewardAccountsAssetMap[asset.id];
|
||||
const vestedAccount = accountsForAsset?.find(
|
||||
(a) => a.type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||
);
|
||||
|
||||
// No vested rewards available to withdraw, so skip over USDT
|
||||
if (!vestedAccount || Number(vestedAccount.balance) <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={assetId}
|
||||
title={t('{{assetSymbol}} Reward pot', {
|
||||
assetSymbol: asset.symbol,
|
||||
})}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={assetId}
|
||||
// Ensure that these values are shown as 0
|
||||
vestingBalancesSummary={{
|
||||
lockedBalances: [],
|
||||
vestingBalances: [],
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={assetId}
|
||||
title={t('{{assetSymbol}} Reward pot', {
|
||||
assetSymbol: asset.symbol,
|
||||
})}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={assetId}
|
||||
vestingBalancesSummary={
|
||||
rewardsData?.party?.vestingBalancesSummary
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
<Card
|
||||
title={t('Rewards history')}
|
||||
className="lg:col-span-full"
|
||||
@@ -144,6 +231,7 @@ export const RewardsContainer = () => {
|
||||
<RewardsHistoryContainer
|
||||
epoch={Number(epochData?.epoch.id)}
|
||||
pubKey={pubKey}
|
||||
assets={assetMap}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -167,6 +255,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 +331,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 +345,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(),
|
||||
@@ -303,13 +398,14 @@ export const RewardPot = ({
|
||||
export const Vesting = ({
|
||||
pubKey,
|
||||
baseRate,
|
||||
multiplier = '1',
|
||||
multiplier,
|
||||
}: {
|
||||
pubKey: string | null;
|
||||
baseRate: string;
|
||||
multiplier?: string;
|
||||
}) => {
|
||||
const rate = new BigNumber(baseRate).times(multiplier);
|
||||
const t = useT();
|
||||
const rate = new BigNumber(baseRate).times(multiplier || 1);
|
||||
const rateFormatted = formatPercentage(Number(rate));
|
||||
const baseRateFormatted = formatPercentage(Number(baseRate));
|
||||
|
||||
@@ -324,7 +420,7 @@ export const Vesting = ({
|
||||
{pubKey && (
|
||||
<tr>
|
||||
<CardTableTH>{t('Vesting multiplier')}</CardTableTH>
|
||||
<CardTableTD>{multiplier}x</CardTableTD>
|
||||
<CardTableTD>{multiplier ? `${multiplier}x` : '-'}</CardTableTD>
|
||||
</tr>
|
||||
)}
|
||||
</CardTable>
|
||||
@@ -334,15 +430,16 @@ export const Vesting = ({
|
||||
|
||||
export const Multipliers = ({
|
||||
pubKey,
|
||||
streakMultiplier = '1',
|
||||
hoarderMultiplier = '1',
|
||||
streakMultiplier,
|
||||
hoarderMultiplier,
|
||||
}: {
|
||||
pubKey: string | null;
|
||||
streakMultiplier?: string;
|
||||
hoarderMultiplier?: string;
|
||||
}) => {
|
||||
const combinedMultiplier = new BigNumber(streakMultiplier).times(
|
||||
hoarderMultiplier
|
||||
const t = useT();
|
||||
const combinedMultiplier = new BigNumber(streakMultiplier || 1).times(
|
||||
hoarderMultiplier || 1
|
||||
);
|
||||
|
||||
if (!pubKey) {
|
||||
@@ -363,11 +460,15 @@ export const Multipliers = ({
|
||||
<CardTable>
|
||||
<tr>
|
||||
<CardTableTH>{t('Streak reward multiplier')}</CardTableTH>
|
||||
<CardTableTD>{streakMultiplier}x</CardTableTD>
|
||||
<CardTableTD>
|
||||
{streakMultiplier ? `${streakMultiplier}x` : '-'}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<CardTableTH>{t('Hoarder reward multiplier')}</CardTableTH>
|
||||
<CardTableTD>{hoarderMultiplier}x</CardTableTD>
|
||||
<CardTableTD>
|
||||
{hoarderMultiplier ? `${hoarderMultiplier}x` : '-'}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
</CardTable>
|
||||
</div>
|
||||
|
||||
@@ -61,6 +61,14 @@ const rewardSummaries = [
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
epoch: 7,
|
||||
assetId: assets.asset2.id,
|
||||
amount: '300',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getCell = (cells: HTMLElement[], colId: string) => {
|
||||
@@ -69,7 +77,7 @@ const getCell = (cells: HTMLElement[], colId: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
describe('RewarsHistoryTable', () => {
|
||||
describe('RewardsHistoryTable', () => {
|
||||
const props = {
|
||||
epochRewardSummaries: {
|
||||
edges: rewardSummaries,
|
||||
@@ -88,7 +96,7 @@ describe('RewarsHistoryTable', () => {
|
||||
loading: false,
|
||||
};
|
||||
|
||||
it('Renders table with accounts summed up by asset', () => {
|
||||
it('renders table with accounts summed up by asset', () => {
|
||||
render(<RewardHistoryTable {...props} />);
|
||||
|
||||
const container = within(
|
||||
@@ -110,17 +118,27 @@ describe('RewarsHistoryTable', () => {
|
||||
assets.asset2.name
|
||||
);
|
||||
|
||||
// First row
|
||||
const marketCreationCell = getCell(cells, 'marketCreation');
|
||||
expect(
|
||||
marketCreationCell.getByTestId('stack-cell-primary')
|
||||
).toHaveTextContent('300');
|
||||
expect(
|
||||
marketCreationCell.getByTestId('stack-cell-secondary')
|
||||
).toHaveTextContent('100.00%');
|
||||
).toHaveTextContent('50.00%');
|
||||
|
||||
const infrastructureFeesCell = getCell(cells, 'infrastructureFees');
|
||||
expect(
|
||||
infrastructureFeesCell.getByTestId('stack-cell-primary')
|
||||
).toHaveTextContent('300');
|
||||
expect(
|
||||
infrastructureFeesCell.getByTestId('stack-cell-secondary')
|
||||
).toHaveTextContent('50.00%');
|
||||
|
||||
let totalCell = getCell(cells, 'total');
|
||||
expect(totalCell.getByText('300.00')).toBeInTheDocument();
|
||||
expect(totalCell.getByText('600.00')).toBeInTheDocument();
|
||||
|
||||
// Second row
|
||||
row = within(rows[1]);
|
||||
cells = row.getAllByRole('gridcell');
|
||||
|
||||
|
||||
@@ -2,10 +2,7 @@ import debounce from 'lodash/debounce';
|
||||
import { useMemo, useState } from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ColDef, ValueFormatterFunc } from 'ag-grid-community';
|
||||
import {
|
||||
useAssetsMapProvider,
|
||||
type AssetFieldsFragment,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { type AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import {
|
||||
addDecimalsFormatNumberQuantum,
|
||||
formatNumberPercentage,
|
||||
@@ -16,27 +13,27 @@ 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,
|
||||
pubKey,
|
||||
assets,
|
||||
}: {
|
||||
pubKey: string | null;
|
||||
epoch: number;
|
||||
assets: Record<string, AssetFieldsFragment>;
|
||||
}) => {
|
||||
const [epochVariables, setEpochVariables] = useState(() => ({
|
||||
from: epoch - 1,
|
||||
to: epoch,
|
||||
}));
|
||||
|
||||
const { data: assets } = useAssetsMapProvider();
|
||||
|
||||
// No need to specify the fromEpoch as it will by default give you the last
|
||||
const { refetch, data, loading } = useRewardsHistoryQuery({
|
||||
variables: {
|
||||
@@ -140,6 +137,7 @@ export const RewardHistoryTable = ({
|
||||
onEpochChange: (epochVariables: { from: number; to: number }) => void;
|
||||
loading: boolean;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const [isParty, setIsParty] = useState(false);
|
||||
|
||||
const rowData = useRewardsRowData({
|
||||
@@ -153,10 +151,12 @@ export const RewardHistoryTable = ({
|
||||
const rewardValueFormatter: ValueFormatterFunc<RewardRow> = ({
|
||||
data,
|
||||
value,
|
||||
...rest
|
||||
}) => {
|
||||
if (!value || !data) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return addDecimalsFormatNumberQuantum(
|
||||
value,
|
||||
data.asset.decimals,
|
||||
@@ -196,6 +196,11 @@ export const RewardHistoryTable = ({
|
||||
},
|
||||
sort: 'desc',
|
||||
},
|
||||
{
|
||||
field: 'infrastructureFees',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'staking',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { type AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import { getRewards } from './use-reward-row-data';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const asset1 = {
|
||||
id: 'asset1',
|
||||
name: 'USD (KRW)',
|
||||
symbol: 'USD-KRW',
|
||||
decimals: 6,
|
||||
quantum: '1000000',
|
||||
status: Schema.AssetStatus.STATUS_ENABLED,
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
} as AssetFieldsFragment;
|
||||
|
||||
const asset2 = {
|
||||
id: 'asset2',
|
||||
name: 'tDAI TEST',
|
||||
symbol: 'tDAI',
|
||||
decimals: 5,
|
||||
quantum: '1',
|
||||
status: Schema.AssetStatus.STATUS_ENABLED,
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
} as AssetFieldsFragment;
|
||||
|
||||
const asset3 = {
|
||||
id: 'asset3',
|
||||
name: 'Tether USD',
|
||||
symbol: 'USDT',
|
||||
decimals: 6,
|
||||
quantum: '1000000',
|
||||
status: Schema.AssetStatus.STATUS_ENABLED,
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
} as AssetFieldsFragment;
|
||||
|
||||
const asset4 = {
|
||||
id: 'asset4',
|
||||
name: 'USDT-T',
|
||||
symbol: 'USDT-T',
|
||||
decimals: 18,
|
||||
quantum: '1',
|
||||
status: Schema.AssetStatus.STATUS_ENABLED,
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
} as AssetFieldsFragment;
|
||||
|
||||
const assets: Record<string, AssetFieldsFragment> = {
|
||||
asset1,
|
||||
asset2,
|
||||
asset3,
|
||||
asset4,
|
||||
};
|
||||
|
||||
const testData = {
|
||||
rewards: [
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
assetId: 'asset1',
|
||||
amount: '31897424',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
assetId: 'asset2',
|
||||
amount: '57',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
assetId: 'asset3',
|
||||
amount: '5501',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
|
||||
assetId: 'asset3',
|
||||
amount: '5501',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
|
||||
assetId: 'asset4',
|
||||
amount: '5501',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
assetId: 'asset4',
|
||||
amount: '456',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING,
|
||||
assetId: 'asset4',
|
||||
amount: '4565',
|
||||
},
|
||||
],
|
||||
assets,
|
||||
};
|
||||
|
||||
describe('getRewards', () => {
|
||||
it('should return the correct rewards when infra fees are included', () => {
|
||||
const rewards = getRewards(testData.rewards, testData.assets);
|
||||
expect(rewards).toEqual([
|
||||
{
|
||||
asset: asset1,
|
||||
infrastructureFees: 31897424,
|
||||
staking: 0,
|
||||
priceTaking: 0,
|
||||
priceMaking: 0,
|
||||
liquidityProvision: 0,
|
||||
marketCreation: 0,
|
||||
averagePosition: 0,
|
||||
relativeReturns: 0,
|
||||
returnsVolatility: 0,
|
||||
validatorRanking: 0,
|
||||
total: 31897424,
|
||||
},
|
||||
{
|
||||
asset: asset2,
|
||||
infrastructureFees: 57,
|
||||
staking: 0,
|
||||
priceTaking: 0,
|
||||
priceMaking: 0,
|
||||
liquidityProvision: 0,
|
||||
marketCreation: 0,
|
||||
averagePosition: 0,
|
||||
relativeReturns: 0,
|
||||
returnsVolatility: 0,
|
||||
validatorRanking: 0,
|
||||
total: 57,
|
||||
},
|
||||
{
|
||||
asset: asset3,
|
||||
infrastructureFees: 5501,
|
||||
staking: 0,
|
||||
priceTaking: 0,
|
||||
priceMaking: 0,
|
||||
liquidityProvision: 0,
|
||||
marketCreation: 0,
|
||||
averagePosition: 5501,
|
||||
relativeReturns: 0,
|
||||
returnsVolatility: 0,
|
||||
validatorRanking: 0,
|
||||
total: 11002,
|
||||
},
|
||||
{
|
||||
asset: asset4,
|
||||
infrastructureFees: 0,
|
||||
staking: 0,
|
||||
priceTaking: 0,
|
||||
priceMaking: 5501,
|
||||
liquidityProvision: 456,
|
||||
marketCreation: 0,
|
||||
averagePosition: 0,
|
||||
relativeReturns: 0,
|
||||
returnsVolatility: 0,
|
||||
validatorRanking: 4565,
|
||||
total: 10522,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -16,9 +16,10 @@ const REWARD_ACCOUNT_TYPES = [
|
||||
AccountType.ACCOUNT_TYPE_REWARD_RELATIVE_RETURN,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING,
|
||||
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
];
|
||||
|
||||
const getRewards = (
|
||||
export const getRewards = (
|
||||
rewards: Array<{
|
||||
rewardType: AccountType;
|
||||
assetId: string;
|
||||
@@ -56,6 +57,9 @@ const getRewards = (
|
||||
|
||||
return {
|
||||
asset,
|
||||
infrastructureFees: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE
|
||||
),
|
||||
staking: totals.get(AccountType.ACCOUNT_TYPE_GLOBAL_REWARD),
|
||||
priceTaking: totals.get(AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES),
|
||||
priceMaking: totals.get(
|
||||
@@ -101,7 +105,8 @@ export const useRewardsRowData = ({
|
||||
assetId: r.asset.id,
|
||||
amount: r.amount,
|
||||
}));
|
||||
return getRewards(rewards, assets);
|
||||
const result = getRewards(rewards, assets);
|
||||
return result;
|
||||
}
|
||||
|
||||
const rewards = removePaginationWrapper(epochRewardSummaries?.edges);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 && (
|
||||
|
||||
@@ -16,6 +16,7 @@ import { GetStarted } from '../welcome-dialog';
|
||||
import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
|
||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ErrorBoundary } from '../error-boundary';
|
||||
|
||||
export enum ViewType {
|
||||
Order = 'Order',
|
||||
@@ -163,12 +164,14 @@ export const SidebarContent = () => {
|
||||
if (params.marketId) {
|
||||
return (
|
||||
<ContentWrapper>
|
||||
<DealTicketContainer
|
||||
marketId={params.marketId}
|
||||
onDeposit={(assetId) =>
|
||||
setViews({ type: ViewType.Deposit, assetId }, currentRouteId)
|
||||
}
|
||||
/>
|
||||
<ErrorBoundary feature="deal-ticket">
|
||||
<DealTicketContainer
|
||||
marketId={params.marketId}
|
||||
onDeposit={(assetId) =>
|
||||
setViews({ type: ViewType.Deposit, assetId }, currentRouteId)
|
||||
}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<GetStarted />
|
||||
</ContentWrapper>
|
||||
);
|
||||
@@ -181,7 +184,9 @@ export const SidebarContent = () => {
|
||||
if (params.marketId) {
|
||||
return (
|
||||
<ContentWrapper>
|
||||
<MarketInfoAccordionContainer marketId={params.marketId} />
|
||||
<ErrorBoundary feature="market-info">
|
||||
<MarketInfoAccordionContainer marketId={params.marketId} />
|
||||
</ErrorBoundary>
|
||||
</ContentWrapper>
|
||||
);
|
||||
} else {
|
||||
@@ -192,7 +197,9 @@ export const SidebarContent = () => {
|
||||
if (view.type === ViewType.Deposit) {
|
||||
return (
|
||||
<ContentWrapper title={t('Deposit')}>
|
||||
<DepositContainer assetId={view.assetId} />
|
||||
<ErrorBoundary feature="deposit">
|
||||
<DepositContainer assetId={view.assetId} />
|
||||
</ErrorBoundary>
|
||||
</ContentWrapper>
|
||||
);
|
||||
}
|
||||
@@ -200,7 +207,9 @@ export const SidebarContent = () => {
|
||||
if (view.type === ViewType.Withdraw) {
|
||||
return (
|
||||
<ContentWrapper title={t('Withdraw')}>
|
||||
<WithdrawContainer assetId={view.assetId} />
|
||||
<ErrorBoundary feature="withdraw">
|
||||
<WithdrawContainer assetId={view.assetId} />
|
||||
</ErrorBoundary>
|
||||
</ContentWrapper>
|
||||
);
|
||||
}
|
||||
@@ -208,7 +217,9 @@ export const SidebarContent = () => {
|
||||
if (view.type === ViewType.Transfer) {
|
||||
return (
|
||||
<ContentWrapper title={t('Transfer')}>
|
||||
<TransferContainer assetId={view.assetId} />
|
||||
<ErrorBoundary feature="transfer">
|
||||
<TransferContainer assetId={view.assetId} />
|
||||
</ErrorBoundary>
|
||||
</ContentWrapper>
|
||||
);
|
||||
}
|
||||
@@ -216,7 +227,9 @@ export const SidebarContent = () => {
|
||||
if (view.type === ViewType.Settings) {
|
||||
return (
|
||||
<ContentWrapper title={t('Settings')}>
|
||||
<Settings />
|
||||
<ErrorBoundary feature="settings">
|
||||
<Settings />
|
||||
</ErrorBoundary>
|
||||
</ContentWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { positionsDataProvider } from '@vegaprotocol/positions';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
|
||||
const ONBOARDING_STORAGE_KEY = 'vega_onboarding';
|
||||
|
||||
export const useOnboardingStore = create<{
|
||||
dialogOpen: boolean;
|
||||
walletDialogOpen: boolean;
|
||||
@@ -20,7 +21,7 @@ export const useOnboardingStore = create<{
|
||||
}>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
dialogOpen: true,
|
||||
dialogOpen: false,
|
||||
walletDialogOpen: false,
|
||||
dismissed: false,
|
||||
dismiss: () => set({ dismissed: true }),
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import { useEffect } from 'react';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { WelcomeDialogContent } from './welcome-dialog-content';
|
||||
import { useOnboardingStore } from './use-get-onboarding-step';
|
||||
import { VegaConnectDialog } from '@vegaprotocol/wallet';
|
||||
import { Connectors } from '../../lib/vega-connectors';
|
||||
import { RiskMessage } from './risk-message';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { Routes } from '../../lib/links';
|
||||
import { RiskMessage } from './risk-message';
|
||||
import { WelcomeDialogContent } from './welcome-dialog-content';
|
||||
import { useOnboardingStore } from './use-get-onboarding-step';
|
||||
import { ensureSuffix } from '@vegaprotocol/utils';
|
||||
|
||||
/**
|
||||
* A list of paths on which the welcome dialog should be omitted.
|
||||
*/
|
||||
const OMIT_ON_LIST = [ensureSuffix(Routes.REFERRALS, '/*')];
|
||||
|
||||
export const WelcomeDialog = () => {
|
||||
const { pathname } = useLocation();
|
||||
const t = useT();
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
const dismissed = useOnboardingStore((store) => store.dismissed);
|
||||
const dialogOpen = useOnboardingStore((store) => store.dialogOpen);
|
||||
const dismiss = useOnboardingStore((store) => store.dismiss);
|
||||
const setDialogOpen = useOnboardingStore((store) => store.setDialogOpen);
|
||||
const walletDialogOpen = useOnboardingStore(
|
||||
(store) => store.walletDialogOpen
|
||||
);
|
||||
@@ -20,6 +31,16 @@ export const WelcomeDialog = () => {
|
||||
(store) => store.setWalletDialogOpen
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const shouldOmit = OMIT_ON_LIST.map((path) =>
|
||||
matchPath(path, pathname)
|
||||
).some((m) => !!m);
|
||||
|
||||
if (dismissed || shouldOmit) return;
|
||||
|
||||
setDialogOpen(true);
|
||||
}, [dismissed, pathname, setDialogOpen]);
|
||||
|
||||
const content = walletDialogOpen ? (
|
||||
<VegaConnectDialog
|
||||
connectors={Connectors}
|
||||
@@ -31,7 +52,12 @@ export const WelcomeDialog = () => {
|
||||
<WelcomeDialogContent />
|
||||
);
|
||||
|
||||
const onClose = walletDialogOpen ? () => setWalletDialogOpen(false) : dismiss;
|
||||
const onClose = walletDialogOpen
|
||||
? () => setWalletDialogOpen(false)
|
||||
: () => {
|
||||
setDialogOpen(false);
|
||||
dismiss();
|
||||
};
|
||||
|
||||
const title = walletDialogOpen ? null : (
|
||||
<span className="font-alpha calt" data-testid="welcome-title">
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
|
||||
VEGA_VERSION=v0.73.4
|
||||
VEGA_VERSION=v0.73.8
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:develop
|
||||
VEGA_VERSION=v0.73.4
|
||||
VEGA_VERSION=v0.73.8
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:main
|
||||
VEGA_VERSION=v0.73.4
|
||||
VEGA_VERSION=v0.73.8
|
||||
|
||||
@@ -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()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user