Compare commits

..
Author SHA1 Message Date
Matthew Russell a87a4a1af5 chore: formatting 2023-11-19 13:30:26 -08:00
Bartłomiej Głownia ddc62e6913 feat(utils): use i18next 2023-11-19 13:20:37 -08:00
287 changed files with 3617 additions and 6130 deletions
-1
View File
@@ -77,7 +77,6 @@
"fixStyle": "inline-type-imports"
}
],
"@typescript-eslint/no-useless-constructor": 0,
"curly": ["error", "multi-line"]
}
},
+2 -2
View File
@@ -10,7 +10,7 @@ on:
inputs:
console-test-branch:
type: choice
description: 'main: v0.73.5, develop: v0.73.5'
description: 'main: v0.72.14, develop: v0.73.4'
options:
- main
- develop
@@ -215,7 +215,7 @@ jobs:
if: always()
with:
name: playwright-trace
path: apps/trading/e2e/traces/
path: ./traces/
retention-days: 15
#----------------------------------------------
# ----- upload logs -----
+2 -1
View File
@@ -59,4 +59,5 @@ apps/trading/e2e/logs/
apps/trading/e2e/.pytest_cache/
apps/trading/e2e/traces/
.nx/
.nx/cache
+14 -19
View File
@@ -1,4 +1,3 @@
import '../i18n';
import {
NetworkLoader,
NodeFailure,
@@ -29,24 +28,20 @@ function App() {
);
return (
<TendermintWebsocketProvider>
<Suspense fallback={splashLoading}>
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<NodeGuard
skeleton={<div>{t('Loading')}</div>}
failure={
<NodeFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />
}
>
<Suspense fallback={splashLoading}>
<RouterProvider router={router} fallbackElement={splashLoading} />
</Suspense>
</NodeGuard>
<NodeSwitcherDialog
open={nodeSwitcherOpen}
setOpen={setNodeSwitcherOpen}
/>
</NetworkLoader>
</Suspense>
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<NodeGuard
skeleton={<div>{t('Loading')}</div>}
failure={<NodeFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
>
<Suspense fallback={splashLoading}>
<RouterProvider router={router} fallbackElement={splashLoading} />
</Suspense>
</NodeGuard>
<NodeSwitcherDialog
open={nodeSwitcherOpen}
setOpen={setNodeSwitcherOpen}
/>
</NetworkLoader>
</TendermintWebsocketProvider>
);
}
@@ -12,8 +12,7 @@ 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 ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community';
import { type RowClickedEvent, ColDef } from 'ag-grid-community';
type AssetsTableProps = {
data: AssetFieldsFragment[] | null;
@@ -6,7 +6,6 @@ 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;
@@ -70,7 +69,7 @@ const DeterministicOrderDetails = ({
<span className="mx-5 text-base">@</span>
<PriceInMarket price={o.price} marketId={o.market.id} />
</h2>
<p className="text-gray-400 dark:text-gray-600">
<p className="text-gray-200">
In <MarketLink id={o.market.id} /> at <Time date={o.createdAt} />.
</p>
{o.peggedOrder ? (
@@ -84,12 +83,13 @@ 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-5 gap-x-6 mt-4">
<div className="grid md:grid-cols-4 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,16 +114,6 @@ 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,8 +8,7 @@ import {
type VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { type ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community';
import { type RowClickedEvent, ColDef } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
@@ -1,59 +0,0 @@
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,7 +9,6 @@ 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;
@@ -76,12 +75,6 @@ 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,12 +80,6 @@ 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,8 +98,6 @@ describe('TxDetailsTransfer', () => {
},
},
signature: {
version: '1',
algo: 'vega/ed25519',
value:
'610c2e196a7d4fed4413b9e82af267b1ff3e30e943df3a3d28096fd60604d430d752fbaf6dd4f84d496be78885bb6118f40560bff7832c06bd7a3d67b718b700',
},
@@ -20,8 +20,6 @@ const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
'4b782482f587d291e8614219eb9a5ee9280fa2c58982dee71d976782a9be1964',
type: 'Submit Order',
signature: {
version: '1',
algo: 'vega/ed25519',
value: '123',
},
code: 0,
@@ -23,8 +23,6 @@ const txData: BlockExplorerTransactionResult = {
type: 'type',
command: {} as ValidatorHeartbeat,
signature: {
version: '1',
algo: 'vega/ed25519',
value: '123',
},
};
@@ -11,8 +11,6 @@ export interface BlockExplorerTransactionResult {
cursor: string;
command: components['schemas']['blockexplorerv1transaction'];
signature: {
version: string;
algo: string;
value: string;
};
error?: string;
-14
View File
@@ -3,9 +3,6 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
import { locales } from '@vegaprotocol/i18n';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
Object.defineProperty(window, 'ResizeObserver', {
writable: false,
@@ -16,14 +13,3 @@ Object.defineProperty(window, 'ResizeObserver', {
disconnect: jest.fn(),
})),
});
// Set up i18n instance so that components have the correct default
// en translations
i18n.use(initReactI18next).init({
// we init with resources
resources: locales,
fallbackLng: 'en',
nsSeparator: false,
ns: ['explorer'],
defaultNS: 'explorer',
});
-1
View File
@@ -1 +0,0 @@
../../../../libs/i18n/src/locales
-45
View File
@@ -1,45 +0,0 @@
import type { Module } from 'i18next';
import i18n from 'i18next';
import HttpBackend from 'i18next-http-backend';
import LocizeBackend from 'i18next-locize-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
const isInDev = process.env.NODE_ENV === 'development';
const useLocize = isInDev && !!process.env.NX_USE_LOCIZE;
const backend = useLocize
? {
projectId: '96ac1231-4bdd-455a-b9d7-f5322a2e7430',
apiKey: process.env.NX_LOCIZE_API_KEY,
referenceLng: 'en',
}
: {
loadPath: '/assets/locales/{{lng}}/{{ns}}.json',
};
const Backend: Module = useLocize ? LocizeBackend : HttpBackend;
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
lng: 'en',
fallbackLng: 'en',
supportedLngs: ['en'],
load: 'languageOnly',
debug: isInDev,
// have a common namespace used around the full app
ns: ['explorer'],
defaultNS: 'explorer',
keySeparator: false, // we use content as keys
nsSeparator: false,
backend,
saveMissing: useLocize && !!process.env.NX_LOCIZE_API_KEY,
interpolation: {
escapeValue: false,
},
});
export default i18n;
@@ -23,13 +23,10 @@ export const Heading = ({
})}
>
<h1
className={classNames(
'font-alpha calt text-5xl [word-break:break-word]',
{
'mt-0': !marginTop,
'mb-0': !marginBottom,
}
)}
className={classNames('font-alpha calt text-5xl break-words', {
'mt-0': !marginTop,
'mb-0': !marginBottom,
})}
>
{title}
</h1>
@@ -49,8 +49,12 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
? activeProvider
: defaultProvider;
if (account && provider && typeof provider.getSigner === 'function') {
signer = provider.getSigner(account);
if (
account &&
activeProvider &&
typeof activeProvider.getSigner === 'function'
) {
signer = provider.getSigner();
}
const tokenVestingAddress =
@@ -7,10 +7,8 @@ import type { AssetFieldsFragment } from '@vegaprotocol/assets';
export const ProposalAssetDetails = ({
asset,
originalAsset,
}: {
asset: AssetFieldsFragment;
originalAsset?: AssetFieldsFragment;
}) => {
const { t } = useTranslation();
const [showAssetDetails, setShowAssetDetails] = useState(false);
@@ -29,7 +27,6 @@ export const ProposalAssetDetails = ({
<div className="mb-10 pb-4">
<AssetDetailsTable
asset={asset}
originalAsset={originalAsset}
omitRows={[
AssetDetail.STATUS,
AssetDetail.INFRASTRUCTURE_FEE_ACCOUNT_BALANCE,
@@ -54,8 +54,8 @@ export const ProposalReferralProgramDetails = ({
return null;
}
const benefitTiers = proposal?.terms?.change?.benefitTiers.slice();
const stakingTiers = proposal?.terms?.change?.stakingTiers.slice();
const benefitTiers = proposal?.terms?.change?.benefitTiers;
const stakingTiers = proposal?.terms?.change?.stakingTiers;
const windowLength = proposal?.terms?.change?.windowLength;
const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgram;
@@ -65,13 +65,10 @@ 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') {
@@ -231,7 +228,7 @@ export const Proposal = ({
proposal.terms.change.__typename === 'UpdateAsset') &&
asset && (
<div className="mb-4">
<ProposalAssetDetails asset={asset} originalAsset={originalAsset} />
<ProposalAssetDetails asset={asset} />
</div>
)}
@@ -1,7 +1,7 @@
import {
getProposalDialogIcon,
getProposalDialogIntent,
useGetProposalDialogTitle,
getProposalDialogTitle,
} from '@vegaprotocol/proposals';
import type { ProposalEventFieldsFragment } from '@vegaprotocol/proposals';
import type { DialogProps } from '@vegaprotocol/proposals';
@@ -15,7 +15,6 @@ 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 ? (
@@ -25,7 +24,7 @@ export const ProposalFormTransactionDialog = ({
return (
<div data-testid="proposal-transaction-dialog">
<TransactionDialog
title={title}
title={getProposalDialogTitle(finalizedProposal?.state)}
intent={getProposalDialogIntent(finalizedProposal?.state)}
icon={getProposalDialogIcon(finalizedProposal?.state)}
content={{
@@ -8,7 +8,7 @@ import {
networkParamsQueryMock,
nextWeek,
} from '../../test-helpers/mocks';
import { CompactVotes, VoteBreakdown } from './vote-breakdown';
import { VoteBreakdown } from './vote-breakdown';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import {
@@ -346,22 +346,3 @@ 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,21 +3,11 @@ import BigNumber from 'bignumber.js';
import { useTranslation } from 'react-i18next';
import { useVoteInformation } from '../../hooks';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
import { formatNumber, toBigNum } 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'];
@@ -208,7 +198,10 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
)}
>
<button>
<CompactVotes number={yesEquityLikeShareWeight} />
{yesEquityLikeShareWeight
.dividedBy(toBigNum(10 ** 6, 0))
.toFixed(1)}
M
</button>
</Tooltip>
<span>
@@ -233,7 +226,10 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
)}
>
<button>
<CompactVotes number={noEquityLikeShareWeight} />
{noEquityLikeShareWeight
.dividedBy(toBigNum(10 ** 6, 0))
.toFixed(1)}
M
</button>
</Tooltip>
<span>
@@ -283,7 +279,10 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
)}
>
<button>
<CompactVotes number={totalEquityLikeShareWeight} />
{totalEquityLikeShareWeight
.dividedBy(toBigNum(10 ** 6, 0))
.toFixed(1)}
M
</button>
</Tooltip>
<span>
@@ -322,7 +321,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
<span>{t('tokenVotesFor')}:</span>
<Tooltip description={formatNumber(yesTokens, defaultDP)}>
<button data-testid="num-votes-for">
<CompactVotes number={yesTokens} />
{yesTokens.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
</button>
</Tooltip>
<span>
@@ -342,7 +341,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
<span>{t('tokenVotesAgainst')}:</span>
<Tooltip description={formatNumber(noTokens, defaultDP)}>
<button data-testid="num-votes-against">
<CompactVotes number={noTokens} />
{noTokens.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
</button>
</Tooltip>
<span>
@@ -385,7 +384,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
<span>{t('totalTokensVoted')}:</span>
<Tooltip description={formatNumber(totalTokensVoted, defaultDP)}>
<button data-testid="total-voted">
<CompactVotes number={totalTokensVoted} />
{totalTokensVoted.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
</button>
</Tooltip>
<span data-testid="total-voted-percentage">
-4
View File
@@ -104,10 +104,6 @@
list-style: circle;
}
.react-markdown-container a {
text-decoration: underline;
}
.jsondiffpatch-delta,
.jsondiffpatch-delta pre {
font-family: 'Roboto Mono', monospace !important;
@@ -0,0 +1,204 @@
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');
}
@@ -0,0 +1,311 @@
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');
});
});
});
@@ -0,0 +1,47 @@
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`
);
});
});
});
});
@@ -0,0 +1,216 @@
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'
);
});
});
});
});
@@ -0,0 +1,103 @@
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,6 +26,18 @@ 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
@@ -0,0 +1,91 @@
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');
});
});
+4 -8
View File
@@ -3,17 +3,16 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
NX_VEGA_ENV=TESTNET
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
NX_VEGA_TOKEN_URL=https://governance.stagnet1.vega.rocks
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
@@ -26,6 +25,3 @@ NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
-1
View File
@@ -28,4 +28,3 @@ NX_REFERRALS=false
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
NX_DISABLE_CLOSE_POSITION=true
+4 -19
View File
@@ -1,27 +1,12 @@
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 (
<ErrorBoundary feature="fees">
<div className="container p-4 mx-auto">
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
<FeesContainer />
</div>
</ErrorBoundary>
<div className="container p-4 mx-auto">
<h1 className="px-4 pb-4 text-2xl">{t('Fees')}</h1>
<FeesContainer />
</div>
);
};
+25 -2
View File
@@ -1,11 +1,34 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { useNavigateToLastMarket } from '../../lib/hooks/use-navigate-to-last-market';
import { useGlobalStore } from '../../stores';
import { useTopTradedMarkets } from '../../lib/hooks/use-top-traded-markets';
import { Links } from '../../lib/links';
// 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 = () => {
useNavigateToLastMarket();
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]);
return (
<Splash>
@@ -6,7 +6,6 @@ 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',
@@ -59,28 +58,19 @@ export const LiquidityViewContainer = ({
name={t('My liquidity provision')}
hidden={!pubKey}
>
<ErrorBoundary feature="liquidity-party">
<LiquidityContainer
marketId={marketId}
filter={{ partyId: pubKey || undefined }}
/>
</ErrorBoundary>
<LiquidityContainer
marketId={marketId}
filter={{ partyId: pubKey || undefined }}
/>
</Tab>
<Tab id={LiquidityTabs.Active} name={t('Active')}>
<ErrorBoundary feature="liquidity-active">
<LiquidityContainer
marketId={marketId}
filter={{ active: true }}
/>
</ErrorBoundary>
<LiquidityContainer marketId={marketId} filter={{ active: true }} />
</Tab>
<Tab id={LiquidityTabs.Inactive} name={t('Inactive')}>
<ErrorBoundary feature="liquidity-inactive">
<LiquidityContainer
marketId={marketId}
filter={{ active: false }}
/>
</ErrorBoundary>
<LiquidityContainer
marketId={marketId}
filter={{ active: false }}
/>
</Tab>
</Tabs>
</div>
@@ -1,6 +1,7 @@
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,
@@ -144,6 +145,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
/>
</HeaderStat>
)}
<MarketProposalNotification marketId={market.id} />
</>
);
};
+4 -5
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useMemo } from 'react';
import { addDecimalsFormatNumber, titlefy } from '@vegaprotocol/utils';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
import { Link, Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { ExternalLink, Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { getAsset, marketDataProvider, useMarket } from '@vegaprotocol/markets';
import { useGlobalStore, usePageTitleStore } from '../../stores';
import { TradeGrid } from './trade-grid';
@@ -114,16 +114,15 @@ 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={[
<Link
className="underline underline-offset-4 "
<ExternalLink
onClick={() => navigate(Links.MARKETS())}
key="link"
>
market list
</Link>,
</ExternalLink>,
]}
/>
</p>
+16 -47
View File
@@ -20,7 +20,6 @@ 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;
@@ -63,38 +62,28 @@ const MainGrid = memo(
name={t('Chart')}
menu={<TradingViews.candles.menu />}
>
<ErrorBoundary feature="chart">
<TradingViews.candles.component marketId={marketId} />
</ErrorBoundary>
<TradingViews.candles.component marketId={marketId} />
</Tab>
<Tab id="depth" name={t('Depth')}>
<ErrorBoundary feature="depth">
<TradingViews.depth.component marketId={marketId} />
</ErrorBoundary>
<TradingViews.depth.component marketId={marketId} />
</Tab>
<Tab id="liquidity" name={t('Liquidity')}>
<ErrorBoundary feature="liquidity">
<TradingViews.liquidity.component marketId={marketId} />
</ErrorBoundary>
<TradingViews.liquidity.component marketId={marketId} />
</Tab>
{market &&
market.tradableInstrument.instrument.product.__typename ===
'Perpetual' ? (
<Tab id="funding-history" name={t('Funding history')}>
<ErrorBoundary feature="funding-history">
<TradingViews.funding.component marketId={marketId} />
</ErrorBoundary>
<TradingViews.funding.component marketId={marketId} />
</Tab>
) : null}
{market &&
market.tradableInstrument.instrument.product.__typename ===
'Perpetual' ? (
<Tab id="funding-payments" name={t('Funding payments')}>
<ErrorBoundary feature="funding-payments">
<TradingViews.fundingPayments.component
marketId={marketId}
/>
</ErrorBoundary>
<TradingViews.fundingPayments.component
marketId={marketId}
/>
</Tab>
) : null}
</Tabs>
@@ -107,14 +96,10 @@ const MainGrid = memo(
<TradeGridChild>
<Tabs storageKey="console-trade-grid-main-right">
<Tab id="orderbook" name={t('Orderbook')}>
<ErrorBoundary feature="orderbook">
<TradingViews.orderbook.component marketId={marketId} />
</ErrorBoundary>
<TradingViews.orderbook.component marketId={marketId} />
</Tab>
<Tab id="trades" name={t('Trades')}>
<ErrorBoundary feature="trades">
<TradingViews.trades.component marketId={marketId} />
</ErrorBoundary>
<TradingViews.trades.component marketId={marketId} />
</Tab>
</Tabs>
</TradeGridChild>
@@ -133,43 +118,31 @@ const MainGrid = memo(
name={t('Positions')}
menu={<TradingViews.positions.menu />}
>
<ErrorBoundary feature="positions">
<TradingViews.positions.component />
</ErrorBoundary>
<TradingViews.positions.component />
</Tab>
<Tab
id="open-orders"
name={t('Open')}
menu={<TradingViews.activeOrders.menu />}
>
<ErrorBoundary feature="activeOrders">
<TradingViews.activeOrders.component />
</ErrorBoundary>
<TradingViews.activeOrders.component />
</Tab>
<Tab id="closed-orders" name={t('Closed')}>
<ErrorBoundary feature="closedOrders">
<TradingViews.closedOrders.component />
</ErrorBoundary>
<TradingViews.closedOrders.component />
</Tab>
<Tab id="rejected-orders" name={t('Rejected')}>
<ErrorBoundary feature="rejectedOrders">
<TradingViews.rejectedOrders.component />
</ErrorBoundary>
<TradingViews.rejectedOrders.component />
</Tab>
<Tab
id="orders"
name={t('All')}
menu={<TradingViews.orders.menu />}
>
<ErrorBoundary feature="orders">
<TradingViews.orders.component />
</ErrorBoundary>
<TradingViews.orders.component />
</Tab>
{FLAGS.STOP_ORDERS ? (
<Tab id="stop-orders" name={t('Stop orders')}>
<ErrorBoundary feature="stop-orders">
<TradingViews.stopOrders.component />
</ErrorBoundary>
<TradingViews.stopOrders.component />
</Tab>
) : null}
<Tab id="fills" name={t('Fills')}>
@@ -180,11 +153,7 @@ const MainGrid = memo(
name={t('Collateral')}
menu={<TradingViews.collateral.menu />}
>
<ErrorBoundary feature="collateral">
<TradingViews.collateral.component
pinnedAsset={pinnedAsset}
/>
</ErrorBoundary>
<TradingViews.collateral.component pinnedAsset={pinnedAsset} />
</Tab>
</Tabs>
</TradeGridChild>
@@ -1,20 +1,19 @@
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 { ErrorBoundary } from '../../components/error-boundary';
import { type TradingView } from './trade-views';
import { TradingViews } from './trade-views';
import { FLAGS } from '@vegaprotocol/environment';
import { useT } from '../../lib/use-t';
import { Splash } from '@vegaprotocol/ui-toolkit';
interface TradePanelsProps {
market: Market | null;
@@ -35,11 +34,7 @@ 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 (
<ErrorBoundary feature={view}>
<Component marketId={market?.id} pinnedAsset={pinnedAsset} />;
</ErrorBoundary>
);
return <Component marketId={market?.id} pinnedAsset={pinnedAsset} />;
};
const renderMenu = () => {
@@ -1,5 +1,4 @@
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';
@@ -27,7 +26,6 @@ import {
marketsDataQuery,
createMarketsDataFragment,
} from '@vegaprotocol/mock';
import userEvent from '@testing-library/user-event';
describe('Closed', () => {
let originalNow: typeof Date.now;
@@ -170,11 +168,14 @@ describe('Closed', () => {
Date.now = originalNow;
});
const renderComponent = async (mocks: MockedResponse[]) => {
// eslint-disable-next-line jest/no-disabled-tests
it.skip('renders correctly formatted and filtered rows', async () => {
await act(async () => {
render(
<MemoryRouter>
<MockedProvider mocks={mocks}>
<MockedProvider
mocks={[marketsMock, marketsDataMock, oracleDataMock]}
>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
>
@@ -184,10 +185,6 @@ describe('Closed', () => {
</MemoryRouter>
);
});
};
it('renders correct headers', async () => {
await renderComponent([marketsMock, marketsDataMock, oracleDataMock]);
const headers = screen.getAllByRole('columnheader');
const expectedHeaders = [
@@ -203,10 +200,6 @@ 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;
@@ -280,8 +273,21 @@ describe('Closed', () => {
},
},
};
await renderComponent([mixedMarketsMock, marketsDataMock, oracleDataMock]);
await act(async () => {
render(
<MemoryRouter>
<MockedProvider
mocks={[mixedMarketsMock, marketsDataMock, oracleDataMock]}
>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
>
<Closed />
</VegaWalletContext.Provider>
</MockedProvider>
</MemoryRouter>
);
});
// check that the number of rows in datagrid is 2
const container = within(
@@ -313,67 +319,8 @@ describe('Closed', () => {
);
});
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 () => {
// eslint-disable-next-line jest/no-disabled-tests
it.skip('successor marked should be visible', async () => {
const marketsWithSuccessorID = [
{
__typename: 'MarketEdge' as const,
@@ -398,11 +345,21 @@ describe('Closed', () => {
},
};
await renderComponent([
mockWithSuccessors,
marketsDataMock,
oracleDataMock,
]);
await act(async () => {
render(
<MemoryRouter>
<MockedProvider
mocks={[mockWithSuccessors, marketsDataMock, oracleDataMock]}
>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
>
<Closed />
</VegaWalletContext.Provider>
</MockedProvider>
</MemoryRouter>
);
});
const container = within(
document.querySelector('.ag-center-cols-container') as HTMLElement
@@ -1,14 +1,7 @@
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import {
AgGrid,
PriceFlashCell,
useDataGridEvents,
} from '@vegaprotocol/datagrid';
import { AgGrid, PriceFlashCell } 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;
@@ -25,37 +18,8 @@ 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
@@ -64,7 +28,6 @@ export const MarketListTable = (props: Props) => {
columnDefs={columnDefs}
components={components}
rowHeight={45}
{...gridStoreCallbacks}
{...props}
/>
);
@@ -15,7 +15,6 @@ import {
useLinks,
} from '@vegaprotocol/environment';
import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary';
export const MarketsPage = () => {
const t = useT();
@@ -35,9 +34,7 @@ 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')}>
<ErrorBoundary feature="markets-open">
<OpenMarkets />
</ErrorBoundary>
<OpenMarkets />
</Tab>
<Tab
id="proposed-markets"
@@ -52,14 +49,10 @@ export const MarketsPage = () => {
</TradingAnchorButton>
}
>
<ErrorBoundary feature="markets-proposed">
<Proposed />
</ErrorBoundary>
<Proposed />
</Tab>
<Tab id="closed-markets" name={t('Closed markets')}>
<ErrorBoundary feature="markets-closed">
<Closed />
</ErrorBoundary>
<Closed />
</Tab>
</Tabs>
</div>
@@ -1,145 +0,0 @@
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 do not seem to apply for ag-grid
// event.preventDefault or event.stopPropagation dont seem to apply for aggird
const colId = column.getColId();
if (
@@ -51,29 +51,6 @@ 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',
@@ -165,21 +142,27 @@ export const useColumnDefs = () => {
},
},
{
headerName: t('Open Interest'),
field: 'data.openInterest',
type: 'rightAligned',
valueFormatter: ({
headerName: t('Settlement asset'),
field: 'tradableInstrument.instrument.product.settlementAsset.symbol',
cellRenderer: ({
data,
}: VegaValueFormatterParams<
}: VegaICellRendererParams<
MarketMaybeWithData,
'data.openInterest'
>) =>
data?.data?.openInterest === undefined
? '-'
: addDecimalsFormatNumber(
data?.data?.openInterest,
data?.positionDecimalPlaces
),
'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('Spread'),
@@ -25,7 +25,6 @@ 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();
@@ -73,29 +72,19 @@ export const Portfolio = () => {
name={t('Positions')}
menu={<PositionsMenu />}
>
<ErrorBoundary feature="portfolio-positions">
<PositionsContainer allKeys />
</ErrorBoundary>
<PositionsContainer allKeys />
</Tab>
<Tab id="orders" name={t('Orders')}>
<ErrorBoundary feature="portfolio-orders">
<OrdersContainer />
</ErrorBoundary>
<OrdersContainer />
</Tab>
<Tab id="fills" name={t('Fills')}>
<ErrorBoundary feature="portfolio-fills">
<FillsContainer />
</ErrorBoundary>
<FillsContainer />
</Tab>
<Tab id="funding-payments" name={t('Funding payments')}>
<ErrorBoundary feature="portfolio-funding-payments">
<FundingPaymentsContainer />
</ErrorBoundary>
<FundingPaymentsContainer />
</Tab>
<Tab id="ledger-entries" name={t('Ledger entries')}>
<ErrorBoundary feature="portfolio-ledger">
<LedgerContainer />
</ErrorBoundary>
<LedgerContainer />
</Tab>
</Tabs>
</PortfolioGridChild>
@@ -112,14 +101,10 @@ export const Portfolio = () => {
name={t('Collateral')}
menu={<AccountsMenu />}
>
<ErrorBoundary feature="portfolio-accounts">
<AccountsContainer />
</ErrorBoundary>
<AccountsContainer />
</Tab>
<Tab id="deposits" name={t('Deposits')} menu={<DepositsMenu />}>
<ErrorBoundary feature="portfolio-deposit">
<DepositsContainer />
</ErrorBoundary>
<DepositsContainer />
</Tab>
<Tab
id="withdrawals"
@@ -127,9 +112,7 @@ export const Portfolio = () => {
indicator={<WithdrawalsIndicator />}
menu={<WithdrawalsMenu />}
>
<ErrorBoundary feature="portfolio-deposit">
<WithdrawalsContainer />
</ErrorBoundary>
<WithdrawalsContainer />
</Tab>
</Tabs>
</PortfolioGridChild>
@@ -10,7 +10,7 @@ import { useForm } from 'react-hook-form';
import classNames from 'classnames';
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { RainbowButton } from './buttons';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { useReferral } from './hooks/use-referral';
@@ -32,19 +32,6 @@ const validateCode = (value: string, t: ReturnType<typeof useT>) => {
return true;
};
export const ApplyCodeFormContainer = () => {
const { pubKey } = useVegaWallet();
const { data: referee } = useReferral({ pubKey, role: 'referee' });
const { data: referrer } = useReferral({ pubKey, role: 'referrer' });
// go to main page if the current pubkey is already a referrer or referee
if (referee || referrer) {
return <Navigate to={Routes.REFERRALS} />;
}
return <ApplyCodeForm />;
};
export const ApplyCodeForm = () => {
const t = useT();
const program = useReferralProgram();
@@ -68,29 +55,14 @@ export const ApplyCodeForm = () => {
} = useForm();
const [params] = useSearchParams();
const { data: referee } = useReferral({ pubKey, role: 'referee' });
const { data: referrer } = useReferral({ pubKey, role: 'referrer' });
const codeField = watch('code');
const { data: previewData, loading: previewLoading } = useReferral({
code: validateCode(codeField, t) ? codeField : undefined,
});
/**
* Validates the set a user tries to apply to.
*/
const validateSet = useCallback(() => {
if (
codeField &&
!previewLoading &&
previewData &&
!previewData.isEligible
) {
return t('The code is no longer valid.');
}
if (codeField && !previewLoading && !previewData) {
return t('The code is invalid');
}
return true;
}, [codeField, previewData, previewLoading, t]);
useEffect(() => {
const code = params.get('code');
if (code) setValue('code', code);
@@ -172,11 +144,16 @@ export const ApplyCodeForm = () => {
}
}, [navigate, status]);
// go to main page if the current pubkey is already a referrer or referee
if (referee || referrer) {
return <Navigate to={Routes.REFERRALS} />;
}
// show "code applied" message when successfully applied
if (status === 'successful') {
return (
<div className="mx-auto w-1/2">
<h3 className="calt mb-5 flex flex-row items-center justify-center gap-2 text-center text-xl uppercase">
<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">
<span className="text-vega-green-500">
<VegaIcon name={VegaIconNames.TICK} size={20} />
</span>{' '}
@@ -228,18 +205,15 @@ export const ApplyCodeForm = () => {
return (
<>
<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">
<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">
{t('Apply a referral code')}
</h3>
<p className="mb-4 text-center text-base">
{t('Enter a referral code to get trading discounts.')}
</p>
<form
className={classNames('flex w-full flex-col gap-4', {
className={classNames('w-full flex flex-col gap-4', {
'animate-shake': Boolean(errors.code),
})}
onSubmit={handleSubmit(onSubmit)}
@@ -250,37 +224,31 @@ export const ApplyCodeForm = () => {
hasError={Boolean(errors.code)}
{...register('code', {
required: t('You have to provide a code to apply it.'),
validate: (value) => {
const err = validateCode(value, t);
if (err !== true) return err;
return validateSet();
},
validate: (value) => validateCode(value, t),
})}
placeholder="Enter a code"
className="bg-vega-clight-900 dark:bg-vega-cdark-700 mb-2"
className="mb-2 bg-vega-clight-900 dark:bg-vega-cdark-700"
/>
</label>
<RainbowButton variant="border" {...getButtonProps()} />
</form>
{errors.code && (
<InputError className="overflow-auto break-words">
<InputError className="break-words overflow-auto">
{errors.code.message?.toString()}
</InputError>
)}
</div>
{validateCode(codeField, t) === true && previewLoading && !previewData ? (
{previewLoading && !previewData ? (
<div className="mt-10">
<Loader />
</div>
) : null}
{/* TODO: Re-check plural forms once i18n is updated */}
{previewData && previewData.isEligible ? (
{previewData ? (
<div className="mt-10">
<h2 className="mb-5 text-2xl">
<h2 className="text-2xl mb-5">
{t(
'youAreJoiningTheGroup',
'You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.',
{ count: nextBenefitTierEpochsValue }
'You are joining the group shown, but will not have access to benefits until you have completed at least %s epochs.',
[nextBenefitTierEpochsValue.toString()]
)}
</h2>
<Statistics data={previewData} program={program} as="referee" />
@@ -39,10 +39,7 @@ export const CreateCodeForm = () => {
const { pubKey, isReadOnly } = useVegaWallet();
return (
<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"
>
<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">
{t('Create a referral code')}
</h3>
@@ -98,11 +95,6 @@ const CreateCodeDialog = ({
const { stakeAvailable: currentStakeAvailable, requiredStake } =
useStakeAvailable();
const { data: referralSets } = useReferral({
pubKey,
role: 'referrer',
});
const onSubmit = () => {
if (isReadOnly || !pubKey) {
setErr('Not connected');
@@ -201,68 +193,6 @@ const CreateCodeDialog = ({
);
}
if (!referralSets) {
return (
<div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && (
<>
{
<p>
{t(
'There is currently no referral program active, are you sure you want to create a code?'
)}
</p>
}
</>
)}
{status === 'success' && code && (
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700">
<p className="overflow-hidden whitespace-nowrap text-ellipsis">
{code}
</p>
</div>
<CopyWithTooltip text={code}>
<TradingButton
className="text-sm no-underline"
icon={<VegaIcon name={VegaIconNames.COPY} />}
>
<span>{t('Copy')}</span>
</TradingButton>
</CopyWithTooltip>
</div>
)}
<TradingButton
fill={true}
intent={Intent.Primary}
onClick={() => onSubmit()}
{...getButtonProps()}
></TradingButton>
{status === 'idle' && (
<TradingButton
fill={true}
intent={Intent.Primary}
onClick={() => {
refetch();
setDialogOpen(false);
}}
>
{t('No')}
</TradingButton>
)}
{err && <InputError>{err}</InputError>}
<div className="flex justify-center pt-5 mt-2 text-sm border-t gap-4 text-default border-default">
<ExternalLink href={ABOUT_REFERRAL_DOCS_LINK}>
{t('About the referral program')}
</ExternalLink>
<ExternalLink href={DISCLAIMER_REFERRAL_DOCS_LINK}>
{t('Disclaimer')}
</ExternalLink>
</div>
</div>
);
}
return (
<div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && (
@@ -11,7 +11,6 @@ query ReferralSetStats($code: ID!, $epoch: Int) {
rewardsMultiplier
rewardsFactorMultiplier
referrerTakerVolume
wasEligible
}
}
}
@@ -1,10 +0,0 @@
query StakeAvailable($partyId: ID!) {
party(id: $partyId) {
stakingSummary {
currentStakeAvailable
}
}
networkParameter(key: "referralProgram.minStakedVegaTokens") {
value
}
}
@@ -9,7 +9,7 @@ export type ReferralSetStatsQueryVariables = Types.Exact<{
}>;
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string, referrerTakerVolume: string, wasEligible: boolean } } | 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 } } | null> } };
export const ReferralSetStatsDocument = gql`
@@ -26,7 +26,6 @@ export const ReferralSetStatsDocument = gql`
rewardsMultiplier
rewardsFactorMultiplier
referrerTakerVolume
wasEligible
}
}
}
@@ -1,53 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type StakeAvailableQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type StakeAvailableQuery = { __typename?: 'Query', party?: { __typename?: 'Party', stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string } } | null, networkParameter?: { __typename?: 'NetworkParameter', value: string } | null };
export const StakeAvailableDocument = gql`
query StakeAvailable($partyId: ID!) {
party(id: $partyId) {
stakingSummary {
currentStakeAvailable
}
}
networkParameter(key: "referralProgram.minStakedVegaTokens") {
value
}
}
`;
/**
* __useStakeAvailableQuery__
*
* To run a query within a React component, call `useStakeAvailableQuery` and pass it any options that fit your needs.
* When your component renders, `useStakeAvailableQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useStakeAvailableQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function useStakeAvailableQuery(baseOptions: Apollo.QueryHookOptions<StakeAvailableQuery, StakeAvailableQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<StakeAvailableQuery, StakeAvailableQueryVariables>(StakeAvailableDocument, options);
}
export function useStakeAvailableLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<StakeAvailableQuery, StakeAvailableQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<StakeAvailableQuery, StakeAvailableQueryVariables>(StakeAvailableDocument, options);
}
export type StakeAvailableQueryHookResult = ReturnType<typeof useStakeAvailableQuery>;
export type StakeAvailableLazyQueryHookResult = ReturnType<typeof useStakeAvailableLazyQuery>;
export type StakeAvailableQueryResult = Apollo.QueryResult<StakeAvailableQuery, StakeAvailableQueryVariables>;
@@ -75,20 +75,22 @@ export const useReferralProgram = () => {
const benefitTiers = sortBy(data.currentReferralProgram.benefitTiers, (t) =>
Number(t.referralRewardFactor)
).map((t, i) => {
return {
tier: i + 1, // sorted in asc order, hence first is the lowest tier
rewardFactor: Number(t.referralRewardFactor),
commission: Number(t.referralRewardFactor) * 100 + '%',
discountFactor: Number(t.referralDiscountFactor),
discount: Number(t.referralDiscountFactor) * 100 + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: getNumberFormat(0).format(
Number(t.minimumRunningNotionalTakerVolume)
),
epochs: Number(t.minimumEpochs),
};
});
)
.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),
};
});
const stakingTiers = sortBy(
data.currentReferralProgram.stakingTiers,
@@ -1,111 +0,0 @@
import {
Intent,
type Toast,
useToasts,
ToastHeading,
Button,
} from '@vegaprotocol/ui-toolkit';
import { useReferral } from './use-referral';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useEffect } from 'react';
import { useT } from '../../../lib/use-t';
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
import { Routes } from '../../../lib/links';
import { useCurrentEpochInfoQuery } from './__generated__/Epoch';
const REFETCH_INTERVAL = 60 * 60 * 1000; // 1h
const NON_ELIGIBLE_REFERRAL_SET_TOAST_ID = 'non-eligible-referral-set';
const useNonEligibleReferralSet = () => {
const { pubKey } = useVegaWallet();
const { data, loading, refetch } = useReferral({ pubKey, role: 'referee' });
const {
data: epochData,
loading: epochLoading,
refetch: epochRefetch,
} = useCurrentEpochInfoQuery();
useEffect(() => {
const interval = setInterval(() => {
refetch();
epochRefetch();
}, REFETCH_INTERVAL);
return () => {
clearInterval(interval);
};
}, [epochRefetch, refetch]);
return { data, epoch: epochData?.epoch.id, loading: loading || epochLoading };
};
export const useReferralToasts = () => {
const navigate = useNavigate();
const { pathname } = useLocation();
const t = useT();
const [setToast, hasToast, updateToast] = useToasts((store) => [
store.setToast,
store.hasToast,
store.update,
]);
const { data, epoch, loading } = useNonEligibleReferralSet();
useEffect(() => {
if (
data &&
epoch &&
!loading &&
!data.isEligible &&
!hasToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch)
) {
const nonEligibleReferralToast: Toast = {
id: NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch,
intent: Intent.Warning,
content: (
<>
<ToastHeading>{t('Referral code no longer valid')}</ToastHeading>
<p>
{t(
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements.'
)}
</p>
<p className="mt-2">
<Button
data-testid="toast-apply-code"
size="xs"
onClick={() => {
const matched = matchPath(
Routes.REFERRALS_APPLY_CODE,
pathname
);
if (!matched) navigate(Routes.REFERRALS_APPLY_CODE);
updateToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch, {
hidden: true,
});
}}
>
{t('Apply a new code')}
</Button>
</p>
</>
),
onClose: () =>
updateToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch, {
hidden: true,
}),
};
setToast(nonEligibleReferralToast);
}
}, [
data,
epoch,
hasToast,
loading,
navigate,
pathname,
setToast,
t,
updateToast,
]);
};
@@ -4,7 +4,6 @@ import { useRefereesQuery } from './__generated__/Referees';
import compact from 'lodash/compact';
import type { ReferralSetsQueryVariables } from './__generated__/ReferralSets';
import { useReferralSetsQuery } from './__generated__/ReferralSets';
import { useStakeAvailable } from './use-stake-available';
export const DEFAULT_AGGREGATION_DAYS = 30;
@@ -63,8 +62,6 @@ export const useReferral = (args: UseReferralArgs) => {
? referralData.referralSets.edges[0]?.node
: undefined;
const { isEligible } = useStakeAvailable(referralSet?.referrer);
const {
data: refereesData,
loading: refereesLoading,
@@ -106,7 +103,6 @@ export const useReferral = (args: UseReferralArgs) => {
referee: referee,
referrerId: referralSet.referrer,
createdAt: referralSet.createdAt,
isEligible,
referees,
}
: undefined;
@@ -1,35 +1,34 @@
import { gql, useQuery } from '@apollo/client';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useStakeAvailableQuery } from './__generated__/StakeAvailable';
/**
* 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,
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
errorPolicy: 'ignore',
});
const stakeAvailable = data
? BigInt(data.party?.stakingSummary.currentStakeAvailable || '0')
: undefined;
const requiredStake = data
? BigInt(data.networkParameter?.value || '0')
: undefined;
return {
stakeAvailable,
requiredStake,
isEligible:
stakeAvailable != null &&
requiredStake != null &&
stakeAvailable >= requiredStake,
stakeAvailable: data
? BigInt(data.party?.stakingSummary.currentStakeAvailable || '0')
: undefined,
requiredStake: data
? BigInt(data.networkParameter?.value || '0')
: undefined,
};
};
@@ -1,374 +0,0 @@
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
import { render, waitFor } from '@testing-library/react';
import { type VegaWalletContextShape } from '@vegaprotocol/wallet';
import { ReferralStatistics } from './referral-statistics';
import {
ReferralProgramDocument,
type ReferralProgramQuery,
} from './hooks/__generated__/CurrentReferralProgram';
import {
ReferralSetsDocument,
type ReferralSetsQueryVariables,
type ReferralSetsQuery,
} from './hooks/__generated__/ReferralSets';
import {
StakeAvailableDocument,
type StakeAvailableQueryVariables,
type StakeAvailableQuery,
} from './hooks/__generated__/StakeAvailable';
import {
RefereesDocument,
type RefereesQueryVariables,
type RefereesQuery,
} from './hooks/__generated__/Referees';
import { MemoryRouter } from 'react-router-dom';
const MOCK_PUBKEY =
'1234567890123456789012345678901234567890123456789012345678901234';
const MOCK_STAKE_AVAILABLE: StakeAvailableQuery = {
networkParameter: {
__typename: 'NetworkParameter',
value: '1',
},
party: {
__typename: 'Party',
stakingSummary: {
__typename: 'StakingSummary',
currentStakeAvailable: '1',
},
},
};
const MOCK_NON_ELIGIBILE_STAKE_AVAILABLE: StakeAvailableQuery = {
networkParameter: {
__typename: 'NetworkParameter',
value: '1',
},
party: {
__typename: 'Party',
stakingSummary: {
__typename: 'StakingSummary',
currentStakeAvailable: '0',
},
},
};
const MOCK_REFERRAL_PROGRAM: ReferralProgramQuery = {
currentReferralProgram: {
__typename: 'CurrentReferralProgram',
benefitTiers: [
{
__typename: 'BenefitTier',
minimumEpochs: 1,
minimumRunningNotionalTakerVolume: '0',
referralDiscountFactor: '0.01',
referralRewardFactor: '0.01',
},
{
__typename: 'BenefitTier',
minimumEpochs: 2,
minimumRunningNotionalTakerVolume: '10',
referralDiscountFactor: '0.02',
referralRewardFactor: '0.02',
},
],
endOfProgramTimestamp: '202411012023-11-26T05:58:24.045158Z',
id: '123',
stakingTiers: [
{
__typename: 'StakingTier',
minimumStakedTokens: '100',
referralRewardMultiplier: '1',
},
{
__typename: 'StakingTier',
minimumStakedTokens: '1000',
referralRewardMultiplier: '2',
},
],
version: 2,
windowLength: 3,
endedAt: null,
},
};
const MOCK_REFERRER_SET: ReferralSetsQuery = {
referralSets: {
__typename: 'ReferralSetConnection',
edges: [
{
__typename: 'ReferralSetEdge',
node: {
__typename: 'ReferralSet',
createdAt: '2023-11-26T05:58:24.045158Z',
id: '3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
referrer: MOCK_PUBKEY,
updatedAt: '2023-11-26T05:58:24.045158Z',
},
},
],
},
};
const MOCK_REFERREE_SET: ReferralSetsQuery = {
referralSets: {
__typename: 'ReferralSetConnection',
edges: [
{
__typename: 'ReferralSetEdge',
node: {
__typename: 'ReferralSet',
createdAt: '2023-11-26T05:58:24.045158Z',
id: '3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
referrer:
'1111111111111111111111111111111111111111111111111111111111111111',
updatedAt: '2023-11-26T05:58:24.045158Z',
},
},
],
},
};
const MOCK_REFEREES: RefereesQuery = {
referralSetReferees: {
__typename: 'ReferralSetRefereeConnection',
edges: [
{
node: {
atEpoch: 1,
joinedAt: '2023-11-21T14:17:09.257235Z',
refereeId:
'0987654321098765432109876543210987654321098765432109876543219876',
referralSetId:
'3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
totalRefereeGeneratedRewards: '1234',
totalRefereeNotionalTakerVolume: '5678',
__typename: 'ReferralSetReferee',
},
},
],
},
};
const programMock: MockedResponse<ReferralProgramQuery> = {
request: {
query: ReferralProgramDocument,
},
result: { data: MOCK_REFERRAL_PROGRAM },
};
const referralSetAsReferrerMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referrer: MOCK_PUBKEY,
},
},
result: {
data: MOCK_REFERRER_SET,
},
};
const noReferralSetAsReferrerMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referrer: MOCK_PUBKEY,
},
},
result: {
data: { referralSets: { edges: [] } },
},
};
const referralSetAsRefereeMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referee: MOCK_PUBKEY,
},
},
result: {
data: MOCK_REFERREE_SET,
},
};
const noReferralSetAsRefereeMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referee: MOCK_PUBKEY,
},
},
result: {
data: { referralSets: { edges: [] } },
},
};
const stakeAvailableMock: MockedResponse<
StakeAvailableQuery,
StakeAvailableQueryVariables
> = {
request: {
query: StakeAvailableDocument,
variables: {
partyId: MOCK_PUBKEY,
},
},
result: {
data: MOCK_STAKE_AVAILABLE,
},
};
const nonEligibleStakeAvailableMock: MockedResponse<
StakeAvailableQuery,
StakeAvailableQueryVariables
> = {
request: {
query: StakeAvailableDocument,
variables: {
partyId: MOCK_PUBKEY,
},
},
result: {
data: MOCK_NON_ELIGIBILE_STAKE_AVAILABLE,
},
};
const refereesMock: MockedResponse<RefereesQuery, RefereesQueryVariables> = {
request: {
query: RefereesDocument,
variables: {
code: MOCK_REFERRER_SET.referralSets.edges[0]?.node.id as string,
aggregationEpochs:
MOCK_REFERRAL_PROGRAM.currentReferralProgram?.windowLength,
},
},
result: {
data: MOCK_REFEREES,
},
};
jest.mock('@vegaprotocol/wallet', () => {
return {
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: () => {
const ctx: Partial<VegaWalletContextShape> = {
pubKey: MOCK_PUBKEY,
};
return ctx;
},
};
});
describe('ReferralStatistics', () => {
it('displays create code when no data has been found for given pubkey', () => {
const { queryByTestId } = render(
<MockedProvider mocks={[]} showWarnings={false}>
<ReferralStatistics />
</MockedProvider>
);
expect(queryByTestId('referral-create-code-form')).toBeInTheDocument();
});
it('displays referrer stats when given pubkey is a referrer', async () => {
const { queryByTestId } = render(
<MockedProvider
mocks={[
programMock,
referralSetAsReferrerMock,
noReferralSetAsRefereeMock,
stakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
);
await waitFor(() => {
expect(
queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referrer'
);
});
});
it('displays referee stats when given pubkey is a referee', async () => {
const { queryByTestId } = render(
<MemoryRouter>
<MockedProvider
mocks={[
programMock,
noReferralSetAsReferrerMock,
referralSetAsRefereeMock,
stakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
await waitFor(() => {
expect(
queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee'
);
});
});
it('displays eligibility warning when the set is no longer valid due to the referrers stake', async () => {
const { queryByTestId } = render(
<MemoryRouter>
<MockedProvider
mocks={[
programMock,
noReferralSetAsReferrerMock,
referralSetAsRefereeMock,
nonEligibleStakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
await waitFor(() => {
expect(
queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee'
);
expect(queryByTestId('referral-eligibility-warning')).toBeInTheDocument();
expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument();
});
});
});
@@ -1,4 +1,3 @@
import minBy from 'lodash/minBy';
import { CodeTile, StatTile } from './tile';
import {
VegaIcon,
@@ -29,10 +28,10 @@ import sortBy from 'lodash/sortBy';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
import BigNumber from 'bignumber.js';
import maxBy from 'lodash/maxBy';
import { DocsLinks } from '@vegaprotocol/environment';
import { useT, ns } from '../../lib/use-t';
import { Trans } from 'react-i18next';
import { ApplyCodeForm } from './apply-code-form';
export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet();
@@ -51,21 +50,11 @@ export const ReferralStatistics = () => {
});
if (referee?.code) {
return (
<>
<Statistics data={referee} program={program} as="referee" />;
{!referee.isEligible && <ApplyCodeForm />}
</>
);
return <Statistics data={referee} program={program} as="referee" />;
}
if (referrer?.code) {
return (
<>
<Statistics data={referrer} program={program} as="referrer" />;
<RefereesTable data={referrer} program={program} />
</>
);
return <Statistics data={referrer} program={program} as="referrer" />;
}
return <CreateCodeContainer />;
@@ -127,8 +116,8 @@ export const useStats = ({
t.discountFactor === discountFactorValue
);
const nextBenefitTierValue = currentBenefitTierValue
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier + 1)
: minBy(benefitTiers, (bt) => bt.tier); // min tier number is lowest tier
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1)
: maxBy(benefitTiers, (bt) => bt.tier); // max tier number is lowest tier
const epochsValue =
!isNaN(currentEpoch) && refereeInfo?.atEpoch
? currentEpoch - refereeInfo?.atEpoch
@@ -185,7 +174,7 @@ export const Statistics = ({
const { benefitTiers } = useReferralProgram();
const { stakeAvailable, isEligible } = useStakeAvailable();
const { stakeAvailable } = useStakeAvailable();
const { details } = program;
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
@@ -211,24 +200,12 @@ export const Statistics = ({
{baseCommissionValue * 100}%
</StatTile>
);
const stakingMultiplierTile = (
<StatTile
title={t('Staking multiplier')}
description={
<span
className={classNames({
'text-vega-red': !isEligible,
})}
>
{t('{{amount}} $VEGA staked', {
amount: addDecimalsFormatNumber(
stakeAvailable?.toString() || 0,
18
),
})}
</span>
}
description={t('{{amount}} $VEGA staked', {
amount: addDecimalsFormatNumber(stakeAvailable?.toString() || 0, 18),
})}
>
{multiplier || t('None')}
</StatTile>
@@ -261,7 +238,7 @@ export const Statistics = ({
const referrerVolumeTile = (
<StatTile
title={t('myVolume', 'My volume (last {{count}} epochs)', {
title={t('My volume (last {{count}} epochs)', {
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
})}
>
@@ -274,7 +251,7 @@ export const Statistics = ({
.reduce((all, r) => all.plus(r), new BigNumber(0));
const totalCommissionTile = (
<StatTile
title={t('totalCommission', 'Total commission (last {{count}}} epochs)', {
title={t('Total commission (last {{count}}} epochs)', {
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
})}
description={<QUSDTooltip />}
@@ -317,13 +294,9 @@ export const Statistics = ({
);
const runningVolumeTile = (
<StatTile
title={t(
'runningNotionalOverEpochs',
'Combined volume (last {{count}} epochs)',
{
count: details?.windowLength,
}
)}
title={t('Combined volume (last {{count}} epochs)', {
count: details?.windowLength,
})}
>
{compactNumFormat.format(runningVolumeValue)}
</StatTile>
@@ -360,60 +333,28 @@ export const Statistics = ({
</>
);
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 (
<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', {
'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 (
<>
{/* Stats tiles */}
<div
className={classNames(
'grid grid-cols-1 grid-rows-1 gap-5 mx-auto mb-20'
)}
>
{as === 'referrer' && referrerTiles}
{as === 'referee' && refereeTiles}
</div>
{/* Referees (only for referrer view) */}
{data.referees.length > 0 && (
{as === 'referrer' && data.referees.length > 0 && (
<div className="mt-20 mb-20">
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
<div
@@ -443,19 +384,15 @@ export const RefereesTable = ({
{ name: 'joined', displayName: t('Date Joined') },
{
name: 'volume',
displayName: t(
'volumeLastEpochs',
'Volume (last {{count}} epochs)',
{
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
}
),
displayName: t('Volume (last {{count}} epochs)', {
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
}),
},
{
name: 'commission',
displayName: (
<Trans
i18nKey="referralStatisticsCommission"
i18nKey="referral-statistics-commission"
defaults="Commission earned in <0>qUSD</0> (last {{count}} epochs)"
values={{
count:
@@ -18,7 +18,6 @@ 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();
@@ -66,7 +65,7 @@ export const Referrals = () => {
}, [updateTitle, t]);
return (
<ErrorBoundary feature="referrals">
<>
<LandingBanner />
{showNav && <Nav />}
@@ -108,6 +107,6 @@ export const Referrals = () => {
</TradingAnchorButton>
</div>
</div>
</ErrorBoundary>
</>
);
};
+9 -27
View File
@@ -6,12 +6,7 @@ import { BORDER_COLOR, GRADIENT } from './constants';
import { Tag } from './tag';
import type { ComponentProps, ReactNode } from 'react';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import {
DApp,
DocsLinks,
TOKEN_PROPOSALS,
useLinks,
} from '@vegaprotocol/environment';
import { DApp, TOKEN_PROPOSALS, useLinks } from '@vegaprotocol/environment';
import { useT, ns } from '../../lib/use-t';
import { Trans } from 'react-i18next';
@@ -93,19 +88,10 @@ export const TiersContainer = () => {
return (
<div className="text-base px-5 py-10 text-center">
<Trans
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."
defaults="We're sorry but we don't have an active referral programme currently running. You can propose a new programme <0>here</0>."
components={[
<ExternalLink href={governanceLink(TOKEN_PROPOSALS)} key="link">
{t('Governance App')}
</ExternalLink>,
]}
ns={ns}
/>
<Trans
defaults="You can propose a new program via the <0>Docs</0>."
components={[
<ExternalLink href={DocsLinks?.REFERRALS} key="link">
{t('Docs')}
{t('here')}
</ExternalLink>,
]}
ns={ns}
@@ -208,13 +194,9 @@ const TiersTable = ({
{ name: 'discount', displayName: t('Referrer trading discount') },
{
name: 'volume',
displayName: t(
'minTradingVolume',
'Min. trading volume (last {{count}} epochs)',
{
count: windowLength,
}
),
displayName: t('Min. trading volume (last {{count}} epochs)', {
count: windowLength,
}),
},
{ name: 'epochs', displayName: t('Min. epochs') },
]}
@@ -222,13 +204,13 @@ const TiersTable = ({
...d,
className: classNames({
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
d.tier >= 3,
d.tier === 1,
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
d.tier === 2,
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
d.tier === 1,
d.tier === 3,
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
d.tier == 0,
d.tier > 3,
}),
}))}
/>
+2 -26
View File
@@ -8,9 +8,6 @@ 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,
@@ -66,10 +63,6 @@ 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')}
@@ -96,27 +89,10 @@ export const CodeTile = ({
{code}
</div>
</Tooltip>
<CopyWithTooltip text={code} description={t('Copy referral code')}>
<CopyWithTooltip text={code}>
<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.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} />
<VegaIcon size={24} name={VegaIconNames.COPY} />
</Button>
</CopyWithTooltip>
</div>
+5 -19
View File
@@ -1,25 +1,11 @@
import { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import { useT } from '../../lib/use-t';
import { t } from '@vegaprotocol/i18n';
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 (
<ErrorBoundary feature="rewards">
<div className="container mx-auto p-4">
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
<RewardsContainer />
</div>
</ErrorBoundary>
<div className="container mx-auto p-4">
<h1 className="px-4 pb-4 text-2xl">{t('Rewards')}</h1>
<RewardsContainer />
</div>
);
};
@@ -1,79 +0,0 @@
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();
});
});
@@ -1,53 +0,0 @@
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>;
};
@@ -1 +0,0 @@
export { ErrorBoundary } from './error-boundary';
@@ -16,11 +16,18 @@ query DiscountPrograms {
}
}
query Fees($partyId: ID!) {
query Fees(
$partyId: ID!
$volumeDiscountEpochs: Int!
$referralDiscountEpochs: Int!
) {
epoch {
id
}
volumeDiscountStats(partyId: $partyId, pagination: { last: 1 }) {
volumeDiscountStats(
partyId: $partyId
pagination: { last: $volumeDiscountEpochs }
) {
edges {
node {
atEpoch
@@ -52,7 +59,10 @@ query Fees($partyId: ID!) {
}
}
}
referralSetStats(partyId: $partyId, pagination: { last: 1 }) {
referralSetStats(
partyId: $partyId
pagination: { last: $referralDiscountEpochs }
) {
edges {
node {
atEpoch
+10 -3
View File
@@ -10,6 +10,8 @@ export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgr
export type FeesQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
volumeDiscountEpochs: Types.Scalars['Int'];
referralDiscountEpochs: Types.Scalars['Int'];
}>;
@@ -63,11 +65,14 @@ 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!) {
query Fees($partyId: ID!, $volumeDiscountEpochs: Int!, $referralDiscountEpochs: Int!) {
epoch {
id
}
volumeDiscountStats(partyId: $partyId, pagination: {last: 1}) {
volumeDiscountStats(
partyId: $partyId
pagination: {last: $volumeDiscountEpochs}
) {
edges {
node {
atEpoch
@@ -99,7 +104,7 @@ export const FeesDocument = gql`
}
}
}
referralSetStats(partyId: $partyId, pagination: {last: 1}) {
referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) {
edges {
node {
atEpoch
@@ -124,6 +129,8 @@ 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({ errorPolicy: 'ignore' });
useDiscountProgramsQuery();
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,
skip: !pubKey || !programData,
});
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
useVolumeStats(
previousEpoch,
feesData?.volumeDiscountStats.edges?.[0]?.node,
feesData?.volumeDiscountStats,
programData?.currentVolumeDiscountProgram
);
@@ -67,12 +67,12 @@ export const FeesContainer = () => {
code,
isReferrer,
} = useReferralStats(
previousEpoch,
feesData?.referralSetStats.edges?.[0]?.node,
feesData?.referralSetReferees.edges?.[0]?.node,
feesData?.referralSetStats,
feesData?.referralSetReferees,
programData?.currentReferralProgram,
feesData?.referrer.edges?.[0]?.node,
feesData?.referee.edges?.[0]?.node
feesData?.epoch,
feesData?.referrer,
feesData?.referee
);
const loading = paramsLoading || feesLoading || programLoading;
@@ -310,25 +310,16 @@ export const CurrentVolume = ({
const t = useT();
const nextTier = tiers[tierIndex + 1];
const requiredForNextTier = nextTier
? new BigNumber(nextTier.minimumRunningNotionalTakerVolume).minus(
windowLengthVolume
)
: new BigNumber(0);
const currentVolume = new BigNumber(windowLengthVolume);
? Number(nextTier.minimumRunningNotionalTakerVolume) - windowLengthVolume
: 0;
return (
<div className="flex flex-col gap-3 pt-4">
<CardStat
value={
currentVolume.isZero()
? `<${formatNumberRounded(requiredForNextTier)}`
: formatNumberRounded(currentVolume)
}
text={t('pastEpochs', 'Past {{count}} epochs', {
count: windowLength,
})}
value={formatNumberRounded(new BigNumber(windowLengthVolume))}
text={t('Past {{count}} epochs', { count: windowLength })}
/>
{requiredForNextTier.isGreaterThan(0) && (
{requiredForNextTier > 0 && (
<CardStat
value={formatNumber(requiredForNextTier)}
text={t('Required for next tier')}
@@ -353,13 +344,9 @@ const ReferralBenefits = ({
<CardStat
// all sets volume (not just current party)
value={formatNumber(setRunningNotionalTakerVolume)}
text={t(
'runningNotionalOverEpochs',
'Combined running notional over the {{count}} epochs',
{
count: epochs,
}
)}
text={t('Combined running notional over the {{count}} epochs', {
count: epochs,
})}
/>
<CardStat value={epochsInSet} text={t('epochs in referral set')} />
</div>
@@ -466,27 +453,31 @@ const VolumeTiers = ({
<Th>{t('Discount')}</Th>
<Th>{t('Min. trading volume')}</Th>
<Th>
{t('myVolume', 'My volume (last {{count}} epochs)', {
count: windowLength,
})}
{t('My volume (last {{count}} epochs)', { count: windowLength })}
</Th>
<Th />
</tr>
</THead>
<tbody>
{Array.from(tiers).map((tier, i) => {
const isUserTier = tierIndex === i;
{Array.from(tiers)
.reverse()
.map((tier, i) => {
const isUserTier = tiers.length - 1 - 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>
@@ -529,33 +520,37 @@ const ReferralTiers = ({
</tr>
</THead>
<tbody>
{Array.from(tiers).map((t, i) => {
const isUserTier = tierIndex === i;
{Array.from(tiers)
.reverse()
.map((t, i) => {
const isUserTier = tiers.length - 1 - tierIndex === i;
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>
const requiredVolume = Number(
t.minimumRunningNotionalTakerVolume
);
}
let unlocksIn = null;
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>
);
})}
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>
);
})}
</tbody>
</Table>
</div>
@@ -2,15 +2,46 @@ import { renderHook } from '@testing-library/react';
import { useReferralStats } from './use-referral-stats';
describe('useReferralStats', () => {
const stat = {
__typename: 'ReferralSetStats' as const,
atEpoch: 9,
discountFactor: '0.01',
referralSetRunningNotionalTakerVolume: '100',
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 set = {
atEpoch: 4,
const sets = {
edges: [
{
node: {
atEpoch: 3,
},
},
{
node: {
atEpoch: 4,
},
},
],
};
const epoch = {
id: '10',
};
const program = {
@@ -47,36 +78,102 @@ describe('useReferralStats', () => {
});
});
it('returns default values if set is not from previous epoch', () => {
const { result } = renderHook(() =>
useReferralStats(10, stat, set, program)
);
expect(result.current).toEqual({
referralDiscount: 0,
referralVolumeInWindow: 0,
referralTierIndex: -1,
referralTiers: program.benefitTiers,
epochsInSet: 0,
code: undefined,
isReferrer: false,
});
});
it('returns formatted data and tiers', () => {
const { result } = renderHook(() =>
useReferralStats(9, stat, set, program)
useReferralStats(setStats, sets, program, epoch)
);
// should use stats from latest epoch
const stats = setStats.edges[1].node;
const set = sets.edges[1].node;
expect(result.current).toEqual({
referralDiscount: Number(stat.discountFactor),
referralDiscount: Number(stats.discountFactor),
referralVolumeInWindow: Number(
stat.referralSetRunningNotionalTakerVolume
stats.referralSetRunningNotionalTakerVolume
),
referralTierIndex: 0,
referralTierIndex: 1,
referralTiers: program.benefitTiers,
epochsInSet: stat.atEpoch - set.atEpoch,
epochsInSet: Number(epoch.id) - set.atEpoch,
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,
},
},
],
};
const { result } = renderHook(() =>
useReferralStats(statsA, setsA, program, epoch)
);
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);
});
});
@@ -1,24 +1,20 @@
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 = (
previousEpoch?: number,
referralStats?: NonNullable<
FeesQuery['referralSetStats']['edges']['0']
>['node'],
setReferees?: NonNullable<
FeesQuery['referralSetReferees']['edges']['0']
>['node'],
setStats?: FeesQuery['referralSetStats'],
setReferees?: FeesQuery['referralSetReferees'],
program?: DiscountProgramsQuery['currentReferralProgram'],
setIfReferrer?: NonNullable<FeesQuery['referrer']['edges']['0']>['node'],
setIfReferee?: NonNullable<FeesQuery['referee']['edges']['0']>['node']
epoch?: FeesQuery['epoch'],
setIfReferrer?: FeesQuery['referrer'],
setIfReferee?: FeesQuery['referee']
) => {
const referralTiers = program?.benefitTiers || [];
if (
!previousEpoch ||
referralStats?.atEpoch !== previousEpoch ||
!program ||
!setReferees
) {
if (!setStats || !setReferees || !program || !epoch) {
return {
referralDiscount: 0,
referralVolumeInWindow: 0,
@@ -30,22 +26,41 @@ 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 = referralTiers.findIndex(
(tier) => tier.referralDiscountFactor === referralStats?.discountFactor
);
const referralTierIndex = referralStats
? getReferralBenefitTier(
epochsInSet,
Number(referralStats.referralSetRunningNotionalTakerVolume),
referralTiers
)
: -1;
return {
referralDiscount,
referralVolumeInWindow,
referralTierIndex,
referralTiers,
epochsInSet: referralStats.atEpoch - setReferees.atEpoch,
code: (setIfReferrer || setIfReferee)?.id,
isReferrer: Boolean(setIfReferrer),
epochsInSet,
code: (setIfReferrerData || setIfRefereeData)?.id,
isReferrer: Boolean(setIfReferrerData),
};
};
@@ -2,11 +2,27 @@ import { renderHook } from '@testing-library/react';
import { useVolumeStats } from './use-volume-stats';
describe('useReferralStats', () => {
const stats = {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 10,
discountFactor: '0.05',
runningVolume: '200',
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 program = {
@@ -28,7 +44,7 @@ describe('useReferralStats', () => {
};
it('returns correct default values', () => {
const { result } = renderHook(() => useVolumeStats(10));
const { result } = renderHook(() => useVolumeStats());
expect(result.current).toEqual({
volumeDiscount: 0,
volumeInWindow: 0,
@@ -37,18 +53,11 @@ describe('useReferralStats', () => {
});
});
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,
});
});
it('returns formatted data and tiers', () => {
const { result } = renderHook(() => useVolumeStats(10, stats, program));
const { result } = renderHook(() => useVolumeStats(statsList, program));
// should use stats from latest epoch
const stats = statsList.edges[1].node;
expect(result.current).toEqual({
volumeDiscount: Number(stats.discountFactor),
@@ -57,4 +66,30 @@ 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 = (
previousEpoch: number,
lastEpochStats?: NonNullable<
FeesQuery['volumeDiscountStats']['edges']['0']
>['node'],
stats?: FeesQuery['volumeDiscountStats'],
program?: DiscountProgramsQuery['currentVolumeDiscountProgram']
) => {
const volumeTiers = program?.benefitTiers || [];
if (!lastEpochStats || lastEpochStats.atEpoch !== previousEpoch || !program) {
if (!stats || !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 = volumeTiers.findIndex(
(tier) => tier.volumeDiscountFactor === lastEpochStats?.discountFactor
);
const volumeTierIndex = getVolumeTier(volumeInWindow, volumeTiers);
return {
volumeDiscount,
@@ -20,6 +20,73 @@ 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,9 +40,6 @@ 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();
@@ -68,7 +65,7 @@ export const FundingContainer = ({ marketId }: { marketId: string }) => {
if (edge.node.endTime) {
acc?.push({
endTime: fromNanoSeconds(edge.node.endTime),
fundingRate: Number(edge.node.fundingRate),
fundingRate: Number(edge.node.fundingRate) * 100,
});
}
return acc;
@@ -85,8 +82,7 @@ export const FundingContainer = ({ marketId }: { marketId: string }) => {
<LineChart
data={values}
theme={theme}
priceFormat={priceFormat}
yAxisTickFormat="%"
priceFormat={(fundingRate) => `${fundingRate.toFixed(4)}%`}
/>
);
};
@@ -16,12 +16,15 @@ export const LedgerContainer = () => {
});
const assets = (data?.party?.accountsConnection?.edges ?? [])
.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>
);
.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>);
if (!pubKey) {
return (
@@ -172,20 +172,4 @@ 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();
});
});
+2 -19
View File
@@ -11,13 +11,7 @@ import {
} from '@vegaprotocol/environment';
import { useGlobalStore } from '../../stores';
import { VegaWalletConnectButton } from '../vega-wallet-connect-button';
import {
VegaIconNames,
VegaIcon,
VLogo,
LanguageSelector,
ThemeSwitcher,
} from '@vegaprotocol/ui-toolkit';
import { VegaIconNames, VegaIcon, VLogo } 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';
@@ -28,8 +22,7 @@ import { VegaWalletMenu } from '../vega-wallet';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { WalletIcon } from '../icons/wallet';
import { ProtocolUpgradeCountdown } from '@vegaprotocol/proposals';
import { useT, useI18n } from '../../lib/use-t';
import { supportedLngs } from '../../lib/i18n';
import { useT } from '../../lib/use-t';
type MenuState = 'wallet' | 'nav' | null;
type Theme = 'system' | 'yellow';
@@ -41,7 +34,6 @@ export const Navbar = ({
children?: ReactNode;
theme?: Theme;
}) => {
const i18n = useI18n();
const t = useT();
// menu state for small screens
const [menu, setMenu] = useState<MenuState>(null);
@@ -85,15 +77,6 @@ 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) {
@@ -36,26 +36,6 @@ query RewardsPage($partyId: ID!) {
}
}
query ActivityStreak($partyId: ID!) {
partiesConnection(id: $partyId) {
edges {
node {
id
activityStreak {
activeFor
isActive
inactiveFor
rewardDistributionMultiplier
rewardVestingMultiplier
epoch
tradedVolume
openVolume
}
}
}
}
}
query RewardsHistory(
$partyId: ID!
$epochRewardSummariesPagination: Pagination
@@ -10,13 +10,6 @@ export type RewardsPageQueryVariables = Types.Exact<{
export type RewardsPageQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, vestingStats?: { __typename?: 'PartyVestingStats', rewardBonusMultiplier: string } | null, activityStreak?: { __typename?: 'PartyActivityStreak', rewardVestingMultiplier: string, rewardDistributionMultiplier: string } | null, vestingBalancesSummary: { __typename?: 'PartyVestingBalancesSummary', epoch?: number | null, vestingBalances?: Array<{ __typename?: 'PartyVestingBalance', balance: string, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null, lockedBalances?: Array<{ __typename?: 'PartyLockedBalance', balance: string, untilEpoch: number, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null } } | null };
export type ActivityStreakQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type ActivityStreakQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, activityStreak?: { __typename?: 'PartyActivityStreak', activeFor: number, isActive: boolean, inactiveFor: number, rewardDistributionMultiplier: string, rewardVestingMultiplier: string, epoch: number, tradedVolume: string, openVolume: string } | null } }> } | null };
export type RewardsHistoryQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
epochRewardSummariesPagination?: Types.InputMaybe<Types.Pagination>;
@@ -98,55 +91,6 @@ export function useRewardsPageLazyQuery(baseOptions?: Apollo.LazyQueryHookOption
export type RewardsPageQueryHookResult = ReturnType<typeof useRewardsPageQuery>;
export type RewardsPageLazyQueryHookResult = ReturnType<typeof useRewardsPageLazyQuery>;
export type RewardsPageQueryResult = Apollo.QueryResult<RewardsPageQuery, RewardsPageQueryVariables>;
export const ActivityStreakDocument = gql`
query ActivityStreak($partyId: ID!) {
partiesConnection(id: $partyId) {
edges {
node {
id
activityStreak {
activeFor
isActive
inactiveFor
rewardDistributionMultiplier
rewardVestingMultiplier
epoch
tradedVolume
openVolume
}
}
}
}
}
`;
/**
* __useActivityStreakQuery__
*
* To run a query within a React component, call `useActivityStreakQuery` and pass it any options that fit your needs.
* When your component renders, `useActivityStreakQuery` 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 } = useActivityStreakQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function useActivityStreakQuery(baseOptions: Apollo.QueryHookOptions<ActivityStreakQuery, ActivityStreakQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ActivityStreakQuery, ActivityStreakQueryVariables>(ActivityStreakDocument, options);
}
export function useActivityStreakLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ActivityStreakQuery, ActivityStreakQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ActivityStreakQuery, ActivityStreakQueryVariables>(ActivityStreakDocument, options);
}
export type ActivityStreakQueryHookResult = ReturnType<typeof useActivityStreakQuery>;
export type ActivityStreakLazyQueryHookResult = ReturnType<typeof useActivityStreakLazyQuery>;
export type ActivityStreakQueryResult = Apollo.QueryResult<ActivityStreakQuery, ActivityStreakQueryVariables>;
export const RewardsHistoryDocument = gql`
query RewardsHistory($partyId: ID!, $epochRewardSummariesPagination: Pagination, $partyRewardsPagination: Pagination, $fromEpoch: Int, $toEpoch: Int) {
epochRewardSummaries(
@@ -1,347 +0,0 @@
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { useRewardsHistoryQuery } from './__generated__/Rewards';
import { useReferralProgram } from '../../client-pages/referrals/hooks/use-referral-program';
import { useState, useEffect } from 'react';
import { useT } from '../../lib/use-t';
import { useRewardsRowData } from './use-reward-row-data';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import classNames from 'classnames';
import {
Icon,
type IconName,
Intent,
Tooltip,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { IconNames } from '@blueprintjs/icons';
export const ActiveRewards = ({
epoch,
pubKey,
assets,
}: {
pubKey: string | null;
epoch: number;
assets: Record<string, AssetFieldsFragment>;
}) => {
const [epochVariables] = useState(() => ({
from: epoch - 1,
to: epoch,
}));
// No need to specify the fromEpoch as it will by default give you the last
const { refetch, data } = useRewardsHistoryQuery({
variables: {
partyId: pubKey || '',
fromEpoch: epochVariables.from,
toEpoch: epochVariables.to,
},
});
useEffect(() => {
const interval = setInterval(refetch, 30000);
return () => clearInterval(interval);
}, [refetch]);
const rowData = useRewardsRowData({
epochRewardSummaries: data?.epochRewardSummaries,
partyRewards: data?.party?.rewardsConnection,
assets,
partyId: pubKey,
});
const t = useT();
if (!pubKey) {
return (
<div className="pt-4">
<p className="text-muted text-sm">{t('Not connected')}</p>
</div>
);
}
// TODO: extract card component - call it reward tiles
return (
<div className="grid gap-x-8 gap-y-10 h-fit grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] md:grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] lg:grid-cols-[repeat(auto-fill,_minmax(320px,_1fr))] xl:grid-cols-[repeat(auto-fill,_minmax(343px,_1fr))]">
{rowData.map((row, i) => {
// TODO: filter out 0 values
// const entries = Object.entries(row).filter(([key, value]) => {
// value !== 0;
// });
return (
<div key={i}>
<div
className={classNames(
'bg-gradient-to-r col-span-full p-0.5 lg:col-auto h-full',
'rounded-lg',
'from-vega-blue-500 to-vega-green-400'
)}
>
<div className="bg-gradient-to-b from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded p-4 flex flex-col gap-4">
<div className="flex justify-between gap-4">
<div className="flex flex-col gap-2 items-center text-center">
<span className="flex items-center p-2 rounded-full border border-gray-600">
<VegaIcon name={VegaIconNames.MAN} size={18} />
</span>
<span className="text-muted text-xs">
{t('Individual')}
</span>
</div>
<div className="flex flex-col gap-2 items-center text-center">
<span className="flex flex-col gap-1 font-alpha calt text-2xl shrink-1 text-center">
<span>
{`${addDecimalsFormatNumber(
row.total,
row.asset?.decimals || 0
)}`}
</span>
<span>{`${row.asset?.symbol}`}</span>
</span>
<Tooltip description={'pro rata'} underline={true}>
<span className="text-xs">{t('Pro rata')}</span>
</Tooltip>
</div>
<div className="flex flex-col gap-2 items-center text-center">
<span className="flex items-center p-2 rounded-full border border-gray-600">
<VegaIcon name={VegaIconNames.LOCK} size={18} />
</span>
<span className="text-muted text-xs whitespace-nowrap">
{t('11 epochs')}
</span>
</div>
</div>
<span className="border-[0.5px] border-gray-700" />
<span>
{t('Price taking')} {row.asset?.symbol} {row.asset?.name}
</span>
<div className="flex items-center gap-8 flex-wrap">
<span className="flex flex-col">
<span className="text-muted text-xs">{t('Ends in')}</span>
<span>{t('5 epochs')}</span>
</span>
<span className="flex flex-col">
<span className="text-muted text-xs">
{t('Assessed over')}
</span>
<span>{t('5 epochs')}</span>
</span>
</div>
<span className="text-muted text-sm">
{t(
'Get rewards for taking prices on the order book and paying fees.'
)}
</span>
<span className="border-[0.5px] border-gray-700" />
{/* <div className="grid grid-cols-3 items-center gap-3"> */}
<div className="flex justify-between flex-wrap items-center gap-3">
<span className="flex flex-col gap-1">
<span className="flex items-center gap-1 text-muted text-xs">
{t('Entity scope')}{' '}
</span>
<span className="flex items-center gap-1">
<span className="flex items-center p-1 rounded-full border border-gray-600">
<VegaIcon name={VegaIconNames.MAN} size={16} />
</span>
<StatusIndicator
intent={Intent.Success}
icon={IconNames.TICK_CIRCLE}
/>
</span>
</span>
<span className="flex flex-col gap-1">
<span className="flex items-center gap-1 text-muted text-xs">
{t('Amount staked')}{' '}
</span>
<span className="flex items-center gap-1">
{t('200 VEGA')}
<StatusIndicator
intent={Intent.Success}
icon={IconNames.TICK_CIRCLE}
/>
</span>
</span>
<span className="flex flex-col gap-1">
<span className="flex items-center gap-1 text-muted text-xs">
{t('Average position')}{' '}
</span>
<span className="flex items-center gap-1">
{t('100 USDT')}
<StatusIndicator
intent={Intent.Success}
icon={IconNames.TICK_CIRCLE}
/>
</span>
</span>
</div>
</div>
</div>
</div>
);
})}
</div>
);
};
// const getIconIntent = (status: string) => {
// switch (status) {
// case 'GOOD':
// return { icon: IconNames.TICK_CIRCLE, intent: Intent.Success };
// case 'RETIRED':
// return { icon: IconNames.MOON, intent: Intent.None };
// case 'UNKNOWN':
// return { icon: IconNames.HELP, intent: Intent.Primary };
// case 'MALICIOUS':
// return { icon: IconNames.ERROR, intent: Intent.Danger };
// case 'SUSPICIOUS':
// return { icon: IconNames.ERROR, intent: Intent.Danger };
// case 'COMPROMISED':
// return { icon: IconNames.ERROR, intent: Intent.Danger };
// default:
// return { icon: IconNames.HELP, intent: Intent.Primary };
// }
// };
const StatusIndicator = ({
intent,
icon,
}: {
intent: Intent;
icon: string;
}) => {
return (
<span
className={classNames(
{
'text-gray-700 dark:text-gray-300': intent === Intent.None,
'text-vega-blue': intent === Intent.Primary,
'text-vega-green dark:text-vega-green': intent === Intent.Success,
'dark:text-yellow text-yellow-600': intent === Intent.Warning,
'text-vega-red': intent === Intent.Danger,
},
'flex items-start p-1 align-text-bottom'
)}
>
<Icon size={3} name={icon as IconName} />
</span>
);
};
export const ActivityStreak = ({
epoch,
pubKey,
assets,
}: {
pubKey: string | null;
epoch: number;
assets: Record<string, AssetFieldsFragment>;
}) => {
// const { data } = useActivityStreakQuery({
// variables: {
// partyId: pubKey || '',
// },
// });
const { benefitTiers } = useReferralProgram();
// const streaks = data?.partiesConnection?.edges?.map(
// (edge) => edge?.node?.activityStreak
// );
// @Input()
const progress = 30;
const total = 100;
const safeProgress = () => {
return (progress / total) * 100;
};
const progressBarHeight = 'h-6';
return (
<>
<div className="flex flex-col gap-1 w-full">
<div className="flex flex-col gap-1">
<div
className="grid"
style={{
gridTemplateColumns:
'repeat(' + benefitTiers.length + ', minmax(0, 1fr))',
}}
>
{benefitTiers.map((tier, index) => {
return (
<div key={index} className="flex justify-end -mr-10">
<span className="flex flex-col items-center gap-1">
<span className="flex flex-col items-center font-medium">
<span className="text-sm">Tier {tier.tier}</span>
<span className="text-muted text-xs">7 days</span>
</span>
<span className="text-xs flex flex-col items-center justify-center px-2 py-1 rounded-lg text-white border border-pink-600 bg-pink-900">
<span>Reward 1x</span>
<span>Vesting 1.5x</span>
</span>
<span className="text-pink-500 text-xl"></span>
</span>
</div>
);
})}
</div>
</div>
<div className="flex items-center gap-1">
{benefitTiers.map((tier, index) => {
return (
<div
key={index}
className="bg-white dark:bg-gray-800 shadow-card rounded-[100px] grow"
>
<div
className={classNames(
'relative w-full rounded-[100px] bg-gray-200 dark:bg-gray-800',
progressBarHeight
)}
>
<div
className="absolute left-0 top-0 h-full rounded-[100px] bg-gradient-to-r from-vega-pink-600 to-vega-pink-500"
style={{ width: safeProgress() + '%' }}
></div>
</div>
</div>
);
})}
</div>
<div className="flex items-center gap-1">
<VegaIcon name={VegaIconNames.STREAK} />
<span className="flex flex-col text-xs">
<span>4 days streak</span>
<span>
<span className="text-vega-pink-500">3 days</span> &nbsp;to Tier 1
</span>
</span>
</div>
</div>
{/* <div>{JSON.stringify(stakingTiers)}</div> */}
{/* <div>{JSON.stringify(details)}</div> */}
{/* <div>{JSON.stringify(benefitTiers)}</div> */}
{/* <div>{JSON.stringify(streaks)}</div> */}
</>
);
};
@@ -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,85 +31,42 @@ 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';
import { ActiveRewards, ActivityStreak } from './activity-rewards';
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 || !assetMap) return null;
if (!epochData?.epoch) 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)
)
.filter((a) => new BigNumber(a.balance).isGreaterThan(0))
: [];
const rewardAccountsAssetMap = groupBy(rewardAccounts, 'asset.id');
const lockedBalances = rewardsData?.party?.vestingBalancesSummary
.lockedBalances
? rewardsData.party.vestingBalancesSummary.lockedBalances.filter((b) =>
new BigNumber(b.balance).isGreaterThan(0)
? accounts.filter((a) =>
[
AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
AccountType.ACCOUNT_TYPE_VESTING_REWARDS,
].includes(a.type)
)
: [];
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),
]);
const rewardAssetsMap = groupBy(
rewardAccounts.filter((a) => a.asset.id !== params.reward_asset),
'asset.id'
);
return (
<div className="grid auto-rows-min grid-cols-6 gap-3">
@@ -159,96 +116,26 @@ export const RewardsContainer = () => {
</Card>
{/* Show all other reward pots, most of the time users will not have other rewards */}
{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('Activity streak')}
className="lg:col-span-full"
loading={rewardsLoading}
>
<span className="flex flex-col mx-8">
<ActivityStreak
epoch={Number(epochData?.epoch.id)}
pubKey={pubKey}
assets={assetMap}
/>
</span>
</Card>
<Card
title={t('Active rewards')}
className="lg:col-span-full"
loading={rewardsLoading}
>
<ActiveRewards
epoch={Number(epochData?.epoch.id)}
pubKey={pubKey}
assets={assetMap}
/>
</Card>
{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>
);
})}
<Card
title={t('Rewards history')}
className="lg:col-span-full"
@@ -257,7 +144,6 @@ export const RewardsContainer = () => {
<RewardsHistoryContainer
epoch={Number(epochData?.epoch.id)}
pubKey={pubKey}
assets={assetMap}
/>
</Card>
</div>
@@ -281,7 +167,6 @@ 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();
@@ -357,9 +242,7 @@ export const RewardPot = ({
<CardTable>
<tr>
<CardTableTH className="flex items-center gap-1">
{t('Locked {{assetSymbol}}', {
assetSymbol: rewardAsset.symbol,
})}
{t(`Locked ${rewardAsset.symbol}`)}
<VegaIcon name={VegaIconNames.LOCK} size={12} />
</CardTableTH>
<CardTableTD>
@@ -371,11 +254,7 @@ export const RewardPot = ({
</CardTableTD>
</tr>
<tr>
<CardTableTH>
{t('Vesting {{assetSymbol}}', {
assetSymbol: rewardAsset.symbol,
})}
</CardTableTH>
<CardTableTH>{t(`Vesting ${rewardAsset.symbol}`)}</CardTableTH>
<CardTableTD>
{addDecimalsFormatNumberQuantum(
totalVesting.toString(),
@@ -424,14 +303,13 @@ export const RewardPot = ({
export const Vesting = ({
pubKey,
baseRate,
multiplier,
multiplier = '1',
}: {
pubKey: string | null;
baseRate: string;
multiplier?: string;
}) => {
const t = useT();
const rate = new BigNumber(baseRate).times(multiplier || 1);
const rate = new BigNumber(baseRate).times(multiplier);
const rateFormatted = formatPercentage(Number(rate));
const baseRateFormatted = formatPercentage(Number(baseRate));
@@ -446,7 +324,7 @@ export const Vesting = ({
{pubKey && (
<tr>
<CardTableTH>{t('Vesting multiplier')}</CardTableTH>
<CardTableTD>{multiplier ? `${multiplier}x` : '-'}</CardTableTD>
<CardTableTD>{multiplier}x</CardTableTD>
</tr>
)}
</CardTable>
@@ -456,16 +334,15 @@ export const Vesting = ({
export const Multipliers = ({
pubKey,
streakMultiplier,
hoarderMultiplier,
streakMultiplier = '1',
hoarderMultiplier = '1',
}: {
pubKey: string | null;
streakMultiplier?: string;
hoarderMultiplier?: string;
}) => {
const t = useT();
const combinedMultiplier = new BigNumber(streakMultiplier || 1).times(
hoarderMultiplier || 1
const combinedMultiplier = new BigNumber(streakMultiplier).times(
hoarderMultiplier
);
if (!pubKey) {
@@ -486,15 +363,11 @@ export const Multipliers = ({
<CardTable>
<tr>
<CardTableTH>{t('Streak reward multiplier')}</CardTableTH>
<CardTableTD>
{streakMultiplier ? `${streakMultiplier}x` : '-'}
</CardTableTD>
<CardTableTD>{streakMultiplier}x</CardTableTD>
</tr>
<tr>
<CardTableTH>{t('Hoarder reward multiplier')}</CardTableTH>
<CardTableTD>
{hoarderMultiplier ? `${hoarderMultiplier}x` : '-'}
</CardTableTD>
<CardTableTD>{hoarderMultiplier}x</CardTableTD>
</tr>
</CardTable>
</div>
@@ -61,14 +61,6 @@ 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) => {
@@ -77,7 +69,7 @@ const getCell = (cells: HTMLElement[], colId: string) => {
);
};
describe('RewardsHistoryTable', () => {
describe('RewarsHistoryTable', () => {
const props = {
epochRewardSummaries: {
edges: rewardSummaries,
@@ -96,7 +88,7 @@ describe('RewardsHistoryTable', () => {
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(
@@ -118,27 +110,17 @@ describe('RewardsHistoryTable', () => {
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('50.00%');
const infrastructureFeesCell = getCell(cells, 'infrastructureFees');
expect(
infrastructureFeesCell.getByTestId('stack-cell-primary')
).toHaveTextContent('300');
expect(
infrastructureFeesCell.getByTestId('stack-cell-secondary')
).toHaveTextContent('50.00%');
).toHaveTextContent('100.00%');
let totalCell = getCell(cells, 'total');
expect(totalCell.getByText('600.00')).toBeInTheDocument();
expect(totalCell.getByText('300.00')).toBeInTheDocument();
// Second row
row = within(rows[1]);
cells = row.getAllByRole('gridcell');
@@ -2,7 +2,10 @@ import debounce from 'lodash/debounce';
import { useMemo, useState } from 'react';
import BigNumber from 'bignumber.js';
import type { ColDef, ValueFormatterFunc } from 'ag-grid-community';
import { type AssetFieldsFragment } from '@vegaprotocol/assets';
import {
useAssetsMapProvider,
type AssetFieldsFragment,
} from '@vegaprotocol/assets';
import {
addDecimalsFormatNumberQuantum,
formatNumberPercentage,
@@ -13,27 +16,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: {
@@ -137,7 +140,6 @@ export const RewardHistoryTable = ({
onEpochChange: (epochVariables: { from: number; to: number }) => void;
loading: boolean;
}) => {
const t = useT();
const [isParty, setIsParty] = useState(false);
const rowData = useRewardsRowData({
@@ -151,12 +153,10 @@ export const RewardHistoryTable = ({
const rewardValueFormatter: ValueFormatterFunc<RewardRow> = ({
data,
value,
...rest
}) => {
if (!value || !data) {
return '-';
}
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
@@ -196,11 +196,6 @@ export const RewardHistoryTable = ({
},
sort: 'desc',
},
{
field: 'infrastructureFees',
valueFormatter: rewardValueFormatter,
cellRenderer: rewardCellRenderer,
},
{
field: 'staking',
valueFormatter: rewardValueFormatter,
@@ -255,14 +250,6 @@ export const RewardHistoryTable = ({
return colDefs;
}, []);
if (!pubKey) {
return (
<div className="pt-4">
<p className="text-muted text-sm">{t('Not connected')}</p>
</div>
);
}
return (
<div>
<div className="mb-2 flex items-center justify-between gap-2">
@@ -350,7 +337,7 @@ export const RewardHistoryTable = ({
);
};
export const EpochInput = ({
const EpochInput = ({
id,
value,
max,
@@ -1,159 +0,0 @@
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,10 +16,9 @@ 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,
];
export const getRewards = (
const getRewards = (
rewards: Array<{
rewardType: AccountType;
assetId: string;
@@ -57,9 +56,6 @@ export 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(
@@ -105,8 +101,7 @@ export const useRewardsRowData = ({
assetId: r.asset.id,
amount: r.amount,
}));
const result = getRewards(rewards, assets);
return result;
return getRewards(rewards, assets);
}
const rewards = removePaginationWrapper(epochRewardSummaries?.edges);
@@ -1,12 +0,0 @@
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();
});
});
+2 -55
View File
@@ -1,13 +1,7 @@
import {
Dialog,
Intent,
Switch,
ToastPositionSetter,
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import { Switch, ToastPositionSetter } from '@vegaprotocol/ui-toolkit';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
import { useState, type ReactNode } from 'react';
import type { ReactNode } from 'react';
import classNames from 'classnames';
import { useT } from '../../lib/use-t';
@@ -15,7 +9,6 @@ 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')}>
@@ -40,52 +33,6 @@ 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 && (
+11 -24
View File
@@ -16,7 +16,6 @@ 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',
@@ -164,14 +163,12 @@ export const SidebarContent = () => {
if (params.marketId) {
return (
<ContentWrapper>
<ErrorBoundary feature="deal-ticket">
<DealTicketContainer
marketId={params.marketId}
onDeposit={(assetId) =>
setViews({ type: ViewType.Deposit, assetId }, currentRouteId)
}
/>
</ErrorBoundary>
<DealTicketContainer
marketId={params.marketId}
onDeposit={(assetId) =>
setViews({ type: ViewType.Deposit, assetId }, currentRouteId)
}
/>
<GetStarted />
</ContentWrapper>
);
@@ -184,9 +181,7 @@ export const SidebarContent = () => {
if (params.marketId) {
return (
<ContentWrapper>
<ErrorBoundary feature="market-info">
<MarketInfoAccordionContainer marketId={params.marketId} />
</ErrorBoundary>
<MarketInfoAccordionContainer marketId={params.marketId} />
</ContentWrapper>
);
} else {
@@ -197,9 +192,7 @@ export const SidebarContent = () => {
if (view.type === ViewType.Deposit) {
return (
<ContentWrapper title={t('Deposit')}>
<ErrorBoundary feature="deposit">
<DepositContainer assetId={view.assetId} />
</ErrorBoundary>
<DepositContainer assetId={view.assetId} />
</ContentWrapper>
);
}
@@ -207,9 +200,7 @@ export const SidebarContent = () => {
if (view.type === ViewType.Withdraw) {
return (
<ContentWrapper title={t('Withdraw')}>
<ErrorBoundary feature="withdraw">
<WithdrawContainer assetId={view.assetId} />
</ErrorBoundary>
<WithdrawContainer assetId={view.assetId} />
</ContentWrapper>
);
}
@@ -217,9 +208,7 @@ export const SidebarContent = () => {
if (view.type === ViewType.Transfer) {
return (
<ContentWrapper title={t('Transfer')}>
<ErrorBoundary feature="transfer">
<TransferContainer assetId={view.assetId} />
</ErrorBoundary>
<TransferContainer assetId={view.assetId} />
</ContentWrapper>
);
}
@@ -227,9 +216,7 @@ export const SidebarContent = () => {
if (view.type === ViewType.Settings) {
return (
<ContentWrapper title={t('Settings')}>
<ErrorBoundary feature="settings">
<Settings />
</ErrorBoundary>
<Settings />
</ContentWrapper>
);
}
+1 -1
View File
@@ -1,2 +1,2 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
VEGA_VERSION=v0.73.6
VEGA_VERSION=v0.73.4
+1 -1
View File
@@ -1,2 +1,2 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:develop
VEGA_VERSION=v0.73.6
VEGA_VERSION=v0.73.4
+1 -1
View File
@@ -1,2 +1,2 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:main
VEGA_VERSION=v0.73.6
VEGA_VERSION=v0.73.4
+1 -12
View File
@@ -34,15 +34,4 @@ def next_epoch(vega: VegaServiceNull):
"Epoch not started after forwarding the duration of two epochs."
)
vega.wait_fn(1)
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()
vega.wait_for_total_catchup()
+2 -7
View File
@@ -12,7 +12,6 @@ from contextlib import contextmanager
from vega_sim.null_service import VegaServiceNull
from playwright.sync_api import Browser, Page
from config import console_image_name, vega_version
from datetime import datetime, timedelta
from fixtures.market import (
setup_simple_market,
setup_opening_auction_market,
@@ -79,7 +78,6 @@ def init_vega(request=None):
store_transactions=True,
transactions_per_block=1000,
seconds_per_block=seconds_per_block,
genesis_time= datetime.now() - timedelta(days=1),
) as vega:
try:
container = docker_client.containers.run(
@@ -138,11 +136,8 @@ def init_page(vega: VegaServiceNull, browser: Browser, request: pytest.FixtureRe
page.add_init_script(script=window_env)
yield page
finally:
try:
if not os.path.exists("apps/trading/e2e/traces"):
os.makedirs("apps/trading/e2e/traces")
except OSError as e:
print(f"Failed to create directory '{'apps/trading/e2e/traces'}': {e}")
if not os.path.exists("traces"):
os.makedirs("traces")
# Check whether this test failed or passed
outcome = request.config.cache.get(request.node.nodeid, None)
+14 -1
View File
@@ -1,13 +1,26 @@
from collections import namedtuple
from vega_sim.service import VegaService
from actions.vega import submit_multiple_orders, submit_order, submit_liquidity
from wallet_config import MM_WALLET, MM_WALLET2, TERMINATE_WALLET, wallets
import logging
logger = logging.getLogger()
# Defined namedtuples
WalletConfig = namedtuple("WalletConfig", ["name", "passphrase"])
# Wallet Configurations
MM_WALLET = WalletConfig("mm", "pin")
MM_WALLET2 = WalletConfig("mm2", "pin2")
TERMINATE_WALLET = WalletConfig("FJMKnwfZdd48C8NqvYrG", "bY3DxwtsCstMIIZdNpKs")
wallets = [MM_WALLET, MM_WALLET2, TERMINATE_WALLET]
mint_amount: float = 10e5
market_name = "BTC:DAI_2023"
def setup_simple_market(
vega: VegaService,
approve_proposal=True,
+1 -1
View File
@@ -1161,7 +1161,7 @@ profile = ["pytest-profiling", "snakeviz"]
type = "git"
url = "https://github.com/vegaprotocol/vega-market-sim.git"
reference = "HEAD"
resolved_reference = "fbcb974b2055bbc80169cdfd69987f087f9969fb"
resolved_reference = "e93f7dfa8463c59cfd0e299362b845511cebeef6"
[[package]]
name = "websocket-client"
+1 -1
View File
@@ -9,7 +9,7 @@ packages = [{include = "trading market-sim e2e"}]
[tool.poetry.dependencies]
python = ">=3.9,<3.11"
psutil = "^5.9.5"
vega-sim = {git = "https://github.com/vegaprotocol/vega-market-sim.git/", branch = "fix/genesis_panic"}
vega-sim = {git = "https://github.com/vegaprotocol/vega-market-sim.git"}
pytest-playwright = "^0.4.2"
docker = "^6.1.3"
pytest-xdist = "^3.3.1"
@@ -0,0 +1,12 @@
import pytest
from playwright.sync_api import expect, Page
@pytest.mark.usefixtures("page", "continuous_market", "risk_accepted")
def test_see_market_depth_chart(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
# Click on the 'Depth' tab
page.get_by_test_id("Depth").click()
# Check if the 'Depth' tab and the depth chart are visible
# 6006-DEPC-001
expect(page.get_by_test_id("tab-depth")).to_be_visible()
expect(page.locator('[class^="depth-chart-module_canvas__"]').first).to_be_visible()

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