Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f63dc30e69 | ||
|
|
89d81567d9 | ||
|
|
3cd393dac0 | ||
|
|
a52e60d6a2 | ||
|
|
bc13f1b359 | ||
|
|
eb81f4ae44 | ||
|
|
51ab02a2e2 | ||
|
|
ffada1b93d | ||
|
|
9dda3f712b | ||
|
|
df20dbeee0 | ||
|
|
cdfd8a2d00 | ||
|
|
1e5c523bc4 | ||
|
|
37cd69ba6e | ||
|
|
2c11045dd9 | ||
|
|
9aef41a119 | ||
|
|
80ab8821d0 | ||
|
|
8a3657a9b9 | ||
|
|
7100b0e9fc | ||
|
|
614a83b7d6 | ||
|
|
3dc77b0eff | ||
|
|
a59f7dfd29 | ||
|
|
61471228aa | ||
|
|
127e784ceb | ||
|
|
70d748fb15 | ||
|
|
e06f4818fc | ||
|
|
8182da3b31 | ||
|
|
c8c56307bb | ||
|
|
a8cd7f157f | ||
|
|
4f18caa486 | ||
|
|
5c7c626bbc | ||
|
|
e4c4c20631 | ||
|
|
0697302d07 | ||
|
|
2d926c0ce0 | ||
|
|
f57d6a7c7b | ||
|
|
15f905046f | ||
|
|
4f7918f64e | ||
|
|
52ab0562b0 | ||
|
|
4e2b0d1b1d | ||
|
|
0b0bcad9b3 | ||
|
|
bcf17bb34e | ||
|
|
a2b9b0da05 | ||
|
|
7588d0cd11 | ||
|
|
5ee1748495 | ||
|
|
73a118978f | ||
|
|
eac26c1966 |
@@ -12,7 +12,8 @@ import { type VegaICellRendererParams } from '@vegaprotocol/datagrid';
|
||||
import { useRef, useLayoutEffect } from 'react';
|
||||
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { type RowClickedEvent, ColDef } from 'ag-grid-community';
|
||||
import { type ColDef } from 'ag-grid-community';
|
||||
import type { RowClickedEvent } from 'ag-grid-community';
|
||||
|
||||
type AssetsTableProps = {
|
||||
data: AssetFieldsFragment[] | null;
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
type VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { type RowClickedEvent, ColDef } from 'ag-grid-community';
|
||||
import { type ColDef } from 'ag-grid-community';
|
||||
import type { RowClickedEvent } from 'ag-grid-community';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useState } from 'react';
|
||||
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
|
||||
import {
|
||||
CopyWithTooltip,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
interface SignatureProps {
|
||||
signature: BlockExplorerTransactionResult['signature'];
|
||||
}
|
||||
|
||||
const valueClass =
|
||||
'font-mono px-2.5 py-0.5 text-xs max-w-[200px] cursor-pointer';
|
||||
const valueClassClosed = 'text-ellipsis overflow-hidden';
|
||||
const valueClassOpen = 'break-words text-left';
|
||||
|
||||
/**
|
||||
* Viewer component for a vega signature. Featuers copy and pasting, truncation
|
||||
*
|
||||
* @param signature
|
||||
*/
|
||||
export const Signature = ({ signature }: SignatureProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
if (!signature || !signature.value || !signature.version || !signature.algo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="inline-flex border rounded signature-component relative pr-[20px]">
|
||||
<span
|
||||
className="bg-gray-100 px-2.5 py-0.5 text-xs text-gray-500 select-none cursor-default"
|
||||
title={`Version ${signature.version}`}
|
||||
>
|
||||
{signature.algo}
|
||||
</span>
|
||||
<div
|
||||
className={
|
||||
isOpen
|
||||
? `${valueClass} ${valueClassOpen}`
|
||||
: `${valueClass} ${valueClassClosed}`
|
||||
}
|
||||
>
|
||||
<CopyWithTooltip text={signature.value}>
|
||||
<span title={signature.value}>{signature.value}</span>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="absolute top-[-3px] right-0 pr-2"
|
||||
title={t('Show full signature')}
|
||||
>
|
||||
<VegaIcon name={isOpen ? VegaIconNames.EYE_OFF : VegaIconNames.EYE} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { Time } from '../../../time';
|
||||
import { ChainResponseCode } from '../chain-response-code/chain-reponse.code';
|
||||
import { TxDataView } from '../../tx-data-view';
|
||||
import Hash from '../../../links/hash';
|
||||
import { Signature } from '../../../signature/signature';
|
||||
|
||||
interface TxDetailsSharedProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@@ -75,6 +76,12 @@ export const TxDetailsShared = ({
|
||||
<BlockLink height={height} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell {...sharedHeaderProps}>{t('Signature')}</TableCell>
|
||||
<TableCell>
|
||||
<Signature signature={txData.signature} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell {...sharedHeaderProps}>{t('Time')}</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@@ -98,6 +98,8 @@ describe('TxDetailsTransfer', () => {
|
||||
},
|
||||
},
|
||||
signature: {
|
||||
version: '1',
|
||||
algo: 'vega/ed25519',
|
||||
value:
|
||||
'610c2e196a7d4fed4413b9e82af267b1ff3e30e943df3a3d28096fd60604d430d752fbaf6dd4f84d496be78885bb6118f40560bff7832c06bd7a3d67b718b700',
|
||||
},
|
||||
|
||||
@@ -20,6 +20,8 @@ const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
|
||||
'4b782482f587d291e8614219eb9a5ee9280fa2c58982dee71d976782a9be1964',
|
||||
type: 'Submit Order',
|
||||
signature: {
|
||||
version: '1',
|
||||
algo: 'vega/ed25519',
|
||||
value: '123',
|
||||
},
|
||||
code: 0,
|
||||
|
||||
@@ -23,6 +23,8 @@ const txData: BlockExplorerTransactionResult = {
|
||||
type: 'type',
|
||||
command: {} as ValidatorHeartbeat,
|
||||
signature: {
|
||||
version: '1',
|
||||
algo: 'vega/ed25519',
|
||||
value: '123',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface BlockExplorerTransactionResult {
|
||||
cursor: string;
|
||||
command: components['schemas']['blockexplorerv1transaction'];
|
||||
signature: {
|
||||
version: string;
|
||||
algo: string;
|
||||
value: string;
|
||||
};
|
||||
error?: string;
|
||||
|
||||
@@ -23,10 +23,13 @@ export const Heading = ({
|
||||
})}
|
||||
>
|
||||
<h1
|
||||
className={classNames('font-alpha calt text-5xl break-words', {
|
||||
'mt-0': !marginTop,
|
||||
'mb-0': !marginBottom,
|
||||
})}
|
||||
className={classNames(
|
||||
'font-alpha calt text-5xl [word-break:break-word]',
|
||||
{
|
||||
'mt-0': !marginTop,
|
||||
'mb-0': !marginBottom,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
@@ -49,12 +49,8 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
|
||||
? activeProvider
|
||||
: defaultProvider;
|
||||
|
||||
if (
|
||||
account &&
|
||||
activeProvider &&
|
||||
typeof activeProvider.getSigner === 'function'
|
||||
) {
|
||||
signer = provider.getSigner();
|
||||
if (account && provider && typeof provider.getSigner === 'function') {
|
||||
signer = provider.getSigner(account);
|
||||
}
|
||||
|
||||
const tokenVestingAddress =
|
||||
|
||||
+3
@@ -7,8 +7,10 @@ import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
|
||||
export const ProposalAssetDetails = ({
|
||||
asset,
|
||||
originalAsset,
|
||||
}: {
|
||||
asset: AssetFieldsFragment;
|
||||
originalAsset?: AssetFieldsFragment;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [showAssetDetails, setShowAssetDetails] = useState(false);
|
||||
@@ -27,6 +29,7 @@ export const ProposalAssetDetails = ({
|
||||
<div className="mb-10 pb-4">
|
||||
<AssetDetailsTable
|
||||
asset={asset}
|
||||
originalAsset={originalAsset}
|
||||
omitRows={[
|
||||
AssetDetail.STATUS,
|
||||
AssetDetail.INFRASTRUCTURE_FEE_ACCOUNT_BALANCE,
|
||||
|
||||
+2
-2
@@ -54,8 +54,8 @@ export const ProposalReferralProgramDetails = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const benefitTiers = proposal?.terms?.change?.benefitTiers;
|
||||
const stakingTiers = proposal?.terms?.change?.stakingTiers;
|
||||
const benefitTiers = proposal?.terms?.change?.benefitTiers.slice();
|
||||
const stakingTiers = proposal?.terms?.change?.stakingTiers.slice();
|
||||
const windowLength = proposal?.terms?.change?.windowLength;
|
||||
const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgram;
|
||||
|
||||
|
||||
@@ -65,10 +65,13 @@ export const Proposal = ({
|
||||
? removePaginationWrapper(assetData.assetsConnection?.edges)[0]
|
||||
: undefined;
|
||||
|
||||
const originalAsset = asset;
|
||||
|
||||
if (proposal.terms.change.__typename === 'UpdateAsset' && asset) {
|
||||
asset = {
|
||||
...asset,
|
||||
quantum: proposal.terms.change.quantum,
|
||||
source: { ...asset.source },
|
||||
};
|
||||
|
||||
if (asset.source.__typename === 'ERC20') {
|
||||
@@ -228,7 +231,7 @@ export const Proposal = ({
|
||||
proposal.terms.change.__typename === 'UpdateAsset') &&
|
||||
asset && (
|
||||
<div className="mb-4">
|
||||
<ProposalAssetDetails asset={asset} />
|
||||
<ProposalAssetDetails asset={asset} originalAsset={originalAsset} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -104,6 +104,10 @@
|
||||
list-style: circle;
|
||||
}
|
||||
|
||||
.react-markdown-container a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.jsondiffpatch-delta,
|
||||
.jsondiffpatch-delta pre {
|
||||
font-family: 'Roboto Mono', monospace !important;
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const expirtyTooltip = 'expiry-tooltip';
|
||||
const externalLink = 'external-link';
|
||||
const itemHeader = 'item-header';
|
||||
const itemValue = 'item-value';
|
||||
const link = 'link';
|
||||
const liquidityLink = 'view-liquidity-link';
|
||||
const liquiditySupplied = 'liquidity-supplied';
|
||||
const liquiditySuppliedTooltip = 'liquidity-supplied-tooltip';
|
||||
const marketChange = 'market-change';
|
||||
const marketExpiry = 'market-expiry';
|
||||
const marketMode = 'market-trading-mode';
|
||||
const marketName = 'header-title';
|
||||
const marketPrice = 'market-price';
|
||||
const marketSettlement = 'market-settlement-asset';
|
||||
const 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 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Market tooltips', { tags: '@smoke' }, () => {
|
||||
it('should see expiry tooltip', () => {
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketExpiry).within(() => {
|
||||
cy.getByTestId(itemValue)
|
||||
.should('have.text', 'Not time-based')
|
||||
.realHover();
|
||||
});
|
||||
});
|
||||
cy.getByTestId(expirtyTooltip)
|
||||
.eq(0)
|
||||
.should(
|
||||
'contain.text',
|
||||
'This market expires when triggered by its oracle, not on a set date.'
|
||||
)
|
||||
.within(() => {
|
||||
cy.getByTestId(link)
|
||||
.should('have.attr', 'href')
|
||||
.and('include', Cypress.env('EXPLORER_URL'));
|
||||
});
|
||||
});
|
||||
|
||||
it('should see trading conditions tooltip', () => {
|
||||
const toolTipLabel = 'tooltip-label';
|
||||
const toolTipValue = 'tooltip-value';
|
||||
const auctionToolTipLabels = [
|
||||
'Auction start',
|
||||
'Est. auction end',
|
||||
'Target liquidity',
|
||||
'Current liquidity',
|
||||
'Est. uncrossing price',
|
||||
'Est. uncrossing vol',
|
||||
];
|
||||
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketMode).within(() => {
|
||||
cy.getByTestId(itemValue)
|
||||
.should('contain.text', 'Monitoring auction')
|
||||
.and('contain.text', 'liquidity')
|
||||
.realHover();
|
||||
});
|
||||
});
|
||||
cy.getByTestId(tradingModeTooltip)
|
||||
.should(
|
||||
'contain.text',
|
||||
'This market is in auction until it reaches sufficient liquidity.'
|
||||
)
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.getByTestId(externalLink)
|
||||
.should('have.attr', 'href')
|
||||
.and('include', Cypress.env('TRADING_MODE_LINK'));
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
cy.getByTestId(toolTipLabel)
|
||||
.eq(i)
|
||||
.should('have.text', auctionToolTipLabels[i]);
|
||||
cy.getByTestId(toolTipValue).eq(i).should('not.be.empty');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should see liquidity supplied tooltip', () => {
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(liquiditySupplied).within(() => {
|
||||
cy.getByTestId(itemValue).realHover();
|
||||
});
|
||||
});
|
||||
cy.getByTestId(liquiditySuppliedTooltip)
|
||||
.should('contain.text', 'Supplied stake')
|
||||
.and('contain.text', 'Target stake')
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.getByTestId(liquidityLink).should(
|
||||
'have.text',
|
||||
'View liquidity provision table'
|
||||
);
|
||||
cy.getByTestId(externalLink).should(
|
||||
'have.text',
|
||||
'Learn about providing liquidity'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,103 +0,0 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||
import { createOrder } from '../support/create-order';
|
||||
|
||||
describe(
|
||||
'vega wallet - prompt',
|
||||
{ tags: '@regression', testIsolation: true },
|
||||
() => {
|
||||
describe('must submit order', { tags: '@smoke' }, () => {
|
||||
// 7002-SORD-039
|
||||
beforeEach(() => {
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
it('must see a prompt to check connected vega wallet to approve transaction', () => {
|
||||
// 0003-WTXN-002
|
||||
cy.mockVegaWalletTransaction(1000);
|
||||
const order: OrderSubmission = {
|
||||
marketId: 'market-0',
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
size: '100',
|
||||
};
|
||||
createOrder(order);
|
||||
cy.getByTestId('toast-content').should(
|
||||
'contain.text',
|
||||
'Please go to your Vega wallet application and approve or reject the transaction.'
|
||||
);
|
||||
});
|
||||
|
||||
it('must show error returned by wallet ', () => {
|
||||
// 0003-WTXN-009
|
||||
// 0003-WTXN-011
|
||||
// 0002-WCON-016
|
||||
// 0003-WTXN-008
|
||||
|
||||
//trigger error from the wallet
|
||||
cy.intercept('POST', 'http://localhost:1789/api/v2/requests', (req) => {
|
||||
req.on('response', (res) => {
|
||||
res.send({
|
||||
jsonrpc: '2.0',
|
||||
id: '1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const order: OrderSubmission = {
|
||||
marketId: 'market-0',
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
size: '100',
|
||||
};
|
||||
createOrder(order);
|
||||
cy.getByTestId('toast-content').should(
|
||||
'contain.text',
|
||||
'The connection to your Vega Wallet has been lost.'
|
||||
);
|
||||
cy.getByTestId('connect-vega-wallet').click();
|
||||
cy.getByTestId('dialog-content').should('be.visible');
|
||||
});
|
||||
|
||||
it('must see that the order was rejected by the connected wallet', () => {
|
||||
// 0003-WTXN-007
|
||||
|
||||
//trigger rejection error from the wallet
|
||||
cy.intercept('POST', 'http://localhost:1789/api/v2/requests', (req) => {
|
||||
req.alias = 'client.send_transaction';
|
||||
req.reply({
|
||||
statusCode: 400,
|
||||
body: {
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: 3001,
|
||||
data: 'the user rejected the wallet connection',
|
||||
message: 'User error',
|
||||
},
|
||||
id: '0',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const order: OrderSubmission = {
|
||||
marketId: 'market-0',
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
size: '100',
|
||||
};
|
||||
createOrder(order);
|
||||
cy.getByTestId('toast-content').should(
|
||||
'contain.text',
|
||||
'Error occurredthe user rejected the wallet connection'
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -26,18 +26,6 @@ describe('ethereum wallet', { tags: '@smoke', testIsolation: true }, () => {
|
||||
cy.getByTestId('tab-deposits').should('not.be.empty');
|
||||
});
|
||||
|
||||
it.skip('should see QR code modal for WalletConnect', () => {
|
||||
// 0004-EWAL-003
|
||||
|
||||
cy.getByTestId('Deposits').click();
|
||||
cy.getByTestId('deposit-button').click();
|
||||
cy.getByTestId('connect-eth-wallet-btn').click();
|
||||
cy.getByTestId('web3-connector-list').should('exist');
|
||||
cy.getByTestId('web3-connector-WalletConnect').click();
|
||||
// testing if exists rather than visible because of the long loading time
|
||||
cy.get('#w3m-modal').should('exist');
|
||||
});
|
||||
|
||||
it('able to disconnect eth wallet', () => {
|
||||
// 0004-EWAL-004
|
||||
// 0004-EWAL-005
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import {
|
||||
mockConnectWallet,
|
||||
mockConnectWalletWithUserError,
|
||||
} from '@vegaprotocol/cypress';
|
||||
|
||||
const connectVegaBtn = 'connect-vega-wallet';
|
||||
const manageVegaBtn = 'manage-vega-wallet';
|
||||
const dialogContent = 'dialog-content';
|
||||
|
||||
describe('connect vega wallet', { tags: '@smoke', testIsolation: true }, () => {
|
||||
beforeEach(() => {
|
||||
// Using portfolio page as it requires vega wallet connection
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.setOnBoardingViewed();
|
||||
cy.visit('/#/portfolio');
|
||||
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
|
||||
});
|
||||
|
||||
it('can connect', () => {
|
||||
// 0002-WCON-002
|
||||
// 0002-WCON-005
|
||||
// 0002-WCON-007
|
||||
// 0002-WCON-009
|
||||
|
||||
mockConnectWallet();
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-jsonRpc"]')
|
||||
.click();
|
||||
cy.wait('@walletReq');
|
||||
cy.getByTestId(dialogContent).should(
|
||||
'contain.text',
|
||||
'Approve the connection from your Vega wallet app.'
|
||||
);
|
||||
cy.getByTestId(dialogContent).should('not.exist');
|
||||
cy.getByTestId(manageVegaBtn).should('exist');
|
||||
});
|
||||
|
||||
it('can not connect', () => {
|
||||
// 0002-WCON-002
|
||||
// 0002-WCON-005
|
||||
// 0002-WCON-007
|
||||
// 0002-WCON-015
|
||||
|
||||
mockConnectWalletWithUserError();
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-jsonRpc"]')
|
||||
.click();
|
||||
cy.getByTestId('dialog-content')
|
||||
.should('contain.text', 'User error')
|
||||
.and('contain.text', 'the user rejected the wallet connection');
|
||||
});
|
||||
|
||||
it('can change selected public key and disconnect', () => {
|
||||
// 0002-WCON-022
|
||||
// 0002-WCON-023
|
||||
// 0002-WCON-025
|
||||
// 0002-WCON-026
|
||||
// 0002-WCON-021
|
||||
// 0002-WCON-027
|
||||
// 0002-WCON-030
|
||||
// 0002-WCON-029
|
||||
// 0002-WCON-008
|
||||
// 0002-WCON-035
|
||||
// 0002-WCON-014
|
||||
// 0002-WCON-010
|
||||
// 0003-WTXN-004
|
||||
|
||||
mockConnectWallet();
|
||||
const key2 = Cypress.env('VEGA_PUBLIC_KEY2');
|
||||
const truncatedKey2 = Cypress.env('TRUNCATED_VEGA_PUBLIC_KEY2');
|
||||
cy.connectVegaWallet();
|
||||
cy.getByTestId('manage-vega-wallet').click();
|
||||
cy.getByTestId('keypair-list').should('exist');
|
||||
cy.getByTestId(`key-${key2}`).should('contain.text', truncatedKey2);
|
||||
cy.getByTestId(`key-${key2}`)
|
||||
.find('[data-testid="copy-vega-public-key"]')
|
||||
.should('be.visible');
|
||||
cy.get(`[data-testid="key-${key2}"] > .mr-2`).click();
|
||||
cy.getByTestId('keypair-list')
|
||||
.find('[data-state="checked"]')
|
||||
.should('be.visible');
|
||||
cy.getByTestId('disconnect').click();
|
||||
cy.getByTestId('connect-vega-wallet').should('exist');
|
||||
cy.getByTestId('manage-vega-wallet').should('not.exist');
|
||||
cy.getByTestId('connect-vega-wallet').click();
|
||||
cy.contains('Enter a custom wallet location');
|
||||
});
|
||||
});
|
||||
@@ -28,3 +28,4 @@ NX_REFERRALS=false
|
||||
|
||||
NX_TENDERMINT_URL=https://be.vega.community
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||
NX_DISABLE_CLOSE_POSITION=true
|
||||
|
||||
@@ -1,34 +1,11 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
import { useTopTradedMarkets } from '../../lib/hooks/use-top-traded-markets';
|
||||
import { Links } from '../../lib/links';
|
||||
import { useNavigateToLastMarket } from '../../lib/hooks/use-navigate-to-last-market';
|
||||
|
||||
// The home pages only purpose is to redirect to the users last market,
|
||||
// the top traded if they are new, or fall back to the list of markets.
|
||||
// Thats why we just render a loader here
|
||||
export const Home = () => {
|
||||
const navigate = useNavigate();
|
||||
const { data } = useTopTradedMarkets();
|
||||
const marketId = useGlobalStore((store) => store.marketId);
|
||||
|
||||
useEffect(() => {
|
||||
if (marketId) {
|
||||
navigate(Links.MARKET(marketId), {
|
||||
replace: true,
|
||||
});
|
||||
} else if (data) {
|
||||
const marketDataId = data[0]?.id;
|
||||
if (marketDataId) {
|
||||
navigate(Links.MARKET(marketDataId), {
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
navigate(Links.MARKETS());
|
||||
}
|
||||
}
|
||||
}, [marketId, data, navigate]);
|
||||
useNavigateToLastMarket();
|
||||
|
||||
return (
|
||||
<Splash>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import { DocsLinks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { ButtonLink, ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { MarketProposalNotification } from '@vegaprotocol/proposals';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
@@ -145,7 +144,6 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
||||
/>
|
||||
</HeaderStat>
|
||||
)}
|
||||
<MarketProposalNotification marketId={market.id} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useMemo } from 'react';
|
||||
import { addDecimalsFormatNumber, titlefy } from '@vegaprotocol/utils';
|
||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { ExternalLink, Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { Link, Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { getAsset, marketDataProvider, useMarket } from '@vegaprotocol/markets';
|
||||
import { useGlobalStore, usePageTitleStore } from '../../stores';
|
||||
import { TradeGrid } from './trade-grid';
|
||||
@@ -117,12 +117,13 @@ export const MarketPage = () => {
|
||||
defaults="Please choose another market from the <0>market list</0>"
|
||||
ns={ns}
|
||||
components={[
|
||||
<ExternalLink
|
||||
<Link
|
||||
className="underline underline-offset-4 "
|
||||
onClick={() => navigate(Links.MARKETS())}
|
||||
key="link"
|
||||
>
|
||||
market list
|
||||
</ExternalLink>,
|
||||
</Link>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { act, render, screen, waitFor, within } from '@testing-library/react';
|
||||
// import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Closed } from './closed';
|
||||
import { MarketStateMapping, PropertyKeyType } from '@vegaprotocol/types';
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
marketsDataQuery,
|
||||
createMarketsDataFragment,
|
||||
} from '@vegaprotocol/mock';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('Closed', () => {
|
||||
let originalNow: typeof Date.now;
|
||||
@@ -168,14 +170,11 @@ describe('Closed', () => {
|
||||
Date.now = originalNow;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('renders correctly formatted and filtered rows', async () => {
|
||||
const renderComponent = async (mocks: MockedResponse[]) => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[marketsMock, marketsDataMock, oracleDataMock]}
|
||||
>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
@@ -185,6 +184,10 @@ describe('Closed', () => {
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
it('renders correct headers', async () => {
|
||||
await renderComponent([marketsMock, marketsDataMock, oracleDataMock]);
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
const expectedHeaders = [
|
||||
@@ -200,6 +203,10 @@ describe('Closed', () => {
|
||||
];
|
||||
expect(headers).toHaveLength(expectedHeaders.length);
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
|
||||
});
|
||||
|
||||
it('renders correctly formatted and filtered rows', async () => {
|
||||
await renderComponent([marketsMock, marketsDataMock, oracleDataMock]);
|
||||
|
||||
const assetSymbol = getAsset(market).symbol;
|
||||
|
||||
@@ -273,21 +280,8 @@ describe('Closed', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[mixedMarketsMock, marketsDataMock, oracleDataMock]}
|
||||
>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
<Closed />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
|
||||
await renderComponent([mixedMarketsMock, marketsDataMock, oracleDataMock]);
|
||||
|
||||
// check that the number of rows in datagrid is 2
|
||||
const container = within(
|
||||
@@ -319,8 +313,67 @@ describe('Closed', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('successor marked should be visible', async () => {
|
||||
it('display market actions', async () => {
|
||||
// Use market with a succcessor Id as the actions dropdown will optionally
|
||||
// show a link to the successor market
|
||||
const marketsWithSuccessorAndParent = [
|
||||
{
|
||||
__typename: 'MarketEdge' as const,
|
||||
node: createMarketFragment({
|
||||
id: 'include-0',
|
||||
state: MarketState.STATE_SETTLED,
|
||||
successorMarketID: 'successor',
|
||||
parentMarketID: 'parent',
|
||||
}),
|
||||
},
|
||||
];
|
||||
const mockWithSuccessorAndParent: MockedResponse<MarketsQuery> = {
|
||||
request: {
|
||||
query: MarketsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
__typename: 'MarketConnection',
|
||||
edges: marketsWithSuccessorAndParent,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await renderComponent([
|
||||
mockWithSuccessorAndParent,
|
||||
marketsDataMock,
|
||||
oracleDataMock,
|
||||
]);
|
||||
|
||||
const actionCell = screen
|
||||
.getAllByRole('gridcell')
|
||||
.find((el) => el.getAttribute('col-id') === 'market-actions');
|
||||
|
||||
await userEvent.click(
|
||||
within(actionCell as HTMLElement).getByTestId('dropdown-menu')
|
||||
);
|
||||
|
||||
expect(screen.getByRole('menu')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'Copy Market ID' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'View on Explorer' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'View settlement asset details' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'View parent market' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'View successor market' })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('successor market should be visible', async () => {
|
||||
const marketsWithSuccessorID = [
|
||||
{
|
||||
__typename: 'MarketEdge' as const,
|
||||
@@ -345,21 +398,11 @@ describe('Closed', () => {
|
||||
},
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[mockWithSuccessors, marketsDataMock, oracleDataMock]}
|
||||
>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
<Closed />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
await renderComponent([
|
||||
mockWithSuccessors,
|
||||
marketsDataMock,
|
||||
oracleDataMock,
|
||||
]);
|
||||
|
||||
const container = within(
|
||||
document.querySelector('.ag-center-cols-container') as HTMLElement
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { act, render, screen, within } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { OpenMarkets } from './open-markets';
|
||||
import { Interval } from '@vegaprotocol/types';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type {
|
||||
MarketsDataQuery,
|
||||
MarketsQuery,
|
||||
MarketCandlesQuery,
|
||||
MarketFieldsFragment,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
MarketsDataDocument,
|
||||
MarketsDocument,
|
||||
MarketsCandlesDocument,
|
||||
} from '@vegaprotocol/markets';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
marketsQuery,
|
||||
marketsDataQuery,
|
||||
marketsCandlesQuery,
|
||||
} from '@vegaprotocol/mock';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('Open', () => {
|
||||
let originalNow: typeof Date.now;
|
||||
const mockNowTimestamp = 1672531200000;
|
||||
const pubKey = 'pubKey';
|
||||
|
||||
const marketsQueryData = marketsQuery();
|
||||
const marketsMock: MockedResponse<MarketsQuery> = {
|
||||
request: {
|
||||
query: MarketsDocument,
|
||||
},
|
||||
result: {
|
||||
data: marketsQueryData,
|
||||
},
|
||||
};
|
||||
|
||||
const marketsCandlesQueryData = marketsCandlesQuery();
|
||||
const marketsCandlesMock: MockedResponse<MarketCandlesQuery> = {
|
||||
request: {
|
||||
query: MarketsCandlesDocument,
|
||||
variables: {
|
||||
interval: Interval.INTERVAL_I1H,
|
||||
since: '2022-12-31T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: marketsCandlesQueryData,
|
||||
},
|
||||
};
|
||||
|
||||
const marketsDataQueryData = marketsDataQuery();
|
||||
|
||||
const marketsDataMock: MockedResponse<MarketsDataQuery> = {
|
||||
request: {
|
||||
query: MarketsDataDocument,
|
||||
},
|
||||
result: {
|
||||
data: marketsDataQueryData,
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
originalNow = Date.now;
|
||||
Date.now = jest.fn().mockReturnValue(mockNowTimestamp);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Date.now = originalNow;
|
||||
});
|
||||
|
||||
const renderComponent = async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[marketsMock, marketsCandlesMock, marketsDataMock]}
|
||||
>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
<OpenMarkets />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
it('renders correct headers', async () => {
|
||||
await renderComponent();
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
const expectedHeaders = [
|
||||
'Market',
|
||||
'Description',
|
||||
'Settlement asset',
|
||||
'Trading mode',
|
||||
'Status',
|
||||
'Mark price',
|
||||
'24h volume',
|
||||
'Open Interest',
|
||||
'Spread',
|
||||
'', // Action row
|
||||
];
|
||||
expect(headers).toHaveLength(expectedHeaders.length);
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
|
||||
});
|
||||
|
||||
it('sort columns', async () => {
|
||||
await renderComponent();
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
const marketHeader = headers.find(
|
||||
(h) => h.getAttribute('col-id') === 'tradableInstrument.instrument.code'
|
||||
);
|
||||
if (!marketHeader) {
|
||||
throw new Error('No market header found');
|
||||
}
|
||||
expect(marketHeader).toHaveAttribute('aria-sort', 'none');
|
||||
await userEvent.click(within(marketHeader).getByText(/market/i));
|
||||
// 6001-MARK-064
|
||||
expect(marketHeader).toHaveAttribute('aria-sort', 'ascending');
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests, jest/expect-expect
|
||||
it('renders row', async () => {
|
||||
await renderComponent();
|
||||
|
||||
const container = within(
|
||||
document.querySelector('.ag-center-cols-container') as HTMLElement
|
||||
);
|
||||
|
||||
const markets = marketsQueryData.marketsConnection?.edges.map(
|
||||
(e) => e.node
|
||||
) as MarketFieldsFragment[];
|
||||
|
||||
const rows = container.getAllByRole('row');
|
||||
expect(rows).toHaveLength(markets.length);
|
||||
});
|
||||
});
|
||||
@@ -43,7 +43,7 @@ export const OpenMarkets = () => {
|
||||
if (!data) return;
|
||||
|
||||
// prevent navigating to the market page if any of the below cells are clicked
|
||||
// event.preventDefault or event.stopPropagation dont seem to apply for aggird
|
||||
// event.preventDefault or event.stopPropagation do not seem to apply for ag-grid
|
||||
const colId = column.getColId();
|
||||
|
||||
if (
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useForm } from 'react-hook-form';
|
||||
import classNames from 'classnames';
|
||||
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { RainbowButton } from './buttons';
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { useReferral } from './hooks/use-referral';
|
||||
@@ -32,6 +32,19 @@ const validateCode = (value: string, t: ReturnType<typeof useT>) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const ApplyCodeFormContainer = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { data: referee } = useReferral({ pubKey, role: 'referee' });
|
||||
const { data: referrer } = useReferral({ pubKey, role: 'referrer' });
|
||||
|
||||
// go to main page if the current pubkey is already a referrer or referee
|
||||
if (referee || referrer) {
|
||||
return <Navigate to={Routes.REFERRALS} />;
|
||||
}
|
||||
|
||||
return <ApplyCodeForm />;
|
||||
};
|
||||
|
||||
export const ApplyCodeForm = () => {
|
||||
const t = useT();
|
||||
const program = useReferralProgram();
|
||||
@@ -55,14 +68,29 @@ export const ApplyCodeForm = () => {
|
||||
} = useForm();
|
||||
const [params] = useSearchParams();
|
||||
|
||||
const { data: referee } = useReferral({ pubKey, role: 'referee' });
|
||||
const { data: referrer } = useReferral({ pubKey, role: 'referrer' });
|
||||
|
||||
const codeField = watch('code');
|
||||
const { data: previewData, loading: previewLoading } = useReferral({
|
||||
code: validateCode(codeField, t) ? codeField : undefined,
|
||||
});
|
||||
|
||||
/**
|
||||
* Validates the set a user tries to apply to.
|
||||
*/
|
||||
const validateSet = useCallback(() => {
|
||||
if (
|
||||
codeField &&
|
||||
!previewLoading &&
|
||||
previewData &&
|
||||
!previewData.isEligible
|
||||
) {
|
||||
return t('The code is no longer valid.');
|
||||
}
|
||||
if (codeField && !previewLoading && !previewData) {
|
||||
return t('The code is invalid');
|
||||
}
|
||||
return true;
|
||||
}, [codeField, previewData, previewLoading, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const code = params.get('code');
|
||||
if (code) setValue('code', code);
|
||||
@@ -144,11 +172,6 @@ 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 (
|
||||
@@ -205,7 +228,10 @@ export const ApplyCodeForm = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto w-2/3 max-w-md rounded-lg p-8">
|
||||
<div
|
||||
data-testid="referral-apply-code-form"
|
||||
className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto w-2/3 max-w-md rounded-lg p-8"
|
||||
>
|
||||
<h3 className="calt mb-4 text-center text-2xl">
|
||||
{t('Apply a referral code')}
|
||||
</h3>
|
||||
@@ -224,7 +250,11 @@ export const ApplyCodeForm = () => {
|
||||
hasError={Boolean(errors.code)}
|
||||
{...register('code', {
|
||||
required: t('You have to provide a code to apply it.'),
|
||||
validate: (value) => validateCode(value, t),
|
||||
validate: (value) => {
|
||||
const err = validateCode(value, t);
|
||||
if (err !== true) return err;
|
||||
return validateSet();
|
||||
},
|
||||
})}
|
||||
placeholder="Enter a code"
|
||||
className="bg-vega-clight-900 dark:bg-vega-cdark-700 mb-2"
|
||||
@@ -238,15 +268,17 @@ export const ApplyCodeForm = () => {
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
{previewLoading && !previewData ? (
|
||||
{validateCode(codeField, t) === true && previewLoading && !previewData ? (
|
||||
<div className="mt-10">
|
||||
<Loader />
|
||||
</div>
|
||||
) : null}
|
||||
{previewData ? (
|
||||
{/* TODO: Re-check plural forms once i18n is updated */}
|
||||
{previewData && previewData.isEligible ? (
|
||||
<div className="mt-10">
|
||||
<h2 className="mb-5 text-2xl">
|
||||
{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 }
|
||||
)}
|
||||
|
||||
@@ -39,7 +39,10 @@ export const CreateCodeForm = () => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
|
||||
return (
|
||||
<div className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg">
|
||||
<div
|
||||
data-testid="referral-create-code-form"
|
||||
className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg"
|
||||
>
|
||||
<h3 className="mb-4 text-2xl text-center calt">
|
||||
{t('Create a referral code')}
|
||||
</h3>
|
||||
|
||||
@@ -11,6 +11,7 @@ query ReferralSetStats($code: ID!, $epoch: Int) {
|
||||
rewardsMultiplier
|
||||
rewardsFactorMultiplier
|
||||
referrerTakerVolume
|
||||
wasEligible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
query StakeAvailable($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
stakingSummary {
|
||||
currentStakeAvailable
|
||||
}
|
||||
}
|
||||
networkParameter(key: "referralProgram.minStakedVegaTokens") {
|
||||
value
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -9,7 +9,7 @@ export type ReferralSetStatsQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string, referrerTakerVolume: string } } | null> } };
|
||||
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string, referrerTakerVolume: string, wasEligible: boolean } } | null> } };
|
||||
|
||||
|
||||
export const ReferralSetStatsDocument = gql`
|
||||
@@ -26,6 +26,7 @@ export const ReferralSetStatsDocument = gql`
|
||||
rewardsMultiplier
|
||||
rewardsFactorMultiplier
|
||||
referrerTakerVolume
|
||||
wasEligible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type StakeAvailableQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type StakeAvailableQuery = { __typename?: 'Query', party?: { __typename?: 'Party', stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string } } | null, networkParameter?: { __typename?: 'NetworkParameter', value: string } | null };
|
||||
|
||||
|
||||
export const StakeAvailableDocument = gql`
|
||||
query StakeAvailable($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
stakingSummary {
|
||||
currentStakeAvailable
|
||||
}
|
||||
}
|
||||
networkParameter(key: "referralProgram.minStakedVegaTokens") {
|
||||
value
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useStakeAvailableQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useStakeAvailableQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useStakeAvailableQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useStakeAvailableQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useStakeAvailableQuery(baseOptions: Apollo.QueryHookOptions<StakeAvailableQuery, StakeAvailableQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<StakeAvailableQuery, StakeAvailableQueryVariables>(StakeAvailableDocument, options);
|
||||
}
|
||||
export function useStakeAvailableLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<StakeAvailableQuery, StakeAvailableQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<StakeAvailableQuery, StakeAvailableQueryVariables>(StakeAvailableDocument, options);
|
||||
}
|
||||
export type StakeAvailableQueryHookResult = ReturnType<typeof useStakeAvailableQuery>;
|
||||
export type StakeAvailableLazyQueryHookResult = ReturnType<typeof useStakeAvailableLazyQuery>;
|
||||
export type StakeAvailableQueryResult = Apollo.QueryResult<StakeAvailableQuery, StakeAvailableQueryVariables>;
|
||||
@@ -75,22 +75,20 @@ export const useReferralProgram = () => {
|
||||
|
||||
const benefitTiers = sortBy(data.currentReferralProgram.benefitTiers, (t) =>
|
||||
Number(t.referralRewardFactor)
|
||||
)
|
||||
.reverse()
|
||||
.map((t, i) => {
|
||||
return {
|
||||
tier: i + 1,
|
||||
rewardFactor: Number(t.referralRewardFactor),
|
||||
commission: Number(t.referralRewardFactor) * 100 + '%',
|
||||
discountFactor: Number(t.referralDiscountFactor),
|
||||
discount: Number(t.referralDiscountFactor) * 100 + '%',
|
||||
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
|
||||
volume: getNumberFormat(0).format(
|
||||
Number(t.minimumRunningNotionalTakerVolume)
|
||||
),
|
||||
epochs: Number(t.minimumEpochs),
|
||||
};
|
||||
});
|
||||
).map((t, i) => {
|
||||
return {
|
||||
tier: i + 1, // sorted in asc order, hence first is the lowest tier
|
||||
rewardFactor: Number(t.referralRewardFactor),
|
||||
commission: Number(t.referralRewardFactor) * 100 + '%',
|
||||
discountFactor: Number(t.referralDiscountFactor),
|
||||
discount: Number(t.referralDiscountFactor) * 100 + '%',
|
||||
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
|
||||
volume: getNumberFormat(0).format(
|
||||
Number(t.minimumRunningNotionalTakerVolume)
|
||||
),
|
||||
epochs: Number(t.minimumEpochs),
|
||||
};
|
||||
});
|
||||
|
||||
const stakingTiers = sortBy(
|
||||
data.currentReferralProgram.stakingTiers,
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
Intent,
|
||||
type Toast,
|
||||
useToasts,
|
||||
ToastHeading,
|
||||
Button,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useReferral } from './use-referral';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useEffect } from 'react';
|
||||
import { useT } from '../../../lib/use-t';
|
||||
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Routes } from '../../../lib/links';
|
||||
import { useCurrentEpochInfoQuery } from './__generated__/Epoch';
|
||||
|
||||
const REFETCH_INTERVAL = 60 * 60 * 1000; // 1h
|
||||
const NON_ELIGIBLE_REFERRAL_SET_TOAST_ID = 'non-eligible-referral-set';
|
||||
|
||||
const useNonEligibleReferralSet = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { data, loading, refetch } = useReferral({ pubKey, role: 'referee' });
|
||||
const {
|
||||
data: epochData,
|
||||
loading: epochLoading,
|
||||
refetch: epochRefetch,
|
||||
} = useCurrentEpochInfoQuery();
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
refetch();
|
||||
epochRefetch();
|
||||
}, REFETCH_INTERVAL);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [epochRefetch, refetch]);
|
||||
|
||||
return { data, epoch: epochData?.epoch.id, loading: loading || epochLoading };
|
||||
};
|
||||
|
||||
export const useReferralToasts = () => {
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const t = useT();
|
||||
const [setToast, hasToast, updateToast] = useToasts((store) => [
|
||||
store.setToast,
|
||||
store.hasToast,
|
||||
store.update,
|
||||
]);
|
||||
|
||||
const { data, epoch, loading } = useNonEligibleReferralSet();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
data &&
|
||||
epoch &&
|
||||
!loading &&
|
||||
!data.isEligible &&
|
||||
!hasToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch)
|
||||
) {
|
||||
const nonEligibleReferralToast: Toast = {
|
||||
id: NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch,
|
||||
intent: Intent.Warning,
|
||||
content: (
|
||||
<>
|
||||
<ToastHeading>{t('Referral code no longer valid')}</ToastHeading>
|
||||
<p>
|
||||
{t(
|
||||
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements.'
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
<Button
|
||||
data-testid="toast-apply-code"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
const matched = matchPath(
|
||||
Routes.REFERRALS_APPLY_CODE,
|
||||
pathname
|
||||
);
|
||||
if (!matched) navigate(Routes.REFERRALS_APPLY_CODE);
|
||||
updateToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch, {
|
||||
hidden: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Apply a new code')}
|
||||
</Button>
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
onClose: () =>
|
||||
updateToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch, {
|
||||
hidden: true,
|
||||
}),
|
||||
};
|
||||
setToast(nonEligibleReferralToast);
|
||||
}
|
||||
}, [
|
||||
data,
|
||||
epoch,
|
||||
hasToast,
|
||||
loading,
|
||||
navigate,
|
||||
pathname,
|
||||
setToast,
|
||||
t,
|
||||
updateToast,
|
||||
]);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { useRefereesQuery } from './__generated__/Referees';
|
||||
import compact from 'lodash/compact';
|
||||
import type { ReferralSetsQueryVariables } from './__generated__/ReferralSets';
|
||||
import { useReferralSetsQuery } from './__generated__/ReferralSets';
|
||||
import { useStakeAvailable } from './use-stake-available';
|
||||
|
||||
export const DEFAULT_AGGREGATION_DAYS = 30;
|
||||
|
||||
@@ -62,6 +63,8 @@ export const useReferral = (args: UseReferralArgs) => {
|
||||
? referralData.referralSets.edges[0]?.node
|
||||
: undefined;
|
||||
|
||||
const { isEligible } = useStakeAvailable(referralSet?.referrer);
|
||||
|
||||
const {
|
||||
data: refereesData,
|
||||
loading: refereesLoading,
|
||||
@@ -103,6 +106,7 @@ export const useReferral = (args: UseReferralArgs) => {
|
||||
referee: referee,
|
||||
referrerId: referralSet.referrer,
|
||||
createdAt: referralSet.createdAt,
|
||||
isEligible,
|
||||
referees,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useStakeAvailableQuery } from './__generated__/StakeAvailable';
|
||||
|
||||
const STAKE_QUERY = gql`
|
||||
query CreateCode($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
stakingSummary {
|
||||
currentStakeAvailable
|
||||
}
|
||||
}
|
||||
networkParameter(key: "referralProgram.minStakedVegaTokens") {
|
||||
value
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const useStakeAvailable = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { data } = useQuery(STAKE_QUERY, {
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
/**
|
||||
* Gets the current stake available for given public key and required stake for
|
||||
* the referral program.
|
||||
*
|
||||
* (Uses currently connected public key if left empty)
|
||||
*/
|
||||
export const useStakeAvailable = (pubKey?: string) => {
|
||||
const { pubKey: currentPubKey } = useVegaWallet();
|
||||
const partyId = pubKey || currentPubKey;
|
||||
const { data } = useStakeAvailableQuery({
|
||||
variables: { partyId: partyId || '' },
|
||||
skip: !partyId,
|
||||
// TODO: remove when network params available
|
||||
errorPolicy: 'ignore',
|
||||
});
|
||||
|
||||
const stakeAvailable = data
|
||||
? BigInt(data.party?.stakingSummary.currentStakeAvailable || '0')
|
||||
: undefined;
|
||||
const requiredStake = data
|
||||
? BigInt(data.networkParameter?.value || '0')
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
stakeAvailable: data
|
||||
? BigInt(data.party?.stakingSummary.currentStakeAvailable || '0')
|
||||
: undefined,
|
||||
requiredStake: data
|
||||
? BigInt(data.networkParameter?.value || '0')
|
||||
: undefined,
|
||||
stakeAvailable,
|
||||
requiredStake,
|
||||
isEligible:
|
||||
stakeAvailable != null &&
|
||||
requiredStake != null &&
|
||||
stakeAvailable >= requiredStake,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { type VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { ReferralStatistics } from './referral-statistics';
|
||||
import {
|
||||
ReferralProgramDocument,
|
||||
type ReferralProgramQuery,
|
||||
} from './hooks/__generated__/CurrentReferralProgram';
|
||||
import {
|
||||
ReferralSetsDocument,
|
||||
type ReferralSetsQueryVariables,
|
||||
type ReferralSetsQuery,
|
||||
} from './hooks/__generated__/ReferralSets';
|
||||
import {
|
||||
StakeAvailableDocument,
|
||||
type StakeAvailableQueryVariables,
|
||||
type StakeAvailableQuery,
|
||||
} from './hooks/__generated__/StakeAvailable';
|
||||
import {
|
||||
RefereesDocument,
|
||||
type RefereesQueryVariables,
|
||||
type RefereesQuery,
|
||||
} from './hooks/__generated__/Referees';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
const MOCK_PUBKEY =
|
||||
'1234567890123456789012345678901234567890123456789012345678901234';
|
||||
|
||||
const MOCK_STAKE_AVAILABLE: StakeAvailableQuery = {
|
||||
networkParameter: {
|
||||
__typename: 'NetworkParameter',
|
||||
value: '1',
|
||||
},
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
stakingSummary: {
|
||||
__typename: 'StakingSummary',
|
||||
currentStakeAvailable: '1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const MOCK_NON_ELIGIBILE_STAKE_AVAILABLE: StakeAvailableQuery = {
|
||||
networkParameter: {
|
||||
__typename: 'NetworkParameter',
|
||||
value: '1',
|
||||
},
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
stakingSummary: {
|
||||
__typename: 'StakingSummary',
|
||||
currentStakeAvailable: '0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const MOCK_REFERRAL_PROGRAM: ReferralProgramQuery = {
|
||||
currentReferralProgram: {
|
||||
__typename: 'CurrentReferralProgram',
|
||||
benefitTiers: [
|
||||
{
|
||||
__typename: 'BenefitTier',
|
||||
minimumEpochs: 1,
|
||||
minimumRunningNotionalTakerVolume: '0',
|
||||
referralDiscountFactor: '0.01',
|
||||
referralRewardFactor: '0.01',
|
||||
},
|
||||
{
|
||||
__typename: 'BenefitTier',
|
||||
minimumEpochs: 2,
|
||||
minimumRunningNotionalTakerVolume: '10',
|
||||
referralDiscountFactor: '0.02',
|
||||
referralRewardFactor: '0.02',
|
||||
},
|
||||
],
|
||||
endOfProgramTimestamp: '202411012023-11-26T05:58:24.045158Z',
|
||||
id: '123',
|
||||
stakingTiers: [
|
||||
{
|
||||
__typename: 'StakingTier',
|
||||
minimumStakedTokens: '100',
|
||||
referralRewardMultiplier: '1',
|
||||
},
|
||||
{
|
||||
__typename: 'StakingTier',
|
||||
minimumStakedTokens: '1000',
|
||||
referralRewardMultiplier: '2',
|
||||
},
|
||||
],
|
||||
version: 2,
|
||||
windowLength: 3,
|
||||
endedAt: null,
|
||||
},
|
||||
};
|
||||
|
||||
const MOCK_REFERRER_SET: ReferralSetsQuery = {
|
||||
referralSets: {
|
||||
__typename: 'ReferralSetConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetEdge',
|
||||
node: {
|
||||
__typename: 'ReferralSet',
|
||||
createdAt: '2023-11-26T05:58:24.045158Z',
|
||||
id: '3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
|
||||
referrer: MOCK_PUBKEY,
|
||||
updatedAt: '2023-11-26T05:58:24.045158Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const MOCK_REFERREE_SET: ReferralSetsQuery = {
|
||||
referralSets: {
|
||||
__typename: 'ReferralSetConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetEdge',
|
||||
node: {
|
||||
__typename: 'ReferralSet',
|
||||
createdAt: '2023-11-26T05:58:24.045158Z',
|
||||
id: '3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
|
||||
referrer:
|
||||
'1111111111111111111111111111111111111111111111111111111111111111',
|
||||
updatedAt: '2023-11-26T05:58:24.045158Z',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const MOCK_REFEREES: RefereesQuery = {
|
||||
referralSetReferees: {
|
||||
__typename: 'ReferralSetRefereeConnection',
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 1,
|
||||
joinedAt: '2023-11-21T14:17:09.257235Z',
|
||||
refereeId:
|
||||
'0987654321098765432109876543210987654321098765432109876543219876',
|
||||
referralSetId:
|
||||
'3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
|
||||
totalRefereeGeneratedRewards: '1234',
|
||||
totalRefereeNotionalTakerVolume: '5678',
|
||||
__typename: 'ReferralSetReferee',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const programMock: MockedResponse<ReferralProgramQuery> = {
|
||||
request: {
|
||||
query: ReferralProgramDocument,
|
||||
},
|
||||
result: { data: MOCK_REFERRAL_PROGRAM },
|
||||
};
|
||||
|
||||
const referralSetAsReferrerMock: MockedResponse<
|
||||
ReferralSetsQuery,
|
||||
ReferralSetsQueryVariables
|
||||
> = {
|
||||
request: {
|
||||
query: ReferralSetsDocument,
|
||||
variables: {
|
||||
referrer: MOCK_PUBKEY,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: MOCK_REFERRER_SET,
|
||||
},
|
||||
};
|
||||
|
||||
const noReferralSetAsReferrerMock: MockedResponse<
|
||||
ReferralSetsQuery,
|
||||
ReferralSetsQueryVariables
|
||||
> = {
|
||||
request: {
|
||||
query: ReferralSetsDocument,
|
||||
variables: {
|
||||
referrer: MOCK_PUBKEY,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: { referralSets: { edges: [] } },
|
||||
},
|
||||
};
|
||||
|
||||
const referralSetAsRefereeMock: MockedResponse<
|
||||
ReferralSetsQuery,
|
||||
ReferralSetsQueryVariables
|
||||
> = {
|
||||
request: {
|
||||
query: ReferralSetsDocument,
|
||||
variables: {
|
||||
referee: MOCK_PUBKEY,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: MOCK_REFERREE_SET,
|
||||
},
|
||||
};
|
||||
|
||||
const noReferralSetAsRefereeMock: MockedResponse<
|
||||
ReferralSetsQuery,
|
||||
ReferralSetsQueryVariables
|
||||
> = {
|
||||
request: {
|
||||
query: ReferralSetsDocument,
|
||||
variables: {
|
||||
referee: MOCK_PUBKEY,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: { referralSets: { edges: [] } },
|
||||
},
|
||||
};
|
||||
|
||||
const stakeAvailableMock: MockedResponse<
|
||||
StakeAvailableQuery,
|
||||
StakeAvailableQueryVariables
|
||||
> = {
|
||||
request: {
|
||||
query: StakeAvailableDocument,
|
||||
variables: {
|
||||
partyId: MOCK_PUBKEY,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: MOCK_STAKE_AVAILABLE,
|
||||
},
|
||||
};
|
||||
|
||||
const nonEligibleStakeAvailableMock: MockedResponse<
|
||||
StakeAvailableQuery,
|
||||
StakeAvailableQueryVariables
|
||||
> = {
|
||||
request: {
|
||||
query: StakeAvailableDocument,
|
||||
variables: {
|
||||
partyId: MOCK_PUBKEY,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: MOCK_NON_ELIGIBILE_STAKE_AVAILABLE,
|
||||
},
|
||||
};
|
||||
|
||||
const refereesMock: MockedResponse<RefereesQuery, RefereesQueryVariables> = {
|
||||
request: {
|
||||
query: RefereesDocument,
|
||||
variables: {
|
||||
code: MOCK_REFERRER_SET.referralSets.edges[0]?.node.id as string,
|
||||
aggregationEpochs:
|
||||
MOCK_REFERRAL_PROGRAM.currentReferralProgram?.windowLength,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: MOCK_REFEREES,
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('@vegaprotocol/wallet', () => {
|
||||
return {
|
||||
...jest.requireActual('@vegaprotocol/wallet'),
|
||||
useVegaWallet: () => {
|
||||
const ctx: Partial<VegaWalletContextShape> = {
|
||||
pubKey: MOCK_PUBKEY,
|
||||
};
|
||||
return ctx;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('ReferralStatistics', () => {
|
||||
it('displays create code when no data has been found for given pubkey', () => {
|
||||
const { queryByTestId } = render(
|
||||
<MockedProvider mocks={[]} showWarnings={false}>
|
||||
<ReferralStatistics />
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(queryByTestId('referral-create-code-form')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays referrer stats when given pubkey is a referrer', async () => {
|
||||
const { queryByTestId } = render(
|
||||
<MockedProvider
|
||||
mocks={[
|
||||
programMock,
|
||||
referralSetAsReferrerMock,
|
||||
noReferralSetAsRefereeMock,
|
||||
stakeAvailableMock,
|
||||
refereesMock,
|
||||
]}
|
||||
showWarnings={false}
|
||||
>
|
||||
<ReferralStatistics />
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
queryByTestId('referral-create-code-form')
|
||||
).not.toBeInTheDocument();
|
||||
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
|
||||
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
|
||||
'referrer'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays referee stats when given pubkey is a referee', async () => {
|
||||
const { queryByTestId } = render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[
|
||||
programMock,
|
||||
noReferralSetAsReferrerMock,
|
||||
referralSetAsRefereeMock,
|
||||
stakeAvailableMock,
|
||||
refereesMock,
|
||||
]}
|
||||
showWarnings={false}
|
||||
>
|
||||
<ReferralStatistics />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
queryByTestId('referral-create-code-form')
|
||||
).not.toBeInTheDocument();
|
||||
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
|
||||
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
|
||||
'referee'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays eligibility warning when the set is no longer valid due to the referrers stake', async () => {
|
||||
const { queryByTestId } = render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[
|
||||
programMock,
|
||||
noReferralSetAsReferrerMock,
|
||||
referralSetAsRefereeMock,
|
||||
nonEligibleStakeAvailableMock,
|
||||
refereesMock,
|
||||
]}
|
||||
showWarnings={false}
|
||||
>
|
||||
<ReferralStatistics />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
queryByTestId('referral-create-code-form')
|
||||
).not.toBeInTheDocument();
|
||||
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
|
||||
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
|
||||
'referee'
|
||||
);
|
||||
expect(queryByTestId('referral-eligibility-warning')).toBeInTheDocument();
|
||||
expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import minBy from 'lodash/minBy';
|
||||
import { CodeTile, StatTile } from './tile';
|
||||
import {
|
||||
VegaIcon,
|
||||
@@ -28,10 +29,10 @@ import sortBy from 'lodash/sortBy';
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { useT, ns } from '../../lib/use-t';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { ApplyCodeForm } from './apply-code-form';
|
||||
|
||||
export const ReferralStatistics = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
@@ -50,11 +51,21 @@ export const ReferralStatistics = () => {
|
||||
});
|
||||
|
||||
if (referee?.code) {
|
||||
return <Statistics data={referee} program={program} as="referee" />;
|
||||
return (
|
||||
<>
|
||||
<Statistics data={referee} program={program} as="referee" />;
|
||||
{!referee.isEligible && <ApplyCodeForm />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (referrer?.code) {
|
||||
return <Statistics data={referrer} program={program} as="referrer" />;
|
||||
return (
|
||||
<>
|
||||
<Statistics data={referrer} program={program} as="referrer" />;
|
||||
<RefereesTable data={referrer} program={program} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <CreateCodeContainer />;
|
||||
@@ -116,8 +127,8 @@ export const useStats = ({
|
||||
t.discountFactor === discountFactorValue
|
||||
);
|
||||
const nextBenefitTierValue = currentBenefitTierValue
|
||||
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1)
|
||||
: maxBy(benefitTiers, (bt) => bt.tier); // max tier number is lowest tier
|
||||
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier + 1)
|
||||
: minBy(benefitTiers, (bt) => bt.tier); // min tier number is lowest tier
|
||||
const epochsValue =
|
||||
!isNaN(currentEpoch) && refereeInfo?.atEpoch
|
||||
? currentEpoch - refereeInfo?.atEpoch
|
||||
@@ -174,7 +185,7 @@ export const Statistics = ({
|
||||
|
||||
const { benefitTiers } = useReferralProgram();
|
||||
|
||||
const { stakeAvailable } = useStakeAvailable();
|
||||
const { stakeAvailable, isEligible } = useStakeAvailable();
|
||||
const { details } = program;
|
||||
|
||||
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
||||
@@ -200,12 +211,24 @@ export const Statistics = ({
|
||||
{baseCommissionValue * 100}%
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
const stakingMultiplierTile = (
|
||||
<StatTile
|
||||
title={t('Staking multiplier')}
|
||||
description={t('{{amount}} $VEGA staked', {
|
||||
amount: addDecimalsFormatNumber(stakeAvailable?.toString() || 0, 18),
|
||||
})}
|
||||
description={
|
||||
<span
|
||||
className={classNames({
|
||||
'text-vega-red': !isEligible,
|
||||
})}
|
||||
>
|
||||
{t('{{amount}} $VEGA staked', {
|
||||
amount: addDecimalsFormatNumber(
|
||||
stakeAvailable?.toString() || 0,
|
||||
18
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{multiplier || t('None')}
|
||||
</StatTile>
|
||||
@@ -238,7 +261,7 @@ export const Statistics = ({
|
||||
|
||||
const referrerVolumeTile = (
|
||||
<StatTile
|
||||
title={t('My volume (last {{count}} epochs)', {
|
||||
title={t('myVolume', 'My volume (last {{count}} epochs)', {
|
||||
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
||||
})}
|
||||
>
|
||||
@@ -251,7 +274,7 @@ export const Statistics = ({
|
||||
.reduce((all, r) => all.plus(r), new BigNumber(0));
|
||||
const totalCommissionTile = (
|
||||
<StatTile
|
||||
title={t('Total commission (last {{count}}} epochs)', {
|
||||
title={t('totalCommission', 'Total commission (last {{count}}} epochs)', {
|
||||
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
||||
})}
|
||||
description={<QUSDTooltip />}
|
||||
@@ -294,9 +317,13 @@ export const Statistics = ({
|
||||
);
|
||||
const runningVolumeTile = (
|
||||
<StatTile
|
||||
title={t('Combined volume (last {{count}} epochs)', {
|
||||
count: details?.windowLength,
|
||||
})}
|
||||
title={t(
|
||||
'runningNotionalOverEpochs',
|
||||
'Combined volume (last {{count}} epochs)',
|
||||
{
|
||||
count: details?.windowLength,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{compactNumFormat.format(runningVolumeValue)}
|
||||
</StatTile>
|
||||
@@ -333,28 +360,60 @@ export const Statistics = ({
|
||||
</>
|
||||
);
|
||||
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
useLayoutEffect(() => {
|
||||
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
|
||||
setCollapsed(true);
|
||||
}
|
||||
}, []);
|
||||
const eligibilityWarning = as === 'referee' && !isEligible && (
|
||||
<div
|
||||
data-testid="referral-eligibility-warning"
|
||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
|
||||
>
|
||||
<h2 className="text-2xl mb-2">{t('Referral code no longer valid')}</h2>
|
||||
<p>
|
||||
{t(
|
||||
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Stats tiles */}
|
||||
<div
|
||||
data-testid="referral-statistics"
|
||||
data-as={as}
|
||||
className="relative mx-auto mb-20"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'grid grid-cols-1 grid-rows-1 gap-5 mx-auto mb-20'
|
||||
)}
|
||||
className={classNames('grid grid-cols-1 grid-rows-1 gap-5', {
|
||||
'opacity-20 pointer-events-none': as === 'referee' && !isEligible,
|
||||
})}
|
||||
>
|
||||
{as === 'referrer' && referrerTiles}
|
||||
{as === 'referee' && refereeTiles}
|
||||
</div>
|
||||
|
||||
{eligibilityWarning}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const RefereesTable = ({
|
||||
data,
|
||||
program,
|
||||
}: {
|
||||
data: NonNullable<ReturnType<typeof useReferral>['data']>;
|
||||
program: ReturnType<typeof useReferralProgram>;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
const { details } = program;
|
||||
useLayoutEffect(() => {
|
||||
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
|
||||
setCollapsed(true);
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
{/* Referees (only for referrer view) */}
|
||||
{as === 'referrer' && data.referees.length > 0 && (
|
||||
{data.referees.length > 0 && (
|
||||
<div className="mt-20 mb-20">
|
||||
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
|
||||
<div
|
||||
@@ -384,15 +443,19 @@ export const Statistics = ({
|
||||
{ name: 'joined', displayName: t('Date Joined') },
|
||||
{
|
||||
name: 'volume',
|
||||
displayName: t('Volume (last {{count}} epochs)', {
|
||||
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
||||
}),
|
||||
displayName: t(
|
||||
'volumeLastEpochs',
|
||||
'Volume (last {{count}} epochs)',
|
||||
{
|
||||
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'commission',
|
||||
displayName: (
|
||||
<Trans
|
||||
i18nKey="referral-statistics-commission"
|
||||
i18nKey="referralStatisticsCommission"
|
||||
defaults="Commission earned in <0>qUSD</0> (last {{count}} epochs)"
|
||||
values={{
|
||||
count:
|
||||
|
||||
@@ -208,9 +208,13 @@ const TiersTable = ({
|
||||
{ name: 'discount', displayName: t('Referrer trading discount') },
|
||||
{
|
||||
name: 'volume',
|
||||
displayName: t('Min. trading volume (last {{count}} epochs)', {
|
||||
count: windowLength,
|
||||
}),
|
||||
displayName: t(
|
||||
'minTradingVolume',
|
||||
'Min. trading volume (last {{count}} epochs)',
|
||||
{
|
||||
count: windowLength,
|
||||
}
|
||||
),
|
||||
},
|
||||
{ name: 'epochs', displayName: t('Min. epochs') },
|
||||
]}
|
||||
@@ -218,13 +222,13 @@ const TiersTable = ({
|
||||
...d,
|
||||
className: classNames({
|
||||
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
|
||||
d.tier === 1,
|
||||
d.tier >= 3,
|
||||
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
|
||||
d.tier === 2,
|
||||
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
|
||||
d.tier === 3,
|
||||
d.tier === 1,
|
||||
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
|
||||
d.tier > 3,
|
||||
d.tier == 0,
|
||||
}),
|
||||
}))}
|
||||
/>
|
||||
|
||||
@@ -16,18 +16,11 @@ query DiscountPrograms {
|
||||
}
|
||||
}
|
||||
|
||||
query Fees(
|
||||
$partyId: ID!
|
||||
$volumeDiscountEpochs: Int!
|
||||
$referralDiscountEpochs: Int!
|
||||
) {
|
||||
query Fees($partyId: ID!) {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(
|
||||
partyId: $partyId
|
||||
pagination: { last: $volumeDiscountEpochs }
|
||||
) {
|
||||
volumeDiscountStats(partyId: $partyId, pagination: { last: 1 }) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
@@ -59,10 +52,7 @@ query Fees(
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(
|
||||
partyId: $partyId
|
||||
pagination: { last: $referralDiscountEpochs }
|
||||
) {
|
||||
referralSetStats(partyId: $partyId, pagination: { last: 1 }) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
|
||||
+3
-10
@@ -10,8 +10,6 @@ export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgr
|
||||
|
||||
export type FeesQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
volumeDiscountEpochs: Types.Scalars['Int'];
|
||||
referralDiscountEpochs: Types.Scalars['Int'];
|
||||
}>;
|
||||
|
||||
|
||||
@@ -65,14 +63,11 @@ export type DiscountProgramsQueryHookResult = ReturnType<typeof useDiscountProgr
|
||||
export type DiscountProgramsLazyQueryHookResult = ReturnType<typeof useDiscountProgramsLazyQuery>;
|
||||
export type DiscountProgramsQueryResult = Apollo.QueryResult<DiscountProgramsQuery, DiscountProgramsQueryVariables>;
|
||||
export const FeesDocument = gql`
|
||||
query Fees($partyId: ID!, $volumeDiscountEpochs: Int!, $referralDiscountEpochs: Int!) {
|
||||
query Fees($partyId: ID!) {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(
|
||||
partyId: $partyId
|
||||
pagination: {last: $volumeDiscountEpochs}
|
||||
) {
|
||||
volumeDiscountStats(partyId: $partyId, pagination: {last: 1}) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
@@ -104,7 +99,7 @@ export const FeesDocument = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) {
|
||||
referralSetStats(partyId: $partyId, pagination: {last: 1}) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
@@ -129,8 +124,6 @@ export const FeesDocument = gql`
|
||||
* const { data, loading, error } = useFeesQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* volumeDiscountEpochs: // value for 'volumeDiscountEpochs'
|
||||
* referralDiscountEpochs: // value for 'referralDiscountEpochs'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -36,25 +36,25 @@ export const FeesContainer = () => {
|
||||
const { data: markets, loading: marketsLoading } = useMarketList();
|
||||
|
||||
const { data: programData, loading: programLoading } =
|
||||
useDiscountProgramsQuery();
|
||||
useDiscountProgramsQuery({ errorPolicy: 'ignore' });
|
||||
|
||||
const volumeDiscountWindowLength =
|
||||
programData?.currentVolumeDiscountProgram?.windowLength || 1;
|
||||
const referralDiscountWindowLength =
|
||||
programData?.currentReferralProgram?.windowLength || 1;
|
||||
|
||||
const { data: feesData, loading: feesLoading } = useFeesQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
volumeDiscountEpochs: volumeDiscountWindowLength,
|
||||
referralDiscountEpochs: referralDiscountWindowLength,
|
||||
},
|
||||
skip: !pubKey || !programData,
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
|
||||
|
||||
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
|
||||
useVolumeStats(
|
||||
feesData?.volumeDiscountStats,
|
||||
previousEpoch,
|
||||
feesData?.volumeDiscountStats.edges?.[0]?.node,
|
||||
programData?.currentVolumeDiscountProgram
|
||||
);
|
||||
|
||||
@@ -67,12 +67,12 @@ export const FeesContainer = () => {
|
||||
code,
|
||||
isReferrer,
|
||||
} = useReferralStats(
|
||||
feesData?.referralSetStats,
|
||||
feesData?.referralSetReferees,
|
||||
previousEpoch,
|
||||
feesData?.referralSetStats.edges?.[0]?.node,
|
||||
feesData?.referralSetReferees.edges?.[0]?.node,
|
||||
programData?.currentReferralProgram,
|
||||
feesData?.epoch,
|
||||
feesData?.referrer,
|
||||
feesData?.referee
|
||||
feesData?.referrer.edges?.[0]?.node,
|
||||
feesData?.referee.edges?.[0]?.node
|
||||
);
|
||||
|
||||
const loading = paramsLoading || feesLoading || programLoading;
|
||||
@@ -310,16 +310,25 @@ export const CurrentVolume = ({
|
||||
const t = useT();
|
||||
const nextTier = tiers[tierIndex + 1];
|
||||
const requiredForNextTier = nextTier
|
||||
? Number(nextTier.minimumRunningNotionalTakerVolume) - windowLengthVolume
|
||||
: 0;
|
||||
? new BigNumber(nextTier.minimumRunningNotionalTakerVolume).minus(
|
||||
windowLengthVolume
|
||||
)
|
||||
: new BigNumber(0);
|
||||
const currentVolume = new BigNumber(windowLengthVolume);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 pt-4">
|
||||
<CardStat
|
||||
value={formatNumberRounded(new BigNumber(windowLengthVolume))}
|
||||
text={t('Past {{count}} epochs', { count: windowLength })}
|
||||
value={
|
||||
currentVolume.isZero()
|
||||
? `<${formatNumberRounded(requiredForNextTier)}`
|
||||
: formatNumberRounded(currentVolume)
|
||||
}
|
||||
text={t('pastEpochs', 'Past {{count}} epochs', {
|
||||
count: windowLength,
|
||||
})}
|
||||
/>
|
||||
{requiredForNextTier > 0 && (
|
||||
{requiredForNextTier.isGreaterThan(0) && (
|
||||
<CardStat
|
||||
value={formatNumber(requiredForNextTier)}
|
||||
text={t('Required for next tier')}
|
||||
@@ -344,9 +353,13 @@ const ReferralBenefits = ({
|
||||
<CardStat
|
||||
// all sets volume (not just current party)
|
||||
value={formatNumber(setRunningNotionalTakerVolume)}
|
||||
text={t('Combined running notional over the {{count}} epochs', {
|
||||
count: epochs,
|
||||
})}
|
||||
text={t(
|
||||
'runningNotionalOverEpochs',
|
||||
'Combined running notional over the {{count}} epochs',
|
||||
{
|
||||
count: epochs,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<CardStat value={epochsInSet} text={t('epochs in referral set')} />
|
||||
</div>
|
||||
@@ -453,31 +466,27 @@ const VolumeTiers = ({
|
||||
<Th>{t('Discount')}</Th>
|
||||
<Th>{t('Min. trading volume')}</Th>
|
||||
<Th>
|
||||
{t('My volume (last {{count}} epochs)', { count: windowLength })}
|
||||
{t('myVolume', 'My volume (last {{count}} epochs)', {
|
||||
count: windowLength,
|
||||
})}
|
||||
</Th>
|
||||
<Th />
|
||||
</tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers)
|
||||
.reverse()
|
||||
.map((tier, i) => {
|
||||
const isUserTier = tiers.length - 1 - tierIndex === i;
|
||||
{Array.from(tiers).map((tier, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>
|
||||
{formatPercentage(Number(tier.volumeDiscountFactor))}%
|
||||
</Td>
|
||||
<Td>
|
||||
{formatNumber(tier.minimumRunningNotionalTakerVolume)}
|
||||
</Td>
|
||||
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : null}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>{formatPercentage(Number(tier.volumeDiscountFactor))}%</Td>
|
||||
<Td>{formatNumber(tier.minimumRunningNotionalTakerVolume)}</Td>
|
||||
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : null}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
@@ -520,37 +529,33 @@ const ReferralTiers = ({
|
||||
</tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers)
|
||||
.reverse()
|
||||
.map((t, i) => {
|
||||
const isUserTier = tiers.length - 1 - tierIndex === i;
|
||||
{Array.from(tiers).map((t, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
|
||||
const requiredVolume = Number(
|
||||
t.minimumRunningNotionalTakerVolume
|
||||
const requiredVolume = Number(t.minimumRunningNotionalTakerVolume);
|
||||
let unlocksIn = null;
|
||||
|
||||
if (
|
||||
referralVolumeInWindow >= requiredVolume &&
|
||||
epochsInSet < t.minimumEpochs
|
||||
) {
|
||||
unlocksIn = (
|
||||
<span className="text-muted">
|
||||
Unlocks in {t.minimumEpochs - epochsInSet} epochs
|
||||
</span>
|
||||
);
|
||||
let unlocksIn = null;
|
||||
}
|
||||
|
||||
if (
|
||||
referralVolumeInWindow >= requiredVolume &&
|
||||
epochsInSet < t.minimumEpochs
|
||||
) {
|
||||
unlocksIn = (
|
||||
<span className="text-muted">
|
||||
Unlocks in {t.minimumEpochs - epochsInSet} epochs
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
|
||||
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
|
||||
<Td>{t.minimumEpochs}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
|
||||
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
|
||||
<Td>{t.minimumEpochs}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@@ -2,46 +2,15 @@ import { renderHook } from '@testing-library/react';
|
||||
import { useReferralStats } from './use-referral-stats';
|
||||
|
||||
describe('useReferralStats', () => {
|
||||
const setStats = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 9,
|
||||
discountFactor: '0.2',
|
||||
referralSetRunningNotionalTakerVolume: '100',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: '200',
|
||||
},
|
||||
},
|
||||
],
|
||||
const stat = {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 9,
|
||||
discountFactor: '0.01',
|
||||
referralSetRunningNotionalTakerVolume: '100',
|
||||
};
|
||||
|
||||
const sets = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
atEpoch: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const epoch = {
|
||||
id: '10',
|
||||
const set = {
|
||||
atEpoch: 4,
|
||||
};
|
||||
|
||||
const program = {
|
||||
@@ -78,102 +47,36 @@ describe('useReferralStats', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns formatted data and tiers', () => {
|
||||
it('returns default values if set is not from previous epoch', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(setStats, sets, program, epoch)
|
||||
useReferralStats(10, stat, set, program)
|
||||
);
|
||||
|
||||
// should use stats from latest epoch
|
||||
const stats = setStats.edges[1].node;
|
||||
const set = sets.edges[1].node;
|
||||
|
||||
expect(result.current).toEqual({
|
||||
referralDiscount: Number(stats.discountFactor),
|
||||
referralVolumeInWindow: Number(
|
||||
stats.referralSetRunningNotionalTakerVolume
|
||||
),
|
||||
referralTierIndex: 1,
|
||||
referralDiscount: 0,
|
||||
referralVolumeInWindow: 0,
|
||||
referralTierIndex: -1,
|
||||
referralTiers: program.benefitTiers,
|
||||
epochsInSet: Number(epoch.id) - set.atEpoch,
|
||||
epochsInSet: 0,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ joinedAt: 2, index: -1 },
|
||||
{ joinedAt: 3, index: -1 },
|
||||
{ joinedAt: 4, index: 0 },
|
||||
{ joinedAt: 5, index: 0 },
|
||||
{ joinedAt: 6, index: 1 },
|
||||
{ joinedAt: 7, index: 1 },
|
||||
{ joinedAt: 8, index: 2 },
|
||||
{ joinedAt: 9, index: 2 },
|
||||
])('joined at epoch: $joinedAt should be index: $index', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: '100000',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const setsA = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: Number(epoch.id) - obj.joinedAt,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(statsA, setsA, program, epoch)
|
||||
useReferralStats(9, stat, set, program)
|
||||
);
|
||||
|
||||
expect(result.current.referralTierIndex).toEqual(obj.index);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ volume: '50', index: -1 },
|
||||
{ volume: '100', index: 0 },
|
||||
{ volume: '150', index: 0 },
|
||||
{ volume: '200', index: 1 },
|
||||
{ volume: '250', index: 1 },
|
||||
{ volume: '300', index: 2 },
|
||||
{ volume: '999', index: 2 },
|
||||
])('volume: $volume should be index: $index', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: obj.volume,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const setsA = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(statsA, setsA, program, epoch)
|
||||
);
|
||||
|
||||
expect(result.current.referralTierIndex).toEqual(obj.index);
|
||||
expect(result.current).toEqual({
|
||||
referralDiscount: Number(stat.discountFactor),
|
||||
referralVolumeInWindow: Number(
|
||||
stat.referralSetRunningNotionalTakerVolume
|
||||
),
|
||||
referralTierIndex: 0,
|
||||
referralTiers: program.benefitTiers,
|
||||
epochsInSet: stat.atEpoch - set.atEpoch,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import compact from 'lodash/compact';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { getReferralBenefitTier } from './utils';
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
import { first } from 'lodash';
|
||||
|
||||
export const useReferralStats = (
|
||||
setStats?: FeesQuery['referralSetStats'],
|
||||
setReferees?: FeesQuery['referralSetReferees'],
|
||||
previousEpoch?: number,
|
||||
referralStats?: NonNullable<
|
||||
FeesQuery['referralSetStats']['edges']['0']
|
||||
>['node'],
|
||||
setReferees?: NonNullable<
|
||||
FeesQuery['referralSetReferees']['edges']['0']
|
||||
>['node'],
|
||||
program?: DiscountProgramsQuery['currentReferralProgram'],
|
||||
epoch?: FeesQuery['epoch'],
|
||||
setIfReferrer?: FeesQuery['referrer'],
|
||||
setIfReferee?: FeesQuery['referee']
|
||||
setIfReferrer?: NonNullable<FeesQuery['referrer']['edges']['0']>['node'],
|
||||
setIfReferee?: NonNullable<FeesQuery['referee']['edges']['0']>['node']
|
||||
) => {
|
||||
const referralTiers = program?.benefitTiers || [];
|
||||
|
||||
if (!setStats || !setReferees || !program || !epoch) {
|
||||
if (
|
||||
!previousEpoch ||
|
||||
referralStats?.atEpoch !== previousEpoch ||
|
||||
!program ||
|
||||
!setReferees
|
||||
) {
|
||||
return {
|
||||
referralDiscount: 0,
|
||||
referralVolumeInWindow: 0,
|
||||
@@ -26,41 +30,22 @@ export const useReferralStats = (
|
||||
};
|
||||
}
|
||||
|
||||
const setIfReferrerData = first(
|
||||
compact(setIfReferrer?.edges).map((e) => e.node)
|
||||
);
|
||||
const setIfRefereeData = first(
|
||||
compact(setIfReferee?.edges).map((e) => e.node)
|
||||
);
|
||||
|
||||
const referralSetsStats = compact(setStats.edges).map((e) => e.node);
|
||||
const referralSets = compact(setReferees.edges).map((e) => e.node);
|
||||
|
||||
const referralSet = maxBy(referralSets, (s) => s.atEpoch);
|
||||
const referralStats = maxBy(referralSetsStats, (s) => s.atEpoch);
|
||||
|
||||
const epochsInSet = referralSet ? Number(epoch.id) - referralSet.atEpoch : 0;
|
||||
|
||||
const referralDiscount = Number(referralStats?.discountFactor || 0);
|
||||
const referralVolumeInWindow = Number(
|
||||
referralStats?.referralSetRunningNotionalTakerVolume || 0
|
||||
);
|
||||
|
||||
const referralTierIndex = referralStats
|
||||
? getReferralBenefitTier(
|
||||
epochsInSet,
|
||||
Number(referralStats.referralSetRunningNotionalTakerVolume),
|
||||
referralTiers
|
||||
)
|
||||
: -1;
|
||||
const referralTierIndex = referralTiers.findIndex(
|
||||
(tier) => tier.referralDiscountFactor === referralStats?.discountFactor
|
||||
);
|
||||
|
||||
return {
|
||||
referralDiscount,
|
||||
referralVolumeInWindow,
|
||||
referralTierIndex,
|
||||
referralTiers,
|
||||
epochsInSet,
|
||||
code: (setIfReferrerData || setIfRefereeData)?.id,
|
||||
isReferrer: Boolean(setIfReferrerData),
|
||||
epochsInSet: referralStats.atEpoch - setReferees.atEpoch,
|
||||
code: (setIfReferrer || setIfReferee)?.id,
|
||||
isReferrer: Boolean(setIfReferrer),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,27 +2,11 @@ import { renderHook } from '@testing-library/react';
|
||||
import { useVolumeStats } from './use-volume-stats';
|
||||
|
||||
describe('useReferralStats', () => {
|
||||
const statsList = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 9,
|
||||
discountFactor: '0.1',
|
||||
runningVolume: '100',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
runningVolume: '200',
|
||||
},
|
||||
},
|
||||
],
|
||||
const stats = {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.05',
|
||||
runningVolume: '200',
|
||||
};
|
||||
|
||||
const program = {
|
||||
@@ -44,7 +28,7 @@ describe('useReferralStats', () => {
|
||||
};
|
||||
|
||||
it('returns correct default values', () => {
|
||||
const { result } = renderHook(() => useVolumeStats());
|
||||
const { result } = renderHook(() => useVolumeStats(10));
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: 0,
|
||||
volumeInWindow: 0,
|
||||
@@ -53,11 +37,18 @@ describe('useReferralStats', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(statsList, program));
|
||||
it('returns default values if no stat is not from previous epoch', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(11, stats, program));
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: 0,
|
||||
volumeInWindow: 0,
|
||||
volumeTierIndex: -1,
|
||||
volumeTiers: program.benefitTiers,
|
||||
});
|
||||
});
|
||||
|
||||
// should use stats from latest epoch
|
||||
const stats = statsList.edges[1].node;
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(10, stats, program));
|
||||
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: Number(stats.discountFactor),
|
||||
@@ -66,30 +57,4 @@ describe('useReferralStats', () => {
|
||||
volumeTiers: program.benefitTiers,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ volume: '100', index: 0 },
|
||||
{ volume: '150', index: 0 },
|
||||
{ volume: '200', index: 1 },
|
||||
{ volume: '250', index: 1 },
|
||||
{ volume: '300', index: 2 },
|
||||
{ volume: '350', index: 2 },
|
||||
])('returns index: $index for the running volume: $volume', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
runningVolume: obj.volume,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useVolumeStats(statsA, program));
|
||||
expect(result.current.volumeTierIndex).toBe(obj.index);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import compact from 'lodash/compact';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { getVolumeTier } from './utils';
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
|
||||
export const useVolumeStats = (
|
||||
stats?: FeesQuery['volumeDiscountStats'],
|
||||
previousEpoch: number,
|
||||
lastEpochStats?: NonNullable<
|
||||
FeesQuery['volumeDiscountStats']['edges']['0']
|
||||
>['node'],
|
||||
program?: DiscountProgramsQuery['currentVolumeDiscountProgram']
|
||||
) => {
|
||||
const volumeTiers = program?.benefitTiers || [];
|
||||
|
||||
if (!stats || !program) {
|
||||
if (!lastEpochStats || lastEpochStats.atEpoch !== previousEpoch || !program) {
|
||||
return {
|
||||
volumeDiscount: 0,
|
||||
volumeTierIndex: -1,
|
||||
@@ -18,11 +18,11 @@ export const useVolumeStats = (
|
||||
};
|
||||
}
|
||||
|
||||
const volumeStats = compact(stats.edges).map((e) => e.node);
|
||||
const lastEpochStats = maxBy(volumeStats, (s) => s.atEpoch);
|
||||
const volumeDiscount = Number(lastEpochStats?.discountFactor || 0);
|
||||
const volumeInWindow = Number(lastEpochStats?.runningVolume || 0);
|
||||
const volumeTierIndex = getVolumeTier(volumeInWindow, volumeTiers);
|
||||
const volumeTierIndex = volumeTiers.findIndex(
|
||||
(tier) => tier.volumeDiscountFactor === lastEpochStats?.discountFactor
|
||||
);
|
||||
|
||||
return {
|
||||
volumeDiscount,
|
||||
|
||||
@@ -20,73 +20,6 @@ export const formatPercentage = (num: number) => {
|
||||
return formatter.format(parseFloat(pct.toFixed(5)));
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the index of the benefit tier for volume discounts. A user
|
||||
* only needs to fulfill a minimum volume requirement for the tier
|
||||
*/
|
||||
export const getVolumeTier = (
|
||||
volume: number,
|
||||
tiers: Array<{
|
||||
minimumRunningNotionalTakerVolume: string;
|
||||
}>
|
||||
) => {
|
||||
return tiers.findIndex((tier, i) => {
|
||||
const nextTier = tiers[i + 1];
|
||||
const validVolume =
|
||||
volume >= Number(tier.minimumRunningNotionalTakerVolume);
|
||||
|
||||
if (nextTier) {
|
||||
return (
|
||||
validVolume &&
|
||||
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
|
||||
);
|
||||
}
|
||||
|
||||
return validVolume;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the index of the benefit tiers for referrals. A user must
|
||||
* fulfill both the minimum epochs in the referral set, and the set
|
||||
* must reach the combined total volume
|
||||
*/
|
||||
export const getReferralBenefitTier = (
|
||||
epochsInSet: number,
|
||||
volume: number,
|
||||
tiers: Array<{
|
||||
minimumRunningNotionalTakerVolume: string;
|
||||
minimumEpochs: number;
|
||||
}>
|
||||
) => {
|
||||
const indexByEpoch = tiers.findIndex((tier, i) => {
|
||||
const nextTier = tiers[i + 1];
|
||||
const validEpochs = epochsInSet >= tier.minimumEpochs;
|
||||
|
||||
if (nextTier) {
|
||||
return validEpochs && epochsInSet < nextTier.minimumEpochs;
|
||||
}
|
||||
|
||||
return validEpochs;
|
||||
});
|
||||
const indexByVolume = tiers.findIndex((tier, i) => {
|
||||
const nextTier = tiers[i + 1];
|
||||
const validVolume =
|
||||
volume >= Number(tier.minimumRunningNotionalTakerVolume);
|
||||
|
||||
if (nextTier) {
|
||||
return (
|
||||
validVolume &&
|
||||
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
|
||||
);
|
||||
}
|
||||
|
||||
return validVolume;
|
||||
});
|
||||
|
||||
return Math.min(indexByEpoch, indexByVolume);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a set of fees and a set of discounts return
|
||||
* the adjusted fee factor
|
||||
|
||||
@@ -16,15 +16,12 @@ export const LedgerContainer = () => {
|
||||
});
|
||||
|
||||
const assets = (data?.party?.accountsConnection?.edges ?? [])
|
||||
.map<PartyAssetFieldsFragment>(
|
||||
(item) => item?.node?.asset ?? ({} as PartyAssetFieldsFragment)
|
||||
)
|
||||
.reduce((aggr, item) => {
|
||||
if ('id' in item && 'symbol' in item) {
|
||||
aggr[item.id as string] = item.symbol as string;
|
||||
}
|
||||
return aggr;
|
||||
}, {} as Record<string, string>);
|
||||
.map((item) => item?.node?.asset)
|
||||
.filter((asset): asset is PartyAssetFieldsFragment => !!asset?.id)
|
||||
.reduce(
|
||||
(aggr, item) => Object.assign(aggr, { [item.id]: item.symbol }),
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
if (!pubKey) {
|
||||
return (
|
||||
|
||||
@@ -172,4 +172,20 @@ describe('Navbar', () => {
|
||||
expect(mockDisconnect).toHaveBeenCalled();
|
||||
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render the language selector until we have more languages', () => {
|
||||
renderComponent();
|
||||
expect(screen.queryByTestId('icon-globe')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the theme switcher', async () => {
|
||||
renderComponent();
|
||||
await userEvent.click(screen.getByTestId('icon-moon'));
|
||||
expect(screen.queryByTestId('icon-moon')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('icon-sun')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByTestId('icon-sun'));
|
||||
expect(screen.queryByTestId('icon-sun')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('icon-moon')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,13 @@ import {
|
||||
} from '@vegaprotocol/environment';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
import { VegaWalletConnectButton } from '../vega-wallet-connect-button';
|
||||
import { VegaIconNames, VegaIcon, VLogo } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
VegaIconNames,
|
||||
VegaIcon,
|
||||
VLogo,
|
||||
LanguageSelector,
|
||||
ThemeSwitcher,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import * as N from '@radix-ui/react-navigation-menu';
|
||||
import * as D from '@radix-ui/react-dialog';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
@@ -22,7 +28,8 @@ import { VegaWalletMenu } from '../vega-wallet';
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { WalletIcon } from '../icons/wallet';
|
||||
import { ProtocolUpgradeCountdown } from '@vegaprotocol/proposals';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { useT, useI18n } from '../../lib/use-t';
|
||||
import { supportedLngs } from '../../lib/i18n';
|
||||
|
||||
type MenuState = 'wallet' | 'nav' | null;
|
||||
type Theme = 'system' | 'yellow';
|
||||
@@ -34,6 +41,7 @@ export const Navbar = ({
|
||||
children?: ReactNode;
|
||||
theme?: Theme;
|
||||
}) => {
|
||||
const i18n = useI18n();
|
||||
const t = useT();
|
||||
// menu state for small screens
|
||||
const [menu, setMenu] = useState<MenuState>(null);
|
||||
@@ -77,6 +85,15 @@ export const Navbar = ({
|
||||
{/* Right section */}
|
||||
<div className="ml-auto flex items-center justify-end gap-2">
|
||||
<ProtocolUpgradeCountdown />
|
||||
<div className="flex">
|
||||
<ThemeSwitcher />
|
||||
{supportedLngs.length > 1 ? (
|
||||
<LanguageSelector
|
||||
languages={supportedLngs}
|
||||
onSelect={(language) => i18n.changeLanguage(language)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<NavbarMobileButton
|
||||
onClick={() => {
|
||||
if (isConnected) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import uniq from 'lodash/uniq';
|
||||
import type { Account } from '@vegaprotocol/accounts';
|
||||
import { useAccounts } from '@vegaprotocol/accounts';
|
||||
import {
|
||||
@@ -31,6 +32,12 @@ import { ViewType, useSidebar } from '../sidebar';
|
||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
import { RewardsHistoryContainer } from './rewards-history';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
||||
|
||||
const ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA = [
|
||||
'bf1e88d19db4b3ca0d1d5bdb73718a01686b18cf731ca26adedf3c8b83802bba', // USDT mainnet
|
||||
'8ba0b10971f0c4747746cd01ff05a53ae75ca91eba1d4d050b527910c983e27e', // USDT testnet
|
||||
];
|
||||
|
||||
export const RewardsContainer = () => {
|
||||
const t = useT();
|
||||
@@ -40,34 +47,67 @@ export const RewardsContainer = () => {
|
||||
NetworkParams.rewards_activityStreak_benefitTiers,
|
||||
NetworkParams.rewards_vesting_baseRate,
|
||||
]);
|
||||
|
||||
const { data: accounts, loading: accountsLoading } = useAccounts(pubKey);
|
||||
|
||||
const { data: assetMap } = useAssetsMapProvider();
|
||||
|
||||
const { data: epochData } = useRewardsEpochQuery();
|
||||
|
||||
// No need to specify the fromEpoch as it will by default give you the last
|
||||
// Note activityStreak in query will fail
|
||||
const { data: rewardsData, loading: rewardsLoading } = useRewardsPageQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
},
|
||||
// Inclusion of activity streak in query currently fails
|
||||
errorPolicy: 'ignore',
|
||||
});
|
||||
|
||||
if (!epochData?.epoch) return null;
|
||||
if (!epochData?.epoch || !assetMap) return null;
|
||||
|
||||
const loading = paramsLoading || accountsLoading || rewardsLoading;
|
||||
|
||||
const rewardAccounts = accounts
|
||||
? accounts.filter((a) =>
|
||||
[
|
||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
AccountType.ACCOUNT_TYPE_VESTING_REWARDS,
|
||||
].includes(a.type)
|
||||
)
|
||||
? accounts
|
||||
.filter((a) =>
|
||||
[
|
||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
AccountType.ACCOUNT_TYPE_VESTING_REWARDS,
|
||||
].includes(a.type)
|
||||
)
|
||||
.filter((a) => new BigNumber(a.balance).isGreaterThan(0))
|
||||
: [];
|
||||
|
||||
const rewardAssetsMap = groupBy(
|
||||
rewardAccounts.filter((a) => a.asset.id !== params.reward_asset),
|
||||
'asset.id'
|
||||
);
|
||||
const rewardAccountsAssetMap = groupBy(rewardAccounts, 'asset.id');
|
||||
|
||||
const lockedBalances = rewardsData?.party?.vestingBalancesSummary
|
||||
.lockedBalances
|
||||
? rewardsData.party.vestingBalancesSummary.lockedBalances.filter((b) =>
|
||||
new BigNumber(b.balance).isGreaterThan(0)
|
||||
)
|
||||
: [];
|
||||
const lockedAssetMap = groupBy(lockedBalances, 'asset.id');
|
||||
|
||||
const vestingBalances = rewardsData?.party?.vestingBalancesSummary
|
||||
.vestingBalances
|
||||
? rewardsData.party.vestingBalancesSummary.vestingBalances.filter((b) =>
|
||||
new BigNumber(b.balance).isGreaterThan(0)
|
||||
)
|
||||
: [];
|
||||
const vestingAssetMap = groupBy(vestingBalances, 'asset.id');
|
||||
|
||||
// each asset reward pot is made up of:
|
||||
// available to withdraw - ACCOUNT_TYPE_VESTED_REWARDS
|
||||
// vesting - vestingBalancesSummary.vestingBalances
|
||||
// locked - vestingBalancesSummary.lockedBalances
|
||||
//
|
||||
// there can be entires for the same asset in each list so we need a uniq list of assets
|
||||
const assets = uniq([
|
||||
...Object.keys(rewardAccountsAssetMap),
|
||||
...Object.keys(lockedAssetMap),
|
||||
...Object.keys(vestingAssetMap),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="grid auto-rows-min grid-cols-6 gap-3">
|
||||
@@ -117,28 +157,72 @@ export const RewardsContainer = () => {
|
||||
</Card>
|
||||
|
||||
{/* Show all other reward pots, most of the time users will not have other rewards */}
|
||||
{Object.keys(rewardAssetsMap).map((assetId) => {
|
||||
const asset = rewardAssetsMap[assetId][0].asset;
|
||||
return (
|
||||
<Card
|
||||
key={assetId}
|
||||
title={t('{{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>
|
||||
);
|
||||
})}
|
||||
{assets
|
||||
.filter((assetId) => assetId !== params.reward_asset)
|
||||
.map((assetId) => {
|
||||
const asset = assetMap ? assetMap[assetId] : null;
|
||||
|
||||
if (!asset) return null;
|
||||
|
||||
// Following code is for mitigating an issue due to a core bug where locked and vesting
|
||||
// balances were incorrectly increased for infrastructure rewards for USDT on mainnet
|
||||
//
|
||||
// We don't want to incorrectly show the wring locked/vesting values, but we DO want to
|
||||
// show the user that they have rewards available to withdraw
|
||||
if (ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA.includes(asset.id)) {
|
||||
const accountsForAsset = rewardAccountsAssetMap[asset.id];
|
||||
const vestedAccount = accountsForAsset?.find(
|
||||
(a) => a.type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||
);
|
||||
|
||||
// No vested rewards available to withdraw, so skip over USDT
|
||||
if (!vestedAccount || Number(vestedAccount.balance) <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={assetId}
|
||||
title={t('{{assetSymbol}} Reward pot', {
|
||||
assetSymbol: asset.symbol,
|
||||
})}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={assetId}
|
||||
// Ensure that these values are shown as 0
|
||||
vestingBalancesSummary={{
|
||||
lockedBalances: [],
|
||||
vestingBalances: [],
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={assetId}
|
||||
title={t('{{assetSymbol}} Reward pot', {
|
||||
assetSymbol: asset.symbol,
|
||||
})}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={assetId}
|
||||
vestingBalancesSummary={
|
||||
rewardsData?.party?.vestingBalancesSummary
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
<Card
|
||||
title={t('Rewards history')}
|
||||
className="lg:col-span-full"
|
||||
@@ -147,6 +231,7 @@ export const RewardsContainer = () => {
|
||||
<RewardsHistoryContainer
|
||||
epoch={Number(epochData?.epoch.id)}
|
||||
pubKey={pubKey}
|
||||
assets={assetMap}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -313,14 +398,14 @@ export const RewardPot = ({
|
||||
export const Vesting = ({
|
||||
pubKey,
|
||||
baseRate,
|
||||
multiplier = '1',
|
||||
multiplier,
|
||||
}: {
|
||||
pubKey: string | null;
|
||||
baseRate: string;
|
||||
multiplier?: string;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const rate = new BigNumber(baseRate).times(multiplier);
|
||||
const rate = new BigNumber(baseRate).times(multiplier || 1);
|
||||
const rateFormatted = formatPercentage(Number(rate));
|
||||
const baseRateFormatted = formatPercentage(Number(baseRate));
|
||||
|
||||
@@ -335,7 +420,7 @@ export const Vesting = ({
|
||||
{pubKey && (
|
||||
<tr>
|
||||
<CardTableTH>{t('Vesting multiplier')}</CardTableTH>
|
||||
<CardTableTD>{multiplier}x</CardTableTD>
|
||||
<CardTableTD>{multiplier ? `${multiplier}x` : '-'}</CardTableTD>
|
||||
</tr>
|
||||
)}
|
||||
</CardTable>
|
||||
@@ -345,16 +430,16 @@ export const Vesting = ({
|
||||
|
||||
export const Multipliers = ({
|
||||
pubKey,
|
||||
streakMultiplier = '1',
|
||||
hoarderMultiplier = '1',
|
||||
streakMultiplier,
|
||||
hoarderMultiplier,
|
||||
}: {
|
||||
pubKey: string | null;
|
||||
streakMultiplier?: string;
|
||||
hoarderMultiplier?: string;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const combinedMultiplier = new BigNumber(streakMultiplier).times(
|
||||
hoarderMultiplier
|
||||
const combinedMultiplier = new BigNumber(streakMultiplier || 1).times(
|
||||
hoarderMultiplier || 1
|
||||
);
|
||||
|
||||
if (!pubKey) {
|
||||
@@ -375,11 +460,15 @@ export const Multipliers = ({
|
||||
<CardTable>
|
||||
<tr>
|
||||
<CardTableTH>{t('Streak reward multiplier')}</CardTableTH>
|
||||
<CardTableTD>{streakMultiplier}x</CardTableTD>
|
||||
<CardTableTD>
|
||||
{streakMultiplier ? `${streakMultiplier}x` : '-'}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<CardTableTH>{t('Hoarder reward multiplier')}</CardTableTH>
|
||||
<CardTableTD>{hoarderMultiplier}x</CardTableTD>
|
||||
<CardTableTD>
|
||||
{hoarderMultiplier ? `${hoarderMultiplier}x` : '-'}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
</CardTable>
|
||||
</div>
|
||||
|
||||
@@ -61,6 +61,14 @@ const rewardSummaries = [
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
epoch: 7,
|
||||
assetId: assets.asset2.id,
|
||||
amount: '300',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getCell = (cells: HTMLElement[], colId: string) => {
|
||||
@@ -69,7 +77,7 @@ const getCell = (cells: HTMLElement[], colId: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
describe('RewarsHistoryTable', () => {
|
||||
describe('RewardsHistoryTable', () => {
|
||||
const props = {
|
||||
epochRewardSummaries: {
|
||||
edges: rewardSummaries,
|
||||
@@ -88,7 +96,7 @@ describe('RewarsHistoryTable', () => {
|
||||
loading: false,
|
||||
};
|
||||
|
||||
it('Renders table with accounts summed up by asset', () => {
|
||||
it('renders table with accounts summed up by asset', () => {
|
||||
render(<RewardHistoryTable {...props} />);
|
||||
|
||||
const container = within(
|
||||
@@ -110,17 +118,27 @@ describe('RewarsHistoryTable', () => {
|
||||
assets.asset2.name
|
||||
);
|
||||
|
||||
// First row
|
||||
const marketCreationCell = getCell(cells, 'marketCreation');
|
||||
expect(
|
||||
marketCreationCell.getByTestId('stack-cell-primary')
|
||||
).toHaveTextContent('300');
|
||||
expect(
|
||||
marketCreationCell.getByTestId('stack-cell-secondary')
|
||||
).toHaveTextContent('100.00%');
|
||||
).toHaveTextContent('50.00%');
|
||||
|
||||
const infrastructureFeesCell = getCell(cells, 'infrastructureFees');
|
||||
expect(
|
||||
infrastructureFeesCell.getByTestId('stack-cell-primary')
|
||||
).toHaveTextContent('300');
|
||||
expect(
|
||||
infrastructureFeesCell.getByTestId('stack-cell-secondary')
|
||||
).toHaveTextContent('50.00%');
|
||||
|
||||
let totalCell = getCell(cells, 'total');
|
||||
expect(totalCell.getByText('300.00')).toBeInTheDocument();
|
||||
expect(totalCell.getByText('600.00')).toBeInTheDocument();
|
||||
|
||||
// Second row
|
||||
row = within(rows[1]);
|
||||
cells = row.getAllByRole('gridcell');
|
||||
|
||||
|
||||
@@ -2,10 +2,7 @@ import debounce from 'lodash/debounce';
|
||||
import { useMemo, useState } from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ColDef, ValueFormatterFunc } from 'ag-grid-community';
|
||||
import {
|
||||
useAssetsMapProvider,
|
||||
type AssetFieldsFragment,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { type AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import {
|
||||
addDecimalsFormatNumberQuantum,
|
||||
formatNumberPercentage,
|
||||
@@ -26,17 +23,17 @@ 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: {
|
||||
@@ -154,10 +151,12 @@ export const RewardHistoryTable = ({
|
||||
const rewardValueFormatter: ValueFormatterFunc<RewardRow> = ({
|
||||
data,
|
||||
value,
|
||||
...rest
|
||||
}) => {
|
||||
if (!value || !data) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return addDecimalsFormatNumberQuantum(
|
||||
value,
|
||||
data.asset.decimals,
|
||||
@@ -197,6 +196,11 @@ export const RewardHistoryTable = ({
|
||||
},
|
||||
sort: 'desc',
|
||||
},
|
||||
{
|
||||
field: 'infrastructureFees',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'staking',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { type AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import { getRewards } from './use-reward-row-data';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const asset1 = {
|
||||
id: 'asset1',
|
||||
name: 'USD (KRW)',
|
||||
symbol: 'USD-KRW',
|
||||
decimals: 6,
|
||||
quantum: '1000000',
|
||||
status: Schema.AssetStatus.STATUS_ENABLED,
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
} as AssetFieldsFragment;
|
||||
|
||||
const asset2 = {
|
||||
id: 'asset2',
|
||||
name: 'tDAI TEST',
|
||||
symbol: 'tDAI',
|
||||
decimals: 5,
|
||||
quantum: '1',
|
||||
status: Schema.AssetStatus.STATUS_ENABLED,
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
} as AssetFieldsFragment;
|
||||
|
||||
const asset3 = {
|
||||
id: 'asset3',
|
||||
name: 'Tether USD',
|
||||
symbol: 'USDT',
|
||||
decimals: 6,
|
||||
quantum: '1000000',
|
||||
status: Schema.AssetStatus.STATUS_ENABLED,
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
} as AssetFieldsFragment;
|
||||
|
||||
const asset4 = {
|
||||
id: 'asset4',
|
||||
name: 'USDT-T',
|
||||
symbol: 'USDT-T',
|
||||
decimals: 18,
|
||||
quantum: '1',
|
||||
status: Schema.AssetStatus.STATUS_ENABLED,
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
} as AssetFieldsFragment;
|
||||
|
||||
const assets: Record<string, AssetFieldsFragment> = {
|
||||
asset1,
|
||||
asset2,
|
||||
asset3,
|
||||
asset4,
|
||||
};
|
||||
|
||||
const testData = {
|
||||
rewards: [
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
assetId: 'asset1',
|
||||
amount: '31897424',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
assetId: 'asset2',
|
||||
amount: '57',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
assetId: 'asset3',
|
||||
amount: '5501',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
|
||||
assetId: 'asset3',
|
||||
amount: '5501',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
|
||||
assetId: 'asset4',
|
||||
amount: '5501',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
assetId: 'asset4',
|
||||
amount: '456',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING,
|
||||
assetId: 'asset4',
|
||||
amount: '4565',
|
||||
},
|
||||
],
|
||||
assets,
|
||||
};
|
||||
|
||||
describe('getRewards', () => {
|
||||
it('should return the correct rewards when infra fees are included', () => {
|
||||
const rewards = getRewards(testData.rewards, testData.assets);
|
||||
expect(rewards).toEqual([
|
||||
{
|
||||
asset: asset1,
|
||||
infrastructureFees: 31897424,
|
||||
staking: 0,
|
||||
priceTaking: 0,
|
||||
priceMaking: 0,
|
||||
liquidityProvision: 0,
|
||||
marketCreation: 0,
|
||||
averagePosition: 0,
|
||||
relativeReturns: 0,
|
||||
returnsVolatility: 0,
|
||||
validatorRanking: 0,
|
||||
total: 31897424,
|
||||
},
|
||||
{
|
||||
asset: asset2,
|
||||
infrastructureFees: 57,
|
||||
staking: 0,
|
||||
priceTaking: 0,
|
||||
priceMaking: 0,
|
||||
liquidityProvision: 0,
|
||||
marketCreation: 0,
|
||||
averagePosition: 0,
|
||||
relativeReturns: 0,
|
||||
returnsVolatility: 0,
|
||||
validatorRanking: 0,
|
||||
total: 57,
|
||||
},
|
||||
{
|
||||
asset: asset3,
|
||||
infrastructureFees: 5501,
|
||||
staking: 0,
|
||||
priceTaking: 0,
|
||||
priceMaking: 0,
|
||||
liquidityProvision: 0,
|
||||
marketCreation: 0,
|
||||
averagePosition: 5501,
|
||||
relativeReturns: 0,
|
||||
returnsVolatility: 0,
|
||||
validatorRanking: 0,
|
||||
total: 11002,
|
||||
},
|
||||
{
|
||||
asset: asset4,
|
||||
infrastructureFees: 0,
|
||||
staking: 0,
|
||||
priceTaking: 0,
|
||||
priceMaking: 5501,
|
||||
liquidityProvision: 456,
|
||||
marketCreation: 0,
|
||||
averagePosition: 0,
|
||||
relativeReturns: 0,
|
||||
returnsVolatility: 0,
|
||||
validatorRanking: 4565,
|
||||
total: 10522,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -16,9 +16,10 @@ const REWARD_ACCOUNT_TYPES = [
|
||||
AccountType.ACCOUNT_TYPE_REWARD_RELATIVE_RETURN,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING,
|
||||
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
];
|
||||
|
||||
const getRewards = (
|
||||
export const getRewards = (
|
||||
rewards: Array<{
|
||||
rewardType: AccountType;
|
||||
assetId: string;
|
||||
@@ -56,6 +57,9 @@ const getRewards = (
|
||||
|
||||
return {
|
||||
asset,
|
||||
infrastructureFees: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE
|
||||
),
|
||||
staking: totals.get(AccountType.ACCOUNT_TYPE_GLOBAL_REWARD),
|
||||
priceTaking: totals.get(AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES),
|
||||
priceMaking: totals.get(
|
||||
@@ -101,7 +105,8 @@ export const useRewardsRowData = ({
|
||||
assetId: r.asset.id,
|
||||
amount: r.amount,
|
||||
}));
|
||||
return getRewards(rewards, assets);
|
||||
const result = getRewards(rewards, assets);
|
||||
return result;
|
||||
}
|
||||
|
||||
const rewards = removePaginationWrapper(epochRewardSummaries?.edges);
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
|
||||
VEGA_VERSION=v0.73.5
|
||||
VEGA_VERSION=v0.73.6
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:develop
|
||||
VEGA_VERSION=v0.73.5
|
||||
VEGA_VERSION=v0.73.6
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:main
|
||||
VEGA_VERSION=v0.73.5
|
||||
VEGA_VERSION=v0.73.6
|
||||
|
||||
@@ -12,6 +12,7 @@ from contextlib import contextmanager
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from playwright.sync_api import Browser, Page
|
||||
from config import console_image_name, vega_version
|
||||
from datetime import datetime, timedelta
|
||||
from fixtures.market import (
|
||||
setup_simple_market,
|
||||
setup_opening_auction_market,
|
||||
@@ -78,6 +79,7 @@ def init_vega(request=None):
|
||||
store_transactions=True,
|
||||
transactions_per_block=1000,
|
||||
seconds_per_block=seconds_per_block,
|
||||
genesis_time= datetime.now() - timedelta(days=1),
|
||||
) as vega:
|
||||
try:
|
||||
container = docker_client.containers.run(
|
||||
|
||||
Generated
+1
-1
@@ -1161,7 +1161,7 @@ profile = ["pytest-profiling", "snakeviz"]
|
||||
type = "git"
|
||||
url = "https://github.com/vegaprotocol/vega-market-sim.git"
|
||||
reference = "HEAD"
|
||||
resolved_reference = "e93f7dfa8463c59cfd0e299362b845511cebeef6"
|
||||
resolved_reference = "fbcb974b2055bbc80169cdfd69987f087f9969fb"
|
||||
|
||||
[[package]]
|
||||
name = "websocket-client"
|
||||
|
||||
@@ -9,7 +9,7 @@ packages = [{include = "trading market-sim e2e"}]
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9,<3.11"
|
||||
psutil = "^5.9.5"
|
||||
vega-sim = {git = "https://github.com/vegaprotocol/vega-market-sim.git"}
|
||||
vega-sim = {git = "https://github.com/vegaprotocol/vega-market-sim.git/", branch = "fix/genesis_panic"}
|
||||
pytest-playwright = "^0.4.2"
|
||||
docker = "^6.1.3"
|
||||
pytest-xdist = "^3.3.1"
|
||||
|
||||
@@ -58,7 +58,6 @@ class TestSettledMarket:
|
||||
def test_settled_rows(self, page: Page, create_settled_market):
|
||||
page.goto(f"/#/markets/all")
|
||||
page.get_by_test_id("Closed markets").click()
|
||||
|
||||
row_selector = page.locator(
|
||||
'[data-testid="tab-closed-markets"] .ag-center-cols-container .ag-row'
|
||||
).first
|
||||
@@ -72,7 +71,7 @@ class TestSettledMarket:
|
||||
# 6001-MARK-009
|
||||
# 6001-MARK-008
|
||||
# 6001-MARK-010
|
||||
pattern = r"(\d+)\s+months\s+ago"
|
||||
pattern = r"(\d+)\s+(months|hours|days)\s+ago"
|
||||
date_text = row_selector.locator('[col-id="settlementDate"]').inner_text()
|
||||
assert re.match(pattern, date_text), f"Expected text to match pattern but got {date_text}"
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.service import VegaService
|
||||
from actions.vega import submit_order
|
||||
from actions.utils import change_keys
|
||||
from wallet_config import MM_WALLET, MM_WALLET2
|
||||
import logging
|
||||
|
||||
@@ -196,3 +197,17 @@ def test_price_monitoring(simple_market, vega: VegaService, page: Page):
|
||||
expect(
|
||||
page.get_by_test_id(liquidity_supplied).get_by_test_id(item_value)
|
||||
).to_have_text("50.00 (>100%)")
|
||||
|
||||
COL_ID_FEE = ".ag-center-cols-container [col-id='fee'] .ag-cell-value"
|
||||
|
||||
@pytest.mark.usefixtures("vega", "page", "continuous_market", "risk_accepted", "auth")
|
||||
def test_auction_uncross_fees(continuous_market, vega: VegaService, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id("Fills").click()
|
||||
expect(page.locator(COL_ID_FEE)).to_have_text("0.00 tDAI")
|
||||
page.locator(COL_ID_FEE).hover()
|
||||
expect(page.get_by_test_id("fee-breakdown-tooltip")).to_have_text("If the market was suspendedIf the market is in monitoring auction, half of the infrastructure and liquidity fees will be paid.Infrastructure fee0.00 tDAILiquidity fee0.00 tDAIMaker fee0.00 tDAITotal fees0.00 tDAI")
|
||||
change_keys(page,vega, "market_maker")
|
||||
expect(page.locator(COL_ID_FEE)).to_have_text("0.00 tDAI")
|
||||
page.locator(COL_ID_FEE).hover()
|
||||
expect(page.get_by_test_id("fee-breakdown-tooltip")).to_have_text("If the market was suspendedIf the market is in monitoring auction, half of the infrastructure and liquidity fees will be paid.Infrastructure fee0.00 tDAILiquidity fee0.00 tDAIMaker fee0.00 tDAITotal fees0.00 tDAI")
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from fixtures.market import setup_continuous_market
|
||||
|
||||
from conftest import init_vega
|
||||
|
||||
market_names = ["ETHBTC.QM21", "BTCUSD.MF21", "SOLUSD", "AAPL.MF21"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega():
|
||||
with init_vega() as vega:
|
||||
yield vega
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def create_markets(vega):
|
||||
for market_name in market_names:
|
||||
setup_continuous_market(vega, custom_market_name=market_name)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_table_headers(page: Page, create_markets):
|
||||
page.goto(f"/#/markets/all")
|
||||
headers = [
|
||||
"Market",
|
||||
"Description",
|
||||
"Settlement asset",
|
||||
"Trading mode",
|
||||
"Status",
|
||||
"Mark price",
|
||||
"24h volume",
|
||||
"Open Interest",
|
||||
"Spread",
|
||||
"",
|
||||
]
|
||||
page.wait_for_selector('[data-testid="tab-open-markets"]', state="visible")
|
||||
page_headers = (
|
||||
page.get_by_test_id("tab-open-markets").locator(".ag-header-cell-text").all()
|
||||
)
|
||||
for i, header in enumerate(headers):
|
||||
expect(page_headers[i]).to_have_text(header)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_markets_tab(page: Page, create_markets):
|
||||
page.goto(f"/#/markets/all")
|
||||
expect(page.get_by_test_id("Open markets")).to_have_attribute(
|
||||
"data-state", "active"
|
||||
)
|
||||
expect(page.get_by_test_id("Proposed markets")).to_have_attribute(
|
||||
"data-state", "inactive"
|
||||
)
|
||||
expect(page.get_by_test_id("Closed markets")).to_have_attribute(
|
||||
"data-state", "inactive"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_markets_content(page: Page, create_markets):
|
||||
page.goto(f"/#/markets/all")
|
||||
row_selector = page.locator(
|
||||
'[data-testid="tab-open-markets"] .ag-center-cols-container .ag-row'
|
||||
).first
|
||||
instrument_code_locator = '[col-id="tradableInstrument.instrument.code"] [data-testid="stack-cell-primary"]'
|
||||
# 6001-MARK-035
|
||||
expect(row_selector.locator(instrument_code_locator)).to_have_text("ETHBTC.QM21")
|
||||
|
||||
# 6001-MARK-073
|
||||
expect(row_selector.locator('[title="Future"]')).to_have_text("Futr")
|
||||
|
||||
# 6001-MARK-036
|
||||
expect(
|
||||
row_selector.locator('[col-id="tradableInstrument.instrument.name"]')
|
||||
).to_have_text("ETHBTC.QM21")
|
||||
|
||||
# 6001-MARK-037
|
||||
expect(row_selector.locator('[col-id="tradingMode"]')).to_have_text("Continuous")
|
||||
|
||||
# 6001-MARK-038
|
||||
expect(row_selector.locator('[col-id="state"]')).to_have_text("Active")
|
||||
|
||||
# 6001-MARK-039
|
||||
expect(row_selector.locator('[col-id="data.markPrice"]')).to_have_text("107.50")
|
||||
|
||||
# 6001-MARK-040
|
||||
expect(row_selector.locator('[col-id="data.candles"]')).to_have_text("0.00")
|
||||
|
||||
# 6001-MARK-042
|
||||
expect(
|
||||
row_selector.locator(
|
||||
'[col-id="tradableInstrument.instrument.product.settlementAsset.symbol"]'
|
||||
)
|
||||
).to_have_text("tDAI")
|
||||
|
||||
expect(row_selector.locator('[col-id="data.bestBidPrice"]')).to_have_text("2")
|
||||
|
||||
# 6001-MARK-043
|
||||
row_selector.locator(
|
||||
'[col-id="tradableInstrument.instrument.product.settlementAsset.symbol"] button'
|
||||
).click()
|
||||
expect(page.get_by_test_id("dialog-title")).to_have_text("Asset details - tDAI")
|
||||
# 6001-MARK-019
|
||||
page.get_by_test_id("close-asset-details-dialog").click()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_market_actions(page: Page, create_markets):
|
||||
# 6001-MARK-044
|
||||
# 6001-MARK-045
|
||||
# 6001-MARK-046
|
||||
# 6001-MARK-047
|
||||
page.goto(f"/#/markets/all")
|
||||
page.locator(
|
||||
'.ag-pinned-right-cols-container [col-id="market-actions"]'
|
||||
).first.locator("button").click()
|
||||
|
||||
actions = [
|
||||
"Copy Market ID",
|
||||
"View on Explorer",
|
||||
"View settlement asset details",
|
||||
]
|
||||
action_elements = (
|
||||
page.get_by_test_id("market-actions-content").get_by_role("menuitem").all()
|
||||
)
|
||||
|
||||
for i, action in enumerate(actions):
|
||||
expect(action_elements[i]).to_have_text(action)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_sort_markets(page: Page, create_markets):
|
||||
# 6001-MARK-064
|
||||
|
||||
page.goto(f"/#/markets/all")
|
||||
sorted_market_names = [
|
||||
"AAPL.MF21",
|
||||
"BTCUSD.MF21",
|
||||
"ETHBTC.QM21",
|
||||
"SOLUSD",
|
||||
]
|
||||
page.locator('.ag-header-row [col-id="tradableInstrument.instrument.code"]').click()
|
||||
for i, market_name in enumerate(sorted_market_names):
|
||||
expect(
|
||||
page.locator(
|
||||
f'[row-index="{i}"] [col-id="tradableInstrument.instrument.name"]'
|
||||
)
|
||||
).to_have_text(market_name)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_drag_and_drop_column(page: Page, create_markets):
|
||||
# 6001-MARK-065
|
||||
page.goto(f"/#/markets/all")
|
||||
col_instrument_code = '.ag-header-row [col-id="tradableInstrument.instrument.code"]'
|
||||
|
||||
page.locator(col_instrument_code).drag_to(
|
||||
page.locator('.ag-header-row [col-id="data.bestBidPrice"]')
|
||||
)
|
||||
expect(page.locator(col_instrument_code)).to_have_attribute("aria-colindex", "9")
|
||||
@@ -1,15 +1,19 @@
|
||||
import pytest
|
||||
import re
|
||||
import vega_sim.api.governance as governance
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.service import VegaService, PeggedOrder
|
||||
import vega_sim.api.governance as governance
|
||||
from actions.vega import submit_order
|
||||
from actions.utils import next_epoch
|
||||
from wallet_config import MM_WALLET, MM_WALLET2, GOVERNANCE_WALLET
|
||||
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("vega", "page", "proposed_market", "risk_accepted")
|
||||
def test_market_lifecycle(proposed_market, vega: VegaService, page: Page):
|
||||
# 7002-SORD-001
|
||||
# 7002-SORD-002
|
||||
trading_mode = page.get_by_test_id("market-trading-mode").get_by_test_id(
|
||||
"item-value"
|
||||
)
|
||||
@@ -18,12 +22,32 @@ def test_market_lifecycle(proposed_market, vega: VegaService, page: Page):
|
||||
# setup market in proposed step, without liquidity provided
|
||||
market_id = proposed_market
|
||||
page.goto(f"/#/markets/{market_id}")
|
||||
|
||||
# 6002-MDET-001
|
||||
expect(page.get_by_test_id("header-title")).to_have_text("BTC:DAI_2023Futr")
|
||||
# 6002-MDET-002
|
||||
expect(page.get_by_test_id("market-expiry")).to_have_text("ExpiryNot time-based")
|
||||
page.get_by_test_id("market-expiry").hover()
|
||||
expect(page.get_by_test_id("expiry-tooltip").first).to_have_text("This market expires when triggered by its oracle, not on a set date.View oracle specification")
|
||||
expect(page.get_by_test_id("expiry-tooltip").first.get_by_test_id("link")).to_have_attribute("href", re.compile('.*'))
|
||||
# 6002-MDET-003
|
||||
expect(page.get_by_test_id("market-price")).to_have_text("Mark Price0.00")
|
||||
# 6002-MDET-004
|
||||
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
|
||||
# 6002-MDET-005
|
||||
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
|
||||
# 6002-MDET-008
|
||||
expect(page.get_by_test_id("market-settlement-asset")).to_have_text("Settlement assettDAI")
|
||||
expect(page.get_by_test_id("liquidity-supplied")).to_have_text("Liquidity supplied 0.00 (0.00%)")
|
||||
page.get_by_test_id("liquidity-supplied").hover()
|
||||
expect(page.get_by_test_id("liquidity-supplied-tooltip").first).to_have_text("Supplied stake0.00Target stake0.00View liquidity provision tableLearn about providing liquidity")
|
||||
expect(page.get_by_test_id("liquidity-supplied-tooltip").first.get_by_test_id("link").first).to_have_text("View liquidity provision table")
|
||||
# check that market is in proposed state
|
||||
# 6002-MDET-006
|
||||
# 6002-MDET-007
|
||||
# 7002-SORD-061
|
||||
expect(trading_mode).to_have_text("No trading")
|
||||
trading_mode.hover()
|
||||
expect(page.get_by_test_id("trading-mode-tooltip").first).to_have_text("No trading enabled for this market.")
|
||||
expect(market_state).to_have_text("Proposed")
|
||||
|
||||
# approve market
|
||||
@@ -35,9 +59,9 @@ def test_market_lifecycle(proposed_market, vega: VegaService, page: Page):
|
||||
|
||||
# "wait" for market to be approved and enacted
|
||||
vega.forward("60s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_fn(10)
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
next_epoch(vega=vega)
|
||||
# check that market is in pending state
|
||||
expect(trading_mode).to_have_text("Opening auction")
|
||||
expect(market_state).to_have_text("Pending")
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import pytest
|
||||
import re
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.service import VegaService
|
||||
from vega_sim.service import MarketStateUpdateType
|
||||
from datetime import datetime, timedelta
|
||||
from conftest import init_vega
|
||||
from actions.utils import change_keys
|
||||
from actions.vega import submit_multiple_orders
|
||||
from fixtures.market import setup_perps_market
|
||||
from wallet_config import MM_WALLET, MM_WALLET2, TERMINATE_WALLET
|
||||
|
||||
row_selector = '[data-testid="tab-funding-payments"] .ag-center-cols-container .ag-row'
|
||||
col_amount = '[col-id="amount"]'
|
||||
|
||||
class TestPerpetuals:
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def vega(self, request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def perps_market(self, vega: VegaService):
|
||||
perps_market = setup_perps_market(vega)
|
||||
submit_multiple_orders(
|
||||
vega, MM_WALLET.name, perps_market, "SIDE_SELL", [[1, 110], [1, 105]]
|
||||
)
|
||||
submit_multiple_orders(
|
||||
vega, MM_WALLET2.name, perps_market, "SIDE_BUY", [[1, 90], [1, 95]]
|
||||
)
|
||||
vega.submit_settlement_data(
|
||||
settlement_key=TERMINATE_WALLET.name,
|
||||
settlement_price=110,
|
||||
market_id=perps_market,
|
||||
)
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
submit_multiple_orders(
|
||||
vega, MM_WALLET.name, perps_market, "SIDE_SELL", [[1, 110], [1, 105]]
|
||||
)
|
||||
submit_multiple_orders(
|
||||
vega, MM_WALLET2.name, perps_market, "SIDE_BUY", [[1, 112], [1, 115]]
|
||||
)
|
||||
vega.submit_settlement_data(
|
||||
settlement_key=TERMINATE_WALLET.name,
|
||||
settlement_price=110,
|
||||
market_id=perps_market,
|
||||
)
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
return perps_market
|
||||
|
||||
@pytest.mark.usefixtures("page","risk_accepted", "auth")
|
||||
def test_funding_payment_profit(self, perps_market, page: Page):
|
||||
page.goto(f"/#/markets/{perps_market}")
|
||||
page.get_by_test_id("Funding payments").click()
|
||||
row = page.locator(row_selector)
|
||||
expect(row.locator(col_amount)).to_have_text("9.00 tDAI")
|
||||
|
||||
@pytest.mark.usefixtures("page","risk_accepted", "auth")
|
||||
def test_funding_payment_loss(self, perps_market, page: Page, vega):
|
||||
page.goto(f"/#/markets/{perps_market}")
|
||||
change_keys(page, vega, "market_maker")
|
||||
page.get_by_test_id("Funding payments").click()
|
||||
row = page.locator(row_selector)
|
||||
expect(row.locator(col_amount)).to_have_text("-27.00 tDAI")
|
||||
|
||||
@pytest.mark.usefixtures("page","risk_accepted", "auth")
|
||||
def test_funding_header(self, perps_market, page: Page):
|
||||
page.goto(f"/#/markets/{perps_market}")
|
||||
expect(page.get_by_test_id("market-funding")).to_contain_text("Funding Rate / Countdown-8.1818%")
|
||||
expect(page.get_by_test_id("index-price")).to_have_text("Index Price110.00")
|
||||
|
||||
@pytest.mark.skip("Skipped due to issue #5421")
|
||||
@pytest.mark.usefixtures("page","risk_accepted", "auth")
|
||||
def test_funding_payment_history(perps_market, page: Page, vega):
|
||||
page.goto(f"/#/markets/{perps_market}")
|
||||
change_keys(page, vega, "market_maker")
|
||||
page.get_by_test_id("Funding history").click()
|
||||
element = page.get_by_test_id("tab-funding-history")
|
||||
# Get the bounding box of the element
|
||||
bounding_box = element.bounding_box()
|
||||
if bounding_box:
|
||||
bottom_right_x = bounding_box["x"] + bounding_box["width"]
|
||||
bottom_right_y = bounding_box["y"] + bounding_box["height"]
|
||||
|
||||
# Hover over the bottom-right corner of the element
|
||||
element.hover(position={"x": bottom_right_x, "y": bottom_right_y})
|
||||
else:
|
||||
print("Bounding box not found for the element")
|
||||
|
||||
@pytest.mark.usefixtures("page","risk_accepted", "auth")
|
||||
def test_perps_market_termination_proposed(page: Page, vega: VegaService):
|
||||
perpetual_market = setup_perps_market(vega)
|
||||
page.goto(f"/#/markets/{perpetual_market}")
|
||||
vega.update_market_state(
|
||||
proposal_key=MM_WALLET.name,
|
||||
market_id=perpetual_market,
|
||||
market_state=MarketStateUpdateType.Terminate,
|
||||
price=100,
|
||||
vote_closing_time = datetime.now() + timedelta(seconds=15),
|
||||
vote_enactment_time = datetime.now() + timedelta(seconds=60),
|
||||
approve_proposal = True,
|
||||
forward_time_to_enactment = False,
|
||||
)
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
banner_text = page.get_by_test_id(f"termination-warning-banner-{perpetual_market}").text_content()
|
||||
pattern = re.compile(
|
||||
r"Trading on Market BTC:DAI_Perpetual may stop on \d{2} [A-Za-z]+\. There is open proposal to close this market\.Proposed final price is 100\.00 BTC\.View proposal"
|
||||
)
|
||||
assert pattern.search(banner_text), f"Text did not match pattern. Text was: {banner_text}"
|
||||
|
||||
@pytest.mark.usefixtures("page","risk_accepted", "auth" )
|
||||
def test_perps_market_terminated(page: Page, vega: VegaService):
|
||||
perpetual_market = setup_perps_market(vega)
|
||||
vega.update_market_state(
|
||||
proposal_key=MM_WALLET.name,
|
||||
market_id=perpetual_market,
|
||||
market_state=MarketStateUpdateType.Terminate,
|
||||
price=100,
|
||||
approve_proposal = True,
|
||||
forward_time_to_enactment = True,
|
||||
)
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
page.goto(f"/#/markets/{perpetual_market}")
|
||||
expect(page.get_by_test_id("market-price")).to_have_text("Mark Price100.00")
|
||||
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)-")
|
||||
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
|
||||
expect(page.get_by_test_id("market-trading-mode")).to_have_text("Trading modeNo trading")
|
||||
expect(page.get_by_test_id("market-state")).to_have_text("StatusClosed")
|
||||
expect(page.get_by_test_id("liquidity-supplied")).to_have_text("Liquidity supplied 0.00 (0.00%)")
|
||||
expect(page.get_by_test_id("market-funding")).to_have_text("Funding Rate / Countdown-Unknown")
|
||||
expect(page.get_by_test_id("index-price")).to_have_text("Index Price-")
|
||||
expect(page.get_by_test_id("deal-ticket-error-message-summary")).to_have_text("This market is closed and not accepting orders")
|
||||
@@ -0,0 +1,116 @@
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
from playwright.sync_api import Page, expect, Route
|
||||
from vega_sim.service import VegaService
|
||||
from conftest import init_vega
|
||||
from fixtures.market import setup_continuous_market
|
||||
|
||||
order_size = "order-size"
|
||||
order_price = "order-price"
|
||||
place_order = "place-order"
|
||||
order_side_sell = "order-side-SIDE_SELL"
|
||||
market_order = "order-type-Market"
|
||||
tif = "order-tif"
|
||||
expire = "expire"
|
||||
api_request_match = r"http://localhost:\d+/api/v2/requests"
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega(request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def continuous_market(vega):
|
||||
return setup_continuous_market(vega)
|
||||
|
||||
def handle_route_connection_lost(route: Route, request):
|
||||
if request.method == "POST" and re.match(api_request_match, request.url):
|
||||
route.fulfill(
|
||||
status=200,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body='{"jsonrpc": "2.0", "id": "1"}'
|
||||
)
|
||||
else:
|
||||
route.continue_()
|
||||
|
||||
def handle_route_connection_rejected(route: Route, request):
|
||||
if request.method == "POST" and re.match(api_request_match, request.url):
|
||||
custom_response = {
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": 3001,
|
||||
"data": "the user rejected the wallet connection",
|
||||
"message": "User error"
|
||||
},
|
||||
"id": "0"
|
||||
}
|
||||
route.fulfill(
|
||||
status=400,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=json.dumps(custom_response)
|
||||
)
|
||||
else:
|
||||
route.continue_()
|
||||
|
||||
def assert_connection_approve(route: Route, request, page:Page):
|
||||
if request.method == "POST" and re.match(api_request_match, request.url):
|
||||
expect(page.get_by_test_id("toast-content")).to_have_text("Please go to your Vega wallet application and approve or reject the transaction.")
|
||||
else:
|
||||
route.continue_()
|
||||
|
||||
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
|
||||
def test_wallet_connection_error(continuous_market, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.route("**/*", handle_route_connection_lost)
|
||||
page.get_by_test_id("connect-vega-wallet").click()
|
||||
page.get_by_test_id("connector-jsonRpc").click()
|
||||
expect(page.get_by_test_id("wallet-dialog-title")).to_have_text("Something went wrong")
|
||||
|
||||
@pytest.mark.usefixtures("page", "risk_accepted")
|
||||
def test_wallet_connection_rejected(continuous_market, page: Page):
|
||||
# 0002-WCON-002
|
||||
# 0002-WCON-005
|
||||
# 0002-WCON-007
|
||||
# 0002-WCON-015
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.route("**/*", handle_route_connection_rejected)
|
||||
page.get_by_test_id("connect-vega-wallet").click()
|
||||
page.get_by_test_id("connector-jsonRpc").click()
|
||||
expect(page.get_by_test_id("dialog-content").nth(1)).to_have_text("User errorthe user rejected the wallet connectionTry againAbout the Vega wallet | Supported browsers ")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
|
||||
def test_wallet_connection_error_transaction(continuous_market, vega: VegaService, page: Page):
|
||||
# 0003-WTXN-009
|
||||
# 0003-WTXN-011
|
||||
# 0002-WCON-016
|
||||
# 0003-WTXN-008
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(order_size).fill("10")
|
||||
page.get_by_test_id(order_price).fill("120")
|
||||
page.route("**/*", handle_route_connection_lost)
|
||||
page.get_by_test_id(place_order).click()
|
||||
expect(page.get_by_test_id("toast-content")).to_have_text("Wallet disconnectedThe connection to your Vega Wallet has been lost.Connect vega wallet")
|
||||
|
||||
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
|
||||
def test_wallet_transaction_rejected(continuous_market, vega: VegaService, page: Page):
|
||||
# 0003-WTXN-007
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(order_size).fill("10")
|
||||
page.get_by_test_id(order_price).fill("120")
|
||||
page.route("**/*", handle_route_connection_rejected)
|
||||
page.get_by_test_id(place_order).click()
|
||||
expect(page.get_by_test_id("toast-content")).to_have_text("Error occurredthe user rejected the wallet connection")
|
||||
|
||||
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
|
||||
def test_wallet_connection_approve(continuous_market, vega: VegaService, page: Page):
|
||||
# 0002-WCON-005
|
||||
# 0002-WCON-007
|
||||
# 0002-WCON-009
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(order_size).fill("10")
|
||||
page.get_by_test_id(order_price).fill("120")
|
||||
page.route("**/*", assert_connection_approve)
|
||||
page.get_by_test_id(place_order).click()
|
||||
@@ -0,0 +1,60 @@
|
||||
import * as router from 'react-router';
|
||||
import { useNavigateToLastMarket } from './use-navigate-to-last-market';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useTopTradedMarkets } from './use-top-traded-markets';
|
||||
import { Links } from '../links';
|
||||
|
||||
const mockLastMarketId = 'LAST';
|
||||
|
||||
jest.mock('../../stores', () => {
|
||||
const original = jest.requireActual('../../stores');
|
||||
return {
|
||||
...original,
|
||||
useGlobalStore: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('./use-top-traded-markets', () => {
|
||||
return {
|
||||
useTopTradedMarkets: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useNavigateToLastMarket', () => {
|
||||
const navigate = jest.fn();
|
||||
beforeAll(() => {
|
||||
jest.spyOn(router, 'useNavigate').mockImplementation(() => navigate);
|
||||
});
|
||||
|
||||
it('navigates to the last market when it is active', () => {
|
||||
(useGlobalStore as unknown as jest.Mock).mockReturnValue(mockLastMarketId);
|
||||
(useTopTradedMarkets as jest.Mock).mockReturnValue({
|
||||
data: [{ id: mockLastMarketId }],
|
||||
});
|
||||
renderHook(() => useNavigateToLastMarket());
|
||||
expect(navigate).toHaveBeenCalledWith(Links.MARKET(mockLastMarketId), {
|
||||
replace: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to the top traded market if the last one is not active', () => {
|
||||
(useGlobalStore as unknown as jest.Mock).mockReturnValue(mockLastMarketId);
|
||||
(useTopTradedMarkets as jest.Mock).mockReturnValue({
|
||||
data: [{ id: 'TOP' }],
|
||||
});
|
||||
renderHook(() => useNavigateToLastMarket());
|
||||
expect(navigate).toHaveBeenCalledWith(Links.MARKET('TOP'), {
|
||||
replace: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to the list of markets when all of the markets are not active', () => {
|
||||
(useGlobalStore as unknown as jest.Mock).mockReturnValue(mockLastMarketId);
|
||||
(useTopTradedMarkets as jest.Mock).mockReturnValue({
|
||||
data: [],
|
||||
});
|
||||
renderHook(() => useNavigateToLastMarket());
|
||||
expect(navigate).toHaveBeenCalledWith(Links.MARKETS());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useGlobalStore } from '../../stores';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTopTradedMarkets } from './use-top-traded-markets';
|
||||
import { useEffect } from 'react';
|
||||
import { Links } from '../links';
|
||||
|
||||
export const useNavigateToLastMarket = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// this returns a list of active markets ordered by traded factor
|
||||
// hence there's no need to pull markets again or find out in separate
|
||||
// query of the state of last market
|
||||
const { data } = useTopTradedMarkets();
|
||||
const lastMarketId = useGlobalStore((store) => store.marketId);
|
||||
const isLastMarketActive = data?.some((m) => m.id === lastMarketId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
// if last market id is set and it is active, navigate to that market
|
||||
if (lastMarketId && isLastMarketActive) {
|
||||
navigate(Links.MARKET(lastMarketId), {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise if there's a top traded market, navigate to that market
|
||||
const marketDataId = data[0]?.id;
|
||||
if (marketDataId) {
|
||||
navigate(Links.MARKET(marketDataId), {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise navigate to the list of all markets
|
||||
navigate(Links.MARKETS());
|
||||
}, [lastMarketId, data, navigate, isLastMarketActive]);
|
||||
};
|
||||
@@ -6,6 +6,8 @@ import type { HttpBackendOptions, RequestCallback } from 'i18next-http-backend';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
export const supportedLngs = ['en'];
|
||||
|
||||
const isInDev = process.env.NODE_ENV === 'development';
|
||||
const useLocize = isInDev && !!process.env.NX_USE_LOCIZE;
|
||||
|
||||
@@ -51,9 +53,8 @@ i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
lng: 'en',
|
||||
fallbackLng: 'en',
|
||||
supportedLngs: ['en'],
|
||||
supportedLngs,
|
||||
load: 'languageOnly',
|
||||
// have a common namespace used around the full app
|
||||
ns: [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
export const ns = 'trading';
|
||||
export const useT = () => useTranslation('trading').t;
|
||||
export const useI18n = () => useTranslation('trading').i18n;
|
||||
|
||||
@@ -32,7 +32,6 @@ import { SSRLoader } from './ssr-loader';
|
||||
import { PartyActiveOrdersHandler } from './party-active-orders-handler';
|
||||
import { MaybeConnectEagerly } from './maybe-connect-eagerly';
|
||||
import { TransactionHandlers } from './transaction-handlers';
|
||||
import '../lib/i18n';
|
||||
import { useT } from '../lib/use-t';
|
||||
|
||||
const Title = () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Routes as AppRoutes } from '../lib/links';
|
||||
import { LayoutWithSky } from '../client-pages/referrals/layout';
|
||||
import { Referrals } from '../client-pages/referrals/referrals';
|
||||
import { ReferralStatistics } from '../client-pages/referrals/referral-statistics';
|
||||
import { ApplyCodeForm } from '../client-pages/referrals/apply-code-form';
|
||||
import { ApplyCodeFormContainer } from '../client-pages/referrals/apply-code-form';
|
||||
import { CreateCodeContainer } from '../client-pages/referrals/create-code-form';
|
||||
import { NotFound as ReferralNotFound } from '../client-pages/referrals/error-boundary';
|
||||
import { compact } from 'lodash';
|
||||
@@ -79,7 +79,7 @@ export const routerConfig: RouteObject[] = compact([
|
||||
},
|
||||
{
|
||||
path: AppRoutes.REFERRALS_APPLY_CODE,
|
||||
element: <ApplyCodeForm />,
|
||||
element: <ApplyCodeFormContainer />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -170,7 +170,7 @@ html [data-theme='dark'] {
|
||||
|
||||
.ag-theme-balham,
|
||||
.ag-theme-balham-dark {
|
||||
--ag-grid-size: 2px; /* Used for compactness */
|
||||
--ag-grid-size: 3px; /* Used for compactness */
|
||||
--ag-row-height: 36px;
|
||||
--ag-header-height: 28px;
|
||||
}
|
||||
@@ -184,7 +184,7 @@ html [data-theme='dark'] {
|
||||
|
||||
/* Light variables */
|
||||
.ag-theme-balham {
|
||||
--ag-background-color: transparent;
|
||||
--ag-background-color: theme(colors.vega.clight.900);
|
||||
--ag-border-color: theme(colors.vega.clight.600);
|
||||
--ag-header-background-color: theme(colors.vega.clight.700);
|
||||
--ag-odd-row-background-color: transparent;
|
||||
@@ -196,7 +196,7 @@ html [data-theme='dark'] {
|
||||
|
||||
/* Dark variables */
|
||||
.ag-theme-balham-dark {
|
||||
--ag-background-color: transparent;
|
||||
--ag-background-color: theme(colors.vega.cdark.900);
|
||||
--ag-border-color: theme(colors.vega.cdark.600);
|
||||
--ag-header-background-color: theme(colors.vega.cdark.700);
|
||||
--ag-odd-row-background-color: transparent;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
|
||||
import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
|
||||
import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
|
||||
import { Links } from '../lib/links';
|
||||
import { useReferralToasts } from '../client-pages/referrals/hooks/use-referral-toasts';
|
||||
|
||||
export const ToastsManager = () => {
|
||||
useProposalToasts();
|
||||
@@ -14,6 +15,7 @@ export const ToastsManager = () => {
|
||||
useReadyToWithdrawalToasts({
|
||||
withdrawalsLink: Links.PORTFOLIO(),
|
||||
});
|
||||
useReferralToasts();
|
||||
|
||||
const toasts = useToasts((store) => store.toasts);
|
||||
return <ToastsContainer order="desc" toasts={toasts} />;
|
||||
|
||||
@@ -13,10 +13,10 @@ import { type IterableElement } from 'type-fest';
|
||||
import {
|
||||
AccountEventsDocument,
|
||||
AccountsDocument,
|
||||
AccountFieldsFragment,
|
||||
AccountsQuery,
|
||||
AccountEventsSubscription,
|
||||
AccountsQueryVariables,
|
||||
type AccountFieldsFragment,
|
||||
type AccountsQuery,
|
||||
type AccountEventsSubscription,
|
||||
type AccountsQueryVariables,
|
||||
} from './__generated__/Accounts';
|
||||
import { type Asset } from '@vegaprotocol/assets';
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ALLOWED_ACCOUNTS = [
|
||||
|
||||
export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
const t = useT();
|
||||
const { pubKey, pubKeys } = useVegaWallet();
|
||||
const { pubKey, pubKeys, isReadOnly } = useVegaWallet();
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.transfer_fee_factor,
|
||||
NetworkParams.transfer_minTransferQuantumMultiple,
|
||||
@@ -70,6 +70,7 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
<TransferForm
|
||||
pubKey={pubKey}
|
||||
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
|
||||
isReadOnly={isReadOnly}
|
||||
assetId={assetId}
|
||||
feeFactor={params.transfer_fee_factor}
|
||||
minQuantumMultiple={params.transfer_minTransferQuantumMultiple}
|
||||
|
||||
@@ -73,6 +73,28 @@ describe('TransferForm', () => {
|
||||
minQuantumMultiple: '1',
|
||||
};
|
||||
|
||||
const propsNoAssets = {
|
||||
pubKey,
|
||||
pubKeys: [
|
||||
pubKey,
|
||||
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
|
||||
],
|
||||
feeFactor: '0.001',
|
||||
submitTransfer: jest.fn(),
|
||||
accounts: [],
|
||||
minQuantumMultiple: '1',
|
||||
};
|
||||
|
||||
it('renders no assets', async () => {
|
||||
renderComponent(propsNoAssets);
|
||||
expect(screen.getByTestId('no-assets-available')).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders no accounts', async () => {
|
||||
renderComponent(propsNoAssets);
|
||||
expect(screen.getByTestId('no-accounts-available')).toBeVisible();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
targetText: 'Include transfer fee',
|
||||
|
||||
@@ -45,6 +45,7 @@ interface Asset {
|
||||
export interface TransferFormProps {
|
||||
pubKey: string | null;
|
||||
pubKeys: string[] | null;
|
||||
isReadOnly?: boolean;
|
||||
accounts: Array<{
|
||||
type: AccountType;
|
||||
balance: string;
|
||||
@@ -59,6 +60,7 @@ export interface TransferFormProps {
|
||||
export const TransferForm = ({
|
||||
pubKey,
|
||||
pubKeys,
|
||||
isReadOnly,
|
||||
assetId: initialAssetId,
|
||||
feeFactor,
|
||||
submitTransfer,
|
||||
@@ -201,27 +203,36 @@ export const TransferForm = ({
|
||||
<Controller
|
||||
control={control}
|
||||
name="asset"
|
||||
render={({ field }) => (
|
||||
<TradingRichSelect
|
||||
data-testid="select-asset"
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
setValue('fromAccount', '');
|
||||
}}
|
||||
placeholder={t('Please select an asset')}
|
||||
value={field.value}
|
||||
>
|
||||
{assets.map((a) => (
|
||||
<AssetOption
|
||||
key={a.key}
|
||||
asset={a}
|
||||
balance={<Balance balance={a.balance} symbol={a.symbol} />}
|
||||
/>
|
||||
))}
|
||||
</TradingRichSelect>
|
||||
)}
|
||||
render={({ field }) =>
|
||||
assets.length > 0 ? (
|
||||
<TradingRichSelect
|
||||
data-testid="select-asset"
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
setValue('fromAccount', '');
|
||||
}}
|
||||
placeholder={t('Please select an asset')}
|
||||
value={field.value}
|
||||
>
|
||||
{assets.map((a) => (
|
||||
<AssetOption
|
||||
key={a.key}
|
||||
asset={a}
|
||||
balance={<Balance balance={a.balance} symbol={a.symbol} />}
|
||||
/>
|
||||
))}
|
||||
</TradingRichSelect>
|
||||
) : (
|
||||
<span
|
||||
data-testid="no-assets-available"
|
||||
className="text-xs text-vega-clight-100 dark:text-vega-cdark-100"
|
||||
>
|
||||
{t('No assets available')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{errors.asset?.message && (
|
||||
<TradingInputError forInput="asset">
|
||||
@@ -249,48 +260,57 @@ export const TransferForm = ({
|
||||
},
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TradingSelect
|
||||
id="fromAccount"
|
||||
defaultValue=""
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
render={({ field }) =>
|
||||
accounts.length > 0 ? (
|
||||
<TradingSelect
|
||||
id="fromAccount"
|
||||
defaultValue=""
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
|
||||
const [type] = parseFromAccount(e.target.value);
|
||||
const [type] = parseFromAccount(e.target.value);
|
||||
|
||||
// Enforce that if transferring from a vested rewards account it must go to
|
||||
// the current connected general account
|
||||
if (
|
||||
type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS &&
|
||||
pubKey
|
||||
) {
|
||||
setValue('toVegaKey', pubKey);
|
||||
setToVegaKeyMode('select');
|
||||
setIncludeFee(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{accounts
|
||||
.filter((a) => {
|
||||
if (!selectedAssetId) return true;
|
||||
return selectedAssetId === a.asset.id;
|
||||
})
|
||||
.map((a) => {
|
||||
const id = `${a.type}-${a.asset.id}`;
|
||||
return (
|
||||
<option value={id} key={id}>
|
||||
{AccountTypeMapping[a.type]} (
|
||||
{addDecimal(a.balance, a.asset.decimals)} {a.asset.symbol}
|
||||
)
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
)}
|
||||
// Enforce that if transferring from a vested rewards account it must go to
|
||||
// the current connected general account
|
||||
if (
|
||||
type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS &&
|
||||
pubKey
|
||||
) {
|
||||
setValue('toVegaKey', pubKey);
|
||||
setToVegaKeyMode('select');
|
||||
setIncludeFee(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{accounts
|
||||
.filter((a) => {
|
||||
if (!selectedAssetId) return true;
|
||||
return selectedAssetId === a.asset.id;
|
||||
})
|
||||
.map((a) => {
|
||||
const id = `${a.type}-${a.asset.id}`;
|
||||
return (
|
||||
<option value={id} key={id}>
|
||||
{AccountTypeMapping[a.type]} (
|
||||
{addDecimal(a.balance, a.asset.decimals)}{' '}
|
||||
{a.asset.symbol})
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
) : (
|
||||
<span
|
||||
data-testid="no-accounts-available"
|
||||
className="text-xs text-vega-clight-100 dark:text-vega-cdark-100"
|
||||
>
|
||||
{t('No accounts available')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{errors.fromAccount?.message && (
|
||||
<TradingInputError forInput="fromAccount">
|
||||
@@ -454,7 +474,7 @@ export const TransferForm = ({
|
||||
decimals={asset?.decimals}
|
||||
/>
|
||||
)}
|
||||
<TradingButton type="submit" fill={true}>
|
||||
<TradingButton type="submit" fill={true} disabled={isReadOnly}>
|
||||
{t('Confirm transfer')}
|
||||
</TradingButton>
|
||||
</form>
|
||||
|
||||
@@ -18,7 +18,7 @@ type Rows = {
|
||||
key: AssetDetail;
|
||||
label: string;
|
||||
tooltip: string;
|
||||
value: (asset: Asset) => ReactNode | undefined;
|
||||
value: (asset: Asset, orignalAsset?: Asset) => ReactNode | undefined;
|
||||
valueTooltip?: (asset: Asset) => string | null | undefined;
|
||||
}[];
|
||||
|
||||
@@ -52,6 +52,21 @@ const num = (asset: Asset, n: string | undefined | null) => {
|
||||
return addDecimalsFormatNumber(n, asset.decimals);
|
||||
};
|
||||
|
||||
const Diff = ({
|
||||
oldValue,
|
||||
newValue,
|
||||
}: {
|
||||
oldValue: ReactNode;
|
||||
newValue: ReactNode;
|
||||
}) => (
|
||||
<span className="flex gap-1">
|
||||
<span className="line-through bg-vega-red-300 dark:bg-vega-red-600">
|
||||
{oldValue}
|
||||
</span>
|
||||
<span className="bg-vega-green-300 dark:bg-vega-green-600">{newValue}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
export const useRows = () => {
|
||||
const t = useT();
|
||||
const AssetTypeMapping = useAssetTypeMapping();
|
||||
@@ -103,7 +118,14 @@ export const useRows = () => {
|
||||
key: AssetDetail.QUANTUM,
|
||||
label: t('Quantum'),
|
||||
tooltip: t('The minimum economically meaningful amount of the asset'),
|
||||
value: (asset) => num(asset, asset.quantum),
|
||||
value: (asset, originalAsset) => {
|
||||
const value = num(asset, asset.quantum);
|
||||
if (originalAsset && originalAsset.quantum !== asset.quantum) {
|
||||
const original = num(originalAsset, originalAsset.quantum);
|
||||
return <Diff oldValue={original} newValue={value} />;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AssetDetail.STATUS,
|
||||
@@ -143,8 +165,24 @@ export const useRows = () => {
|
||||
tooltip: t('WITHDRAW_THRESHOLD_TOOLTIP_TEXT', {
|
||||
defaultValue: WITHDRAW_THRESHOLD_TOOLTIP_TEXT,
|
||||
}),
|
||||
value: (asset) =>
|
||||
num(asset, (asset.source as Schema.ERC20).withdrawThreshold),
|
||||
value: (asset, originalAsset) => {
|
||||
const value = num(
|
||||
asset,
|
||||
(asset.source as Schema.ERC20).withdrawThreshold
|
||||
);
|
||||
if (
|
||||
originalAsset &&
|
||||
(originalAsset.source as Schema.ERC20).withdrawThreshold !==
|
||||
(asset.source as Schema.ERC20).withdrawThreshold
|
||||
) {
|
||||
const original = num(
|
||||
asset,
|
||||
(originalAsset.source as Schema.ERC20).withdrawThreshold
|
||||
);
|
||||
return <Diff oldValue={original} newValue={value} />;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AssetDetail.LIFETIME_LIMIT,
|
||||
@@ -152,8 +190,26 @@ export const useRows = () => {
|
||||
tooltip: t(
|
||||
'The lifetime deposit limit per address. Note: this is a temporary measure that can be changed or removed through governance'
|
||||
),
|
||||
value: (asset) =>
|
||||
num(asset, (asset.source as Schema.ERC20).lifetimeLimit),
|
||||
value: (asset, originalAsset) => {
|
||||
const value = num(
|
||||
asset,
|
||||
(asset.source as Schema.ERC20).lifetimeLimit
|
||||
);
|
||||
|
||||
if (
|
||||
originalAsset &&
|
||||
(originalAsset.source as Schema.ERC20).lifetimeLimit !==
|
||||
(asset.source as Schema.ERC20).lifetimeLimit
|
||||
) {
|
||||
const original = num(
|
||||
asset,
|
||||
(originalAsset.source as Schema.ERC20).lifetimeLimit
|
||||
);
|
||||
return <Diff oldValue={original} newValue={value} />;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AssetDetail.MAX_FAUCET_AMOUNT_MINT,
|
||||
@@ -261,10 +317,13 @@ export const testId = (detail: AssetDetail, field: 'label' | 'value') =>
|
||||
|
||||
export type AssetDetailsTableProps = {
|
||||
asset: Asset;
|
||||
originalAsset?: Asset;
|
||||
|
||||
omitRows?: AssetDetail[];
|
||||
} & Omit<KeyValueTableRowProps, 'children'>;
|
||||
export const AssetDetailsTable = ({
|
||||
asset,
|
||||
originalAsset,
|
||||
omitRows = [],
|
||||
...props
|
||||
}: AssetDetailsTableProps) => {
|
||||
@@ -275,7 +334,7 @@ export const AssetDetailsTable = ({
|
||||
|
||||
const details = useRows().map((r) => ({
|
||||
...r,
|
||||
value: r.value(asset),
|
||||
value: r.value(asset, originalAsset),
|
||||
valueTooltip: r.valueTooltip?.(asset),
|
||||
}));
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { ApolloError } from '@apollo/client';
|
||||
import type { GraphQLErrors } from '@apollo/client/errors';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { Subscription, Observable } from 'zen-observable-ts';
|
||||
import { type Subscription, type Observable } from 'zen-observable-ts';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
type Item = {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export const useTranslation = () => ({
|
||||
t: (label: string, replacements?: Record<string, string>) => {
|
||||
const replace =
|
||||
replacements?.['replace'] && typeof replacements === 'object'
|
||||
? replacements?.['replace']
|
||||
: replacements;
|
||||
let translatedLabel = replacements?.['defaultValue'] || label;
|
||||
if (typeof replace === 'object' && replace !== null) {
|
||||
Object.keys(replace).forEach((key) => {
|
||||
translatedLabel = translatedLabel.replace(`{{${key}}}`, replace[key]);
|
||||
});
|
||||
}
|
||||
return translatedLabel;
|
||||
},
|
||||
});
|
||||
@@ -36,10 +36,10 @@ export const AgGridThemed = ({
|
||||
<div className={wrapperClasses}>
|
||||
<AgGridReact
|
||||
defaultColDef={defaultColDef}
|
||||
ref={gridRef}
|
||||
overlayLoadingTemplate={t('Loading...')}
|
||||
overlayNoRowsTemplate={t('No data')}
|
||||
suppressDragLeaveHidesColumns
|
||||
ref={gridRef}
|
||||
{...defaultProps}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('Pagination', () => {
|
||||
const mockOnLoad = jest.fn();
|
||||
const count = 10;
|
||||
render(<Pagination {...props} count={count} onLoad={mockOnLoad} />);
|
||||
expect(screen.getByText(`${count} rows loaded`)).toBeInTheDocument();
|
||||
expect(screen.getByText('10 rows loaded')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
|
||||
expect(mockOnLoad).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -18,19 +18,15 @@ export const Pagination = ({
|
||||
let rowMessage = '';
|
||||
|
||||
if (count && !pageInfo?.hasNextPage) {
|
||||
rowMessage = t('paginationAllLoaded', {
|
||||
replace: { count },
|
||||
defaultValue: 'All {{count}} rows loaded',
|
||||
rowMessage = t('paginationAllLoaded', 'all {{count}} rows loaded', {
|
||||
count,
|
||||
});
|
||||
} else {
|
||||
rowMessage = t('paginationLoaded', {
|
||||
replace: { count },
|
||||
defaultValue: '{{count}} rows loaded',
|
||||
});
|
||||
rowMessage = t('paginationLoaded', '{{count}} rows loaded', { count });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-1 border-t border-default">
|
||||
<div className="border-default flex items-center justify-between border-t p-1">
|
||||
<div className="text-xs">
|
||||
{false}
|
||||
{showRetentionMessage &&
|
||||
@@ -47,7 +43,7 @@ export const Pagination = ({
|
||||
) : null}
|
||||
</div>
|
||||
{count && hasDisplayedRows === false ? (
|
||||
<div className="absolute text-xs top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform text-xs">
|
||||
{t('No rows matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
import { locales } from '@vegaprotocol/i18n';
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
// Set up i18n instance so that components have the correct default
|
||||
// en translations
|
||||
i18n.use(initReactI18next).init({
|
||||
// we init with resources
|
||||
resources: locales,
|
||||
fallbackLng: 'en',
|
||||
ns: ['datagrid'],
|
||||
defaultNS: 'datagrid',
|
||||
});
|
||||
|
||||
global.ResizeObserver = ResizeObserver;
|
||||
|
||||
@@ -77,12 +77,10 @@ export const DealTicketFeeDetails = ({
|
||||
label={
|
||||
<>
|
||||
{t('Fees')}
|
||||
{totalDiscountFactor ? (
|
||||
{totalDiscountFactor !== '0' ? (
|
||||
<Pill size="xxs" intent={Intent.Info} className="ml-1">
|
||||
-
|
||||
{formatNumberPercentage(
|
||||
new BigNumber(totalDiscountFactor).multipliedBy(100),
|
||||
2
|
||||
new BigNumber(totalDiscountFactor).multipliedBy(100)
|
||||
)}
|
||||
</Pill>
|
||||
) : null}
|
||||
@@ -105,10 +103,7 @@ export const DealTicketFeeDetails = ({
|
||||
)}
|
||||
</p>
|
||||
<FeesBreakdown
|
||||
totalFeeAmount={feeEstimate?.totalFeeAmount}
|
||||
referralDiscountFactor={feeEstimate?.referralDiscountFactor}
|
||||
volumeDiscountFactor={feeEstimate?.volumeDiscountFactor}
|
||||
fees={feeEstimate?.fees}
|
||||
feeEstimate={feeEstimate}
|
||||
feeFactors={market.fees.factors}
|
||||
symbol={assetSymbol}
|
||||
decimals={assetDecimals}
|
||||
|
||||
@@ -59,9 +59,7 @@ export const TimeInForceSelector = ({
|
||||
components={[
|
||||
<Tooltip
|
||||
description={
|
||||
<SimpleGrid
|
||||
grid={compileGridData(t, market, marketData, t)}
|
||||
/>
|
||||
<SimpleGrid grid={compileGridData(t, market, marketData)} />
|
||||
}
|
||||
>
|
||||
sufficient liquidity
|
||||
@@ -83,9 +81,7 @@ export const TimeInForceSelector = ({
|
||||
components={[
|
||||
<Tooltip
|
||||
description={
|
||||
<SimpleGrid
|
||||
grid={compileGridData(t, market, marketData, t)}
|
||||
/>
|
||||
<SimpleGrid grid={compileGridData(t, market, marketData)} />
|
||||
}
|
||||
>
|
||||
high price volatility
|
||||
|
||||
@@ -6,16 +6,19 @@ describe('getDiscountedFee', () => {
|
||||
discountedFee: '100',
|
||||
volumeDiscount: '0',
|
||||
referralDiscount: '0',
|
||||
totalDiscount: '0',
|
||||
});
|
||||
expect(getDiscountedFee('100', undefined, '0.1')).toEqual({
|
||||
discountedFee: '90',
|
||||
volumeDiscount: '10',
|
||||
referralDiscount: '0',
|
||||
totalDiscount: '10',
|
||||
});
|
||||
expect(getDiscountedFee('100', '0.1', undefined)).toEqual({
|
||||
discountedFee: '90',
|
||||
volumeDiscount: '0',
|
||||
referralDiscount: '10',
|
||||
totalDiscount: '10',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +27,7 @@ describe('getDiscountedFee', () => {
|
||||
discountedFee: '',
|
||||
volumeDiscount: '0',
|
||||
referralDiscount: '0',
|
||||
totalDiscount: '0',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -35,7 +39,7 @@ describe('getTotalDiscountFactor', () => {
|
||||
volumeDiscountFactor: '0',
|
||||
referralDiscountFactor: '0',
|
||||
})
|
||||
).toEqual(0);
|
||||
).toEqual('0');
|
||||
});
|
||||
|
||||
it('returns volumeDiscountFactor if referralDiscountFactor is 0', () => {
|
||||
@@ -44,7 +48,7 @@ describe('getTotalDiscountFactor', () => {
|
||||
volumeDiscountFactor: '0.1',
|
||||
referralDiscountFactor: '0',
|
||||
})
|
||||
).toEqual(0.1);
|
||||
).toEqual('-0.1');
|
||||
});
|
||||
it('returns referralDiscountFactor if volumeDiscountFactor is 0', () => {
|
||||
expect(
|
||||
@@ -52,7 +56,7 @@ describe('getTotalDiscountFactor', () => {
|
||||
volumeDiscountFactor: '0',
|
||||
referralDiscountFactor: '0.1',
|
||||
})
|
||||
).toEqual(0.1);
|
||||
).toEqual('-0.1');
|
||||
});
|
||||
|
||||
it('calculates discount using referralDiscountFactor and volumeDiscountFactor', () => {
|
||||
@@ -61,6 +65,6 @@ describe('getTotalDiscountFactor', () => {
|
||||
volumeDiscountFactor: '0.2',
|
||||
referralDiscountFactor: '0.1',
|
||||
})
|
||||
).toBeCloseTo(0.28);
|
||||
).toBe('-0.28');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ export const getDiscountedFee = (
|
||||
discountedFee: feeAmount,
|
||||
volumeDiscount: '0',
|
||||
referralDiscount: '0',
|
||||
totalDiscount: '0',
|
||||
};
|
||||
}
|
||||
const referralDiscount = new BigNumber(referralDiscountFactor || '0')
|
||||
@@ -23,12 +24,14 @@ export const getDiscountedFee = (
|
||||
const volumeDiscount = new BigNumber(volumeDiscountFactor || '0')
|
||||
.multipliedBy((BigInt(feeAmount) - BigInt(referralDiscount)).toString())
|
||||
.toFixed(0, BigNumber.ROUND_FLOOR);
|
||||
const totalDiscount = (
|
||||
BigInt(referralDiscount) + BigInt(volumeDiscount)
|
||||
).toString();
|
||||
const discountedFee = (
|
||||
BigInt(feeAmount || '0') -
|
||||
BigInt(referralDiscount) -
|
||||
BigInt(volumeDiscount)
|
||||
BigInt(feeAmount || '0') - BigInt(totalDiscount)
|
||||
).toString();
|
||||
return {
|
||||
totalDiscount,
|
||||
referralDiscount,
|
||||
volumeDiscount,
|
||||
discountedFee,
|
||||
@@ -39,16 +42,28 @@ export const getTotalDiscountFactor = (feeEstimate?: {
|
||||
volumeDiscountFactor?: string;
|
||||
referralDiscountFactor?: string;
|
||||
}) => {
|
||||
if (!feeEstimate) {
|
||||
return 0;
|
||||
if (
|
||||
!feeEstimate ||
|
||||
(feeEstimate.referralDiscountFactor === '0' &&
|
||||
feeEstimate.volumeDiscountFactor === '0')
|
||||
) {
|
||||
return '0';
|
||||
}
|
||||
const volumeFactor = Number(feeEstimate?.volumeDiscountFactor) || 0;
|
||||
const referralFactor = Number(feeEstimate?.referralDiscountFactor) || 0;
|
||||
if (!volumeFactor) {
|
||||
return referralFactor;
|
||||
const volumeFactor = new BigNumber(
|
||||
feeEstimate?.volumeDiscountFactor || 0
|
||||
).minus(1);
|
||||
const referralFactor = new BigNumber(
|
||||
feeEstimate?.referralDiscountFactor || 0
|
||||
).minus(1);
|
||||
if (volumeFactor.isZero()) {
|
||||
return feeEstimate.referralDiscountFactor
|
||||
? `-${feeEstimate.referralDiscountFactor}`
|
||||
: '0';
|
||||
}
|
||||
if (!referralFactor) {
|
||||
return volumeFactor;
|
||||
if (referralFactor.isZero()) {
|
||||
return feeEstimate.volumeDiscountFactor
|
||||
? `-${feeEstimate.volumeDiscountFactor}`
|
||||
: '0';
|
||||
}
|
||||
return 1 - (1 - volumeFactor) * (1 - referralFactor);
|
||||
return volumeFactor.multipliedBy(referralFactor).minus(1).toString();
|
||||
};
|
||||
|
||||
@@ -14,13 +14,16 @@ describe('FeesBreakdown', () => {
|
||||
liquidityFee: '100',
|
||||
};
|
||||
const props = {
|
||||
totalFeeAmount: '100',
|
||||
fees,
|
||||
feeFactors,
|
||||
symbol: 'USD',
|
||||
decimals: 2,
|
||||
referralDiscountFactor: '0.01',
|
||||
volumeDiscountFactor: '0.01',
|
||||
|
||||
feeEstimate: {
|
||||
referralDiscountFactor: '0.01',
|
||||
volumeDiscountFactor: '0.01',
|
||||
totalFeeAmount: '100',
|
||||
fees,
|
||||
},
|
||||
};
|
||||
render(<FeesBreakdown {...props} />);
|
||||
expect(screen.getByText('Maker fee').nextElementSibling).toHaveTextContent(
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { sumFeesFactors } from '@vegaprotocol/markets';
|
||||
import type { TradeFee, FeeFactors } from '@vegaprotocol/types';
|
||||
import type { FeeFactors } from '@vegaprotocol/types';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { getDiscountedFee } from '../discounts';
|
||||
import { getDiscountedFee, getTotalDiscountFactor } from '../discounts';
|
||||
import { useT } from '../../use-t';
|
||||
import { type useEstimateFees } from '../../hooks/use-estimate-fees';
|
||||
|
||||
const formatValue = (
|
||||
value: string | number | null | undefined,
|
||||
@@ -24,18 +25,16 @@ const FeesBreakdownItem = ({
|
||||
decimals,
|
||||
}: {
|
||||
label: string;
|
||||
factor?: string;
|
||||
factor?: string | number;
|
||||
value: string;
|
||||
symbol?: string;
|
||||
decimals: number;
|
||||
}) => (
|
||||
<>
|
||||
<dt className="col-span-2">{label}</dt>
|
||||
{factor && (
|
||||
<dd className="text-right col-span-1">
|
||||
{formatNumberPercentage(new BigNumber(factor).times(100))}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-1">
|
||||
{factor ? formatNumberPercentage(new BigNumber(factor).times(100)) : ''}
|
||||
</dd>
|
||||
<dd className="text-right col-span-3">
|
||||
{formatValue(value, decimals)} {symbol || ''}
|
||||
</dd>
|
||||
@@ -43,59 +42,35 @@ const FeesBreakdownItem = ({
|
||||
);
|
||||
|
||||
export const FeesBreakdown = ({
|
||||
totalFeeAmount,
|
||||
fees,
|
||||
feeEstimate,
|
||||
feeFactors,
|
||||
symbol,
|
||||
decimals,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor,
|
||||
}: {
|
||||
totalFeeAmount?: string;
|
||||
fees?: TradeFee;
|
||||
feeEstimate: ReturnType<typeof useEstimateFees>;
|
||||
feeFactors?: FeeFactors;
|
||||
symbol?: string;
|
||||
decimals: number;
|
||||
referralDiscountFactor?: string;
|
||||
volumeDiscountFactor?: string;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const { fees, totalFeeAmount, referralDiscountFactor, volumeDiscountFactor } =
|
||||
feeEstimate || {};
|
||||
if (!fees || !totalFeeAmount || totalFeeAmount === '0') return null;
|
||||
|
||||
const { discountedFee: discountedInfrastructureFee } = getDiscountedFee(
|
||||
fees.infrastructureFee,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
);
|
||||
|
||||
const { discountedFee: discountedLiquidityFee } = getDiscountedFee(
|
||||
fees.liquidityFee,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
);
|
||||
|
||||
const { discountedFee: discountedMakerFee } = getDiscountedFee(
|
||||
fees.makerFee,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
);
|
||||
|
||||
const {
|
||||
discountedFee: discountedTotalFeeAmount,
|
||||
volumeDiscount,
|
||||
referralDiscount,
|
||||
} = getDiscountedFee(
|
||||
totalFeeAmount,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
);
|
||||
const totalDiscountFactor = getTotalDiscountFactor(feeEstimate);
|
||||
const { discountedFee: discountedTotalFeeAmount, totalDiscount } =
|
||||
getDiscountedFee(
|
||||
totalFeeAmount,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
);
|
||||
|
||||
return (
|
||||
<dl className="grid grid-cols-6">
|
||||
<FeesBreakdownItem
|
||||
label={t('Infrastructure fee')}
|
||||
factor={feeFactors?.infrastructureFee}
|
||||
value={discountedInfrastructureFee}
|
||||
value={fees.infrastructureFee}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
@@ -103,7 +78,7 @@ export const FeesBreakdown = ({
|
||||
<FeesBreakdownItem
|
||||
label={t('Liquidity fee')}
|
||||
factor={feeFactors?.liquidityFee}
|
||||
value={discountedLiquidityFee}
|
||||
value={fees.liquidityFee}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
@@ -111,35 +86,50 @@ export const FeesBreakdown = ({
|
||||
<FeesBreakdownItem
|
||||
label={t('Maker fee')}
|
||||
factor={feeFactors?.makerFee}
|
||||
value={discountedMakerFee}
|
||||
value={fees.makerFee}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
{volumeDiscountFactor && volumeDiscount !== '0' && (
|
||||
<FeesBreakdownItem
|
||||
label={t('Volume discount')}
|
||||
factor={volumeDiscountFactor}
|
||||
value={volumeDiscount}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
{totalDiscount && totalDiscount !== '0' ? (
|
||||
<>
|
||||
<FeesBreakdownItem
|
||||
label={t('Subtotal')}
|
||||
value={totalFeeAmount}
|
||||
factor={
|
||||
feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined
|
||||
}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
<div className="col-span-6 mt-2"></div>
|
||||
<FeesBreakdownItem
|
||||
label={t('Discount')}
|
||||
factor={totalDiscountFactor}
|
||||
value={`-${totalDiscount}`}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
<FeesBreakdownItem
|
||||
label={t('Total')}
|
||||
value={discountedTotalFeeAmount}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="col-span-6 mt-2"></div>
|
||||
<FeesBreakdownItem
|
||||
label={t('Total')}
|
||||
factor={
|
||||
feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined
|
||||
}
|
||||
value={totalFeeAmount}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{referralDiscountFactor && referralDiscount !== '0' && (
|
||||
<FeesBreakdownItem
|
||||
label={t('Referral discount')}
|
||||
factor={referralDiscountFactor}
|
||||
value={referralDiscount}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
)}
|
||||
<FeesBreakdownItem
|
||||
label={t('Total fees')}
|
||||
factor={feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined}
|
||||
value={discountedTotalFeeAmount}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,15 +32,19 @@ export const mapFormValuesToOrderSubmission = (
|
||||
? toNanoSeconds(order.expiresAt)
|
||||
: undefined,
|
||||
postOnly:
|
||||
order.type === Schema.OrderType.TYPE_MARKET ? false : order.postOnly,
|
||||
reduceOnly:
|
||||
order.type === Schema.OrderType.TYPE_LIMIT &&
|
||||
![
|
||||
order.type === Schema.OrderType.TYPE_MARKET ||
|
||||
[
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
].includes(order.timeInForce)
|
||||
? false
|
||||
: order.reduceOnly,
|
||||
: order.postOnly,
|
||||
reduceOnly: ![
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
].includes(order.timeInForce)
|
||||
? false
|
||||
: order.reduceOnly,
|
||||
icebergOpts:
|
||||
order.type === Schema.OrderType.TYPE_LIMIT &&
|
||||
isPersistentOrder(order.timeInForce) &&
|
||||
|
||||
@@ -98,4 +98,91 @@ describe('mapFormValuesToOrderSubmission', () => {
|
||||
).size
|
||||
).toEqual('1000');
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK, postOnly: false },
|
||||
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GFA, postOnly: true },
|
||||
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GFN, postOnly: true },
|
||||
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC, postOnly: true },
|
||||
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT, postOnly: true },
|
||||
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_IOC, postOnly: false },
|
||||
])(
|
||||
'sets postOnly correctly when TIF is $timeInForce',
|
||||
({
|
||||
timeInForce,
|
||||
postOnly,
|
||||
}: {
|
||||
timeInForce: OrderTimeInForce;
|
||||
postOnly: boolean;
|
||||
}) => {
|
||||
expect(
|
||||
mapFormValuesToOrderSubmission(
|
||||
{
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
timeInForce,
|
||||
postOnly: true,
|
||||
} as OrderFormValues,
|
||||
'marketId',
|
||||
2,
|
||||
2
|
||||
).postOnly
|
||||
).toEqual(postOnly);
|
||||
// sets always false if type is market
|
||||
expect(
|
||||
mapFormValuesToOrderSubmission(
|
||||
{
|
||||
type: OrderType.TYPE_MARKET,
|
||||
timeInForce,
|
||||
postOnly: true,
|
||||
} as OrderFormValues,
|
||||
'marketId',
|
||||
2,
|
||||
2
|
||||
).postOnly
|
||||
).toEqual(false);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK, reduceOnly: true },
|
||||
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GFA, reduceOnly: false },
|
||||
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GFN, reduceOnly: false },
|
||||
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC, reduceOnly: false },
|
||||
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT, reduceOnly: false },
|
||||
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_IOC, reduceOnly: true },
|
||||
])(
|
||||
'sets reduceOnly correctly when TIF is $timeInForce',
|
||||
({
|
||||
timeInForce,
|
||||
reduceOnly,
|
||||
}: {
|
||||
timeInForce: OrderTimeInForce;
|
||||
reduceOnly: boolean;
|
||||
}) => {
|
||||
expect(
|
||||
mapFormValuesToOrderSubmission(
|
||||
{
|
||||
type: OrderType.TYPE_MARKET,
|
||||
timeInForce,
|
||||
reduceOnly: true,
|
||||
} as OrderFormValues,
|
||||
'marketId',
|
||||
2,
|
||||
2
|
||||
).reduceOnly
|
||||
).toEqual(reduceOnly);
|
||||
expect(
|
||||
mapFormValuesToOrderSubmission(
|
||||
{
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
timeInForce,
|
||||
reduceOnly: true,
|
||||
} as OrderFormValues,
|
||||
'marketId',
|
||||
2,
|
||||
2
|
||||
).reduceOnly
|
||||
).toEqual(reduceOnly);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -216,12 +216,10 @@ const ApprovalTxFeedback = ({
|
||||
<p>
|
||||
{t(
|
||||
'You approved deposits of up to {{assetSymbol}} {{approvedAllowanceValue}}.',
|
||||
[
|
||||
{
|
||||
assetSymbol: selectedAsset?.symbol,
|
||||
approvedAllowanceValue,
|
||||
},
|
||||
]
|
||||
{
|
||||
assetSymbol: selectedAsset?.symbol,
|
||||
approvedAllowanceValue,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
{txLink && <p>{txLink}</p>}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export const useTranslation = () => ({
|
||||
t: (label: string, replacements?: Record<string, string>) => {
|
||||
const replace =
|
||||
replacements?.replace && typeof replacements === 'object'
|
||||
? replacements?.replace
|
||||
: replacements;
|
||||
let translatedLabel = replacements?.defaultValue || label;
|
||||
if (typeof replace === 'object' && replace !== null) {
|
||||
Object.keys(replace).forEach((key) => {
|
||||
translatedLabel = translatedLabel.replace(`{{${key}}}`, replace[key]);
|
||||
});
|
||||
}
|
||||
return translatedLabel;
|
||||
},
|
||||
});
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
type NodeCheckTimeUpdateSubscription,
|
||||
} from '../../utils/__generated__/NodeCheck';
|
||||
import { Networks } from '../../types';
|
||||
import { createMockClient, RequestHandlerResponse } from 'mock-apollo-client';
|
||||
import {
|
||||
createMockClient,
|
||||
type RequestHandlerResponse,
|
||||
} from 'mock-apollo-client';
|
||||
|
||||
export type MockRequestConfig = {
|
||||
hasError?: boolean;
|
||||
|
||||
@@ -150,7 +150,7 @@ export const useEnvironment = create<EnvStore>()((set, get) => ({
|
||||
* Initialize Vega app to dynamically select a node from the
|
||||
* VEGA_CONFIG_URL
|
||||
*
|
||||
* This can be ommitted if you intend to only use a single node,
|
||||
* This can be omitted if you intend to only use a single node,
|
||||
* in those cases be sure to set NX_VEGA_URL
|
||||
*/
|
||||
export const useInitializeEnv = () => {
|
||||
@@ -415,6 +415,12 @@ function compileFeatureFlags(): FeatureFlags {
|
||||
REFERRALS: TRUTHY.includes(
|
||||
windowOrDefault('NX_REFERRALS', process.env['NX_REFERRALS']) as string
|
||||
),
|
||||
DISABLE_CLOSE_POSITION: TRUTHY.includes(
|
||||
windowOrDefault(
|
||||
'NX_DISABLE_CLOSE_POSITION',
|
||||
process.env['NX_DISABLE_CLOSE_POSITION']
|
||||
) as string
|
||||
),
|
||||
UPDATE_MARKET_STATE: TRUTHY.includes(
|
||||
windowOrDefault(
|
||||
'NX_UPDATE_MARKET_STATE',
|
||||
|
||||
@@ -70,10 +70,7 @@ export const useNodeHealth = () => {
|
||||
);
|
||||
intent = Intent.Danger;
|
||||
} else if (blockDiff >= BLOCK_THRESHOLD) {
|
||||
text = t('blocksBehind', {
|
||||
defaultValue: '{{count}} Blocks behind',
|
||||
replace: { count: blockDiff },
|
||||
});
|
||||
text = t('blocksBehind', '{{count}} Blocks behind', { count: blockDiff });
|
||||
intent = Intent.Warning;
|
||||
} else if (blockUpdateMsLatency > WARNING_LATENCY) {
|
||||
text = t(
|
||||
|
||||
@@ -5,6 +5,20 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
import { locales } from '@vegaprotocol/i18n';
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
// Set up i18n instance so that components have the correct default
|
||||
// en translations
|
||||
i18n.use(initReactI18next).init({
|
||||
// we init with resources
|
||||
resources: locales,
|
||||
fallbackLng: 'en',
|
||||
ns: ['environment'],
|
||||
defaultNS: 'environment',
|
||||
});
|
||||
|
||||
global.ResizeObserver = ResizeObserver;
|
||||
|
||||
// Required by radix-ui/react-dropdown-menu
|
||||
|
||||
@@ -27,6 +27,7 @@ export type CosmicElevatorFlags = Pick<
|
||||
| 'UPDATE_MARKET_STATE'
|
||||
| 'GOVERNANCE_TRANSFERS'
|
||||
| 'VOLUME_DISCOUNTS'
|
||||
| 'DISABLE_CLOSE_POSITION'
|
||||
>;
|
||||
export type Configuration = z.infer<typeof tomlConfigSchema>;
|
||||
export const CUSTOM_NODE_KEY = 'custom' as const;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user