Compare commits
46
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91806e51e8 | ||
|
|
80ab8821d0 | ||
|
|
7100b0e9fc | ||
|
|
614a83b7d6 | ||
|
|
3dc77b0eff | ||
|
|
127e784ceb | ||
|
|
e06f4818fc | ||
|
|
8182da3b31 | ||
|
|
c8c56307bb | ||
|
|
a8cd7f157f | ||
|
|
4f18caa486 | ||
|
|
5c7c626bbc | ||
|
|
e4c4c20631 | ||
|
|
0697302d07 | ||
|
|
2d926c0ce0 | ||
|
|
f57d6a7c7b | ||
|
|
15f905046f | ||
|
|
4f7918f64e | ||
|
|
52ab0562b0 | ||
|
|
4e2b0d1b1d | ||
|
|
0b0bcad9b3 | ||
|
|
bcf17bb34e | ||
|
|
a2b9b0da05 | ||
|
|
7588d0cd11 | ||
|
|
5ee1748495 | ||
|
|
73a118978f | ||
|
|
eac26c1966 | ||
|
|
d615587564 | ||
|
|
ba4ce1ce88 | ||
|
|
3bbacc1aa0 | ||
|
|
de5371435d | ||
|
|
12cb5e10b6 | ||
|
|
ee2909cc84 | ||
|
|
068d6abf1b | ||
|
|
e5d4d2b0b8 | ||
|
|
d4e801cfc6 | ||
|
|
964deb2f23 | ||
|
|
6669125dd3 | ||
|
|
129b6c4e89 | ||
|
|
ca418cabfe | ||
|
|
192af844c4 | ||
|
|
6a841f226a | ||
|
|
76426baa2a | ||
|
|
6fdac2419c | ||
|
|
d9dc43b359 | ||
|
|
06a6fe6d67 |
@@ -205,7 +205,7 @@ jobs:
|
||||
# run tests
|
||||
#----------------------------------------------
|
||||
- name: Run tests
|
||||
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v -s --numprocesses 6 --dist loadfile --durations=15
|
||||
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v -s --numprocesses 4 --dist loadfile --durations=15
|
||||
working-directory: apps/trading/e2e
|
||||
#----------------------------------------------
|
||||
# upload traces
|
||||
@@ -215,7 +215,7 @@ jobs:
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-trace
|
||||
path: ./traces/
|
||||
path: apps/trading/e2e/traces/
|
||||
retention-days: 15
|
||||
#----------------------------------------------
|
||||
# ----- upload logs -----
|
||||
|
||||
+1
-2
@@ -59,5 +59,4 @@ apps/trading/e2e/logs/
|
||||
apps/trading/e2e/.pytest_cache/
|
||||
apps/trading/e2e/traces/
|
||||
|
||||
.nx/cache
|
||||
|
||||
.nx/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,11 +1,21 @@
|
||||
import { FeesContainer } from '../../components/fees-container';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
import { titlefy } from '@vegaprotocol/utils';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const Fees = () => {
|
||||
const t = useT();
|
||||
const title = t('Fees');
|
||||
const { updateTitle } = usePageTitleStore((store) => ({
|
||||
updateTitle: store.updateTitle,
|
||||
}));
|
||||
useEffect(() => {
|
||||
updateTitle(titlefy([title]));
|
||||
}, [updateTitle, title]);
|
||||
return (
|
||||
<div className="container p-4 mx-auto">
|
||||
<h1 className="px-4 pb-4 text-2xl">{t('Fees')}</h1>
|
||||
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
|
||||
<FeesContainer />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -95,6 +98,11 @@ const CreateCodeDialog = ({
|
||||
const { stakeAvailable: currentStakeAvailable, requiredStake } =
|
||||
useStakeAvailable();
|
||||
|
||||
const { data: referralSets } = useReferral({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
});
|
||||
|
||||
const onSubmit = () => {
|
||||
if (isReadOnly || !pubKey) {
|
||||
setErr('Not connected');
|
||||
@@ -193,6 +201,68 @@ const CreateCodeDialog = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!referralSets) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{(status === 'idle' || status === 'loading' || status === 'error') && (
|
||||
<>
|
||||
{
|
||||
<p>
|
||||
{t(
|
||||
'There is currently no referral program active, are you sure you want to create a code?'
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
</>
|
||||
)}
|
||||
{status === 'success' && code && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700">
|
||||
<p className="overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{code}
|
||||
</p>
|
||||
</div>
|
||||
<CopyWithTooltip text={code}>
|
||||
<TradingButton
|
||||
className="text-sm no-underline"
|
||||
icon={<VegaIcon name={VegaIconNames.COPY} />}
|
||||
>
|
||||
<span>{t('Copy')}</span>
|
||||
</TradingButton>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
)}
|
||||
<TradingButton
|
||||
fill={true}
|
||||
intent={Intent.Primary}
|
||||
onClick={() => onSubmit()}
|
||||
{...getButtonProps()}
|
||||
></TradingButton>
|
||||
{status === 'idle' && (
|
||||
<TradingButton
|
||||
fill={true}
|
||||
intent={Intent.Primary}
|
||||
onClick={() => {
|
||||
refetch();
|
||||
setDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('No')}
|
||||
</TradingButton>
|
||||
)}
|
||||
{err && <InputError>{err}</InputError>}
|
||||
<div className="flex justify-center pt-5 mt-2 text-sm border-t gap-4 text-default border-default">
|
||||
<ExternalLink href={ABOUT_REFERRAL_DOCS_LINK}>
|
||||
{t('About the referral program')}
|
||||
</ExternalLink>
|
||||
<ExternalLink href={DISCLAIMER_REFERRAL_DOCS_LINK}>
|
||||
{t('Disclaimer')}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{(status === 'idle' || status === 'loading' || status === 'error') && (
|
||||
|
||||
@@ -11,6 +11,7 @@ query ReferralSetStats($code: ID!, $epoch: Int) {
|
||||
rewardsMultiplier
|
||||
rewardsFactorMultiplier
|
||||
referrerTakerVolume
|
||||
wasEligible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
query StakeAvailable($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
stakingSummary {
|
||||
currentStakeAvailable
|
||||
}
|
||||
}
|
||||
networkParameter(key: "referralProgram.minStakedVegaTokens") {
|
||||
value
|
||||
}
|
||||
}
|
||||
+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:
|
||||
|
||||
@@ -6,7 +6,12 @@ import { BORDER_COLOR, GRADIENT } from './constants';
|
||||
import { Tag } from './tag';
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { DApp, TOKEN_PROPOSALS, useLinks } from '@vegaprotocol/environment';
|
||||
import {
|
||||
DApp,
|
||||
DocsLinks,
|
||||
TOKEN_PROPOSALS,
|
||||
useLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { useT, ns } from '../../lib/use-t';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
@@ -88,10 +93,19 @@ export const TiersContainer = () => {
|
||||
return (
|
||||
<div className="text-base px-5 py-10 text-center">
|
||||
<Trans
|
||||
defaults="We're sorry but we don't have an active referral programme currently running. You can propose a new programme <0>here</0>."
|
||||
defaults="There are currently no active referral programs. Check the <0>Governance App</0> to see if there are any proposals in progress and vote."
|
||||
components={[
|
||||
<ExternalLink href={governanceLink(TOKEN_PROPOSALS)} key="link">
|
||||
{t('here')}
|
||||
{t('Governance App')}
|
||||
</ExternalLink>,
|
||||
]}
|
||||
ns={ns}
|
||||
/>
|
||||
<Trans
|
||||
defaults="You can propose a new program via the <0>Docs</0>."
|
||||
components={[
|
||||
<ExternalLink href={DocsLinks?.REFERRALS} key="link">
|
||||
{t('Docs')}
|
||||
</ExternalLink>,
|
||||
]}
|
||||
ns={ns}
|
||||
@@ -194,9 +208,13 @@ const TiersTable = ({
|
||||
{ name: 'discount', displayName: t('Referrer trading discount') },
|
||||
{
|
||||
name: 'volume',
|
||||
displayName: t('Min. trading volume (last {{count}} epochs)', {
|
||||
count: windowLength,
|
||||
}),
|
||||
displayName: t(
|
||||
'minTradingVolume',
|
||||
'Min. trading volume (last {{count}} epochs)',
|
||||
{
|
||||
count: windowLength,
|
||||
}
|
||||
),
|
||||
},
|
||||
{ name: 'epochs', displayName: t('Min. epochs') },
|
||||
]}
|
||||
@@ -204,13 +222,13 @@ const TiersTable = ({
|
||||
...d,
|
||||
className: classNames({
|
||||
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
|
||||
d.tier === 1,
|
||||
d.tier >= 3,
|
||||
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
|
||||
d.tier === 2,
|
||||
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
|
||||
d.tier === 3,
|
||||
d.tier === 1,
|
||||
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
|
||||
d.tier > 3,
|
||||
d.tier == 0,
|
||||
}),
|
||||
}))}
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,9 @@ import classNames from 'classnames';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { Button } from './buttons';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { Routes } from '../../lib/links';
|
||||
import { DApp, useLinks } from '@vegaprotocol/environment';
|
||||
import truncate from 'lodash/truncate';
|
||||
|
||||
export const Tile = ({
|
||||
className,
|
||||
@@ -63,6 +66,10 @@ export const CodeTile = ({
|
||||
className?: string;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const consoleLink = useLinks(DApp.Console);
|
||||
const applyCodeLink = consoleLink(
|
||||
`#${Routes.REFERRALS_APPLY_CODE}?code=${code}`
|
||||
);
|
||||
return (
|
||||
<StatTile
|
||||
title={t('Your referral code')}
|
||||
@@ -89,10 +96,27 @@ export const CodeTile = ({
|
||||
{code}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<CopyWithTooltip text={code}>
|
||||
<CopyWithTooltip text={code} description={t('Copy referral code')}>
|
||||
<Button className="text-sm no-underline !py-0 !px-0 h-fit !bg-transparent">
|
||||
<span className="sr-only">{t('Copy')}</span>
|
||||
<VegaIcon size={24} name={VegaIconNames.COPY} />
|
||||
<VegaIcon size={20} name={VegaIconNames.COPY} />
|
||||
</Button>
|
||||
</CopyWithTooltip>
|
||||
<CopyWithTooltip
|
||||
text={applyCodeLink}
|
||||
description={
|
||||
<>
|
||||
{t('Copy shareable apply code link')}
|
||||
{': '}
|
||||
<a className="text-vega-blue-500 underline" href={applyCodeLink}>
|
||||
{truncate(applyCodeLink, { length: 32 })}
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button className="text-sm no-underline !py-0 !px-0 h-fit !bg-transparent">
|
||||
<span className="sr-only">{t('Copy')}</span>
|
||||
<VegaIcon size={20} name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
</Button>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { RewardsContainer } from '../../components/rewards-container';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
import { titlefy } from '@vegaprotocol/utils';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const Rewards = () => {
|
||||
const t = useT();
|
||||
const title = t('Rewards');
|
||||
const { updateTitle } = usePageTitleStore((store) => ({
|
||||
updateTitle: store.updateTitle,
|
||||
}));
|
||||
useEffect(() => {
|
||||
updateTitle(titlefy([title]));
|
||||
}, [updateTitle, title]);
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="px-4 pb-4 text-2xl">{t('Rewards')}</h1>
|
||||
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
|
||||
<RewardsContainer />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,18 +16,11 @@ query DiscountPrograms {
|
||||
}
|
||||
}
|
||||
|
||||
query Fees(
|
||||
$partyId: ID!
|
||||
$volumeDiscountEpochs: Int!
|
||||
$referralDiscountEpochs: Int!
|
||||
) {
|
||||
query Fees($partyId: ID!) {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(
|
||||
partyId: $partyId
|
||||
pagination: { last: $volumeDiscountEpochs }
|
||||
) {
|
||||
volumeDiscountStats(partyId: $partyId, pagination: { last: 1 }) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
@@ -59,10 +52,7 @@ query Fees(
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(
|
||||
partyId: $partyId
|
||||
pagination: { last: $referralDiscountEpochs }
|
||||
) {
|
||||
referralSetStats(partyId: $partyId, pagination: { last: 1 }) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
|
||||
+3
-10
@@ -10,8 +10,6 @@ export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgr
|
||||
|
||||
export type FeesQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
volumeDiscountEpochs: Types.Scalars['Int'];
|
||||
referralDiscountEpochs: Types.Scalars['Int'];
|
||||
}>;
|
||||
|
||||
|
||||
@@ -65,14 +63,11 @@ export type DiscountProgramsQueryHookResult = ReturnType<typeof useDiscountProgr
|
||||
export type DiscountProgramsLazyQueryHookResult = ReturnType<typeof useDiscountProgramsLazyQuery>;
|
||||
export type DiscountProgramsQueryResult = Apollo.QueryResult<DiscountProgramsQuery, DiscountProgramsQueryVariables>;
|
||||
export const FeesDocument = gql`
|
||||
query Fees($partyId: ID!, $volumeDiscountEpochs: Int!, $referralDiscountEpochs: Int!) {
|
||||
query Fees($partyId: ID!) {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(
|
||||
partyId: $partyId
|
||||
pagination: {last: $volumeDiscountEpochs}
|
||||
) {
|
||||
volumeDiscountStats(partyId: $partyId, pagination: {last: 1}) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
@@ -104,7 +99,7 @@ export const FeesDocument = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) {
|
||||
referralSetStats(partyId: $partyId, pagination: {last: 1}) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
@@ -129,8 +124,6 @@ export const FeesDocument = gql`
|
||||
* const { data, loading, error } = useFeesQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* volumeDiscountEpochs: // value for 'volumeDiscountEpochs'
|
||||
* referralDiscountEpochs: // value for 'referralDiscountEpochs'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -36,25 +36,25 @@ export const FeesContainer = () => {
|
||||
const { data: markets, loading: marketsLoading } = useMarketList();
|
||||
|
||||
const { data: programData, loading: programLoading } =
|
||||
useDiscountProgramsQuery();
|
||||
useDiscountProgramsQuery({ errorPolicy: 'ignore' });
|
||||
|
||||
const volumeDiscountWindowLength =
|
||||
programData?.currentVolumeDiscountProgram?.windowLength || 1;
|
||||
const referralDiscountWindowLength =
|
||||
programData?.currentReferralProgram?.windowLength || 1;
|
||||
|
||||
const { data: feesData, loading: feesLoading } = useFeesQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
volumeDiscountEpochs: volumeDiscountWindowLength,
|
||||
referralDiscountEpochs: referralDiscountWindowLength,
|
||||
},
|
||||
skip: !pubKey || !programData,
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
|
||||
|
||||
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
|
||||
useVolumeStats(
|
||||
feesData?.volumeDiscountStats,
|
||||
previousEpoch,
|
||||
feesData?.volumeDiscountStats.edges?.[0]?.node,
|
||||
programData?.currentVolumeDiscountProgram
|
||||
);
|
||||
|
||||
@@ -67,12 +67,12 @@ export const FeesContainer = () => {
|
||||
code,
|
||||
isReferrer,
|
||||
} = useReferralStats(
|
||||
feesData?.referralSetStats,
|
||||
feesData?.referralSetReferees,
|
||||
previousEpoch,
|
||||
feesData?.referralSetStats.edges?.[0]?.node,
|
||||
feesData?.referralSetReferees.edges?.[0]?.node,
|
||||
programData?.currentReferralProgram,
|
||||
feesData?.epoch,
|
||||
feesData?.referrer,
|
||||
feesData?.referee
|
||||
feesData?.referrer.edges?.[0]?.node,
|
||||
feesData?.referee.edges?.[0]?.node
|
||||
);
|
||||
|
||||
const loading = paramsLoading || feesLoading || programLoading;
|
||||
@@ -317,7 +317,7 @@ export const CurrentVolume = ({
|
||||
<div className="flex flex-col gap-3 pt-4">
|
||||
<CardStat
|
||||
value={formatNumberRounded(new BigNumber(windowLengthVolume))}
|
||||
text={t('Past {{count}} epochs', { count: windowLength })}
|
||||
text={t('pastEpochs', 'Past {{count}} epochs', { count: windowLength })}
|
||||
/>
|
||||
{requiredForNextTier > 0 && (
|
||||
<CardStat
|
||||
@@ -344,9 +344,13 @@ const ReferralBenefits = ({
|
||||
<CardStat
|
||||
// all sets volume (not just current party)
|
||||
value={formatNumber(setRunningNotionalTakerVolume)}
|
||||
text={t('Combined running notional over the {{count}} epochs', {
|
||||
count: epochs,
|
||||
})}
|
||||
text={t(
|
||||
'runningNotionalOverEpochs',
|
||||
'Combined running notional over the {{count}} epochs',
|
||||
{
|
||||
count: epochs,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<CardStat value={epochsInSet} text={t('epochs in referral set')} />
|
||||
</div>
|
||||
@@ -453,31 +457,27 @@ const VolumeTiers = ({
|
||||
<Th>{t('Discount')}</Th>
|
||||
<Th>{t('Min. trading volume')}</Th>
|
||||
<Th>
|
||||
{t('My volume (last {{count}} epochs)', { count: windowLength })}
|
||||
{t('myVolume', 'My volume (last {{count}} epochs)', {
|
||||
count: windowLength,
|
||||
})}
|
||||
</Th>
|
||||
<Th />
|
||||
</tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers)
|
||||
.reverse()
|
||||
.map((tier, i) => {
|
||||
const isUserTier = tiers.length - 1 - tierIndex === i;
|
||||
{Array.from(tiers).map((tier, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>
|
||||
{formatPercentage(Number(tier.volumeDiscountFactor))}%
|
||||
</Td>
|
||||
<Td>
|
||||
{formatNumber(tier.minimumRunningNotionalTakerVolume)}
|
||||
</Td>
|
||||
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : null}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>{formatPercentage(Number(tier.volumeDiscountFactor))}%</Td>
|
||||
<Td>{formatNumber(tier.minimumRunningNotionalTakerVolume)}</Td>
|
||||
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : null}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
@@ -520,37 +520,33 @@ const ReferralTiers = ({
|
||||
</tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers)
|
||||
.reverse()
|
||||
.map((t, i) => {
|
||||
const isUserTier = tiers.length - 1 - tierIndex === i;
|
||||
{Array.from(tiers).map((t, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
|
||||
const requiredVolume = Number(
|
||||
t.minimumRunningNotionalTakerVolume
|
||||
const requiredVolume = Number(t.minimumRunningNotionalTakerVolume);
|
||||
let unlocksIn = null;
|
||||
|
||||
if (
|
||||
referralVolumeInWindow >= requiredVolume &&
|
||||
epochsInSet < t.minimumEpochs
|
||||
) {
|
||||
unlocksIn = (
|
||||
<span className="text-muted">
|
||||
Unlocks in {t.minimumEpochs - epochsInSet} epochs
|
||||
</span>
|
||||
);
|
||||
let unlocksIn = null;
|
||||
}
|
||||
|
||||
if (
|
||||
referralVolumeInWindow >= requiredVolume &&
|
||||
epochsInSet < t.minimumEpochs
|
||||
) {
|
||||
unlocksIn = (
|
||||
<span className="text-muted">
|
||||
Unlocks in {t.minimumEpochs - epochsInSet} epochs
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
|
||||
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
|
||||
<Td>{t.minimumEpochs}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
|
||||
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
|
||||
<Td>{t.minimumEpochs}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@@ -2,46 +2,15 @@ import { renderHook } from '@testing-library/react';
|
||||
import { useReferralStats } from './use-referral-stats';
|
||||
|
||||
describe('useReferralStats', () => {
|
||||
const setStats = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 9,
|
||||
discountFactor: '0.2',
|
||||
referralSetRunningNotionalTakerVolume: '100',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: '200',
|
||||
},
|
||||
},
|
||||
],
|
||||
const stat = {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 9,
|
||||
discountFactor: '0.01',
|
||||
referralSetRunningNotionalTakerVolume: '100',
|
||||
};
|
||||
|
||||
const sets = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
atEpoch: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const epoch = {
|
||||
id: '10',
|
||||
const set = {
|
||||
atEpoch: 4,
|
||||
};
|
||||
|
||||
const program = {
|
||||
@@ -78,102 +47,36 @@ describe('useReferralStats', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns formatted data and tiers', () => {
|
||||
it('returns default values if set is not from previous epoch', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(setStats, sets, program, epoch)
|
||||
useReferralStats(10, stat, set, program)
|
||||
);
|
||||
|
||||
// should use stats from latest epoch
|
||||
const stats = setStats.edges[1].node;
|
||||
const set = sets.edges[1].node;
|
||||
|
||||
expect(result.current).toEqual({
|
||||
referralDiscount: Number(stats.discountFactor),
|
||||
referralVolumeInWindow: Number(
|
||||
stats.referralSetRunningNotionalTakerVolume
|
||||
),
|
||||
referralTierIndex: 1,
|
||||
referralDiscount: 0,
|
||||
referralVolumeInWindow: 0,
|
||||
referralTierIndex: -1,
|
||||
referralTiers: program.benefitTiers,
|
||||
epochsInSet: Number(epoch.id) - set.atEpoch,
|
||||
epochsInSet: 0,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ joinedAt: 2, index: -1 },
|
||||
{ joinedAt: 3, index: -1 },
|
||||
{ joinedAt: 4, index: 0 },
|
||||
{ joinedAt: 5, index: 0 },
|
||||
{ joinedAt: 6, index: 1 },
|
||||
{ joinedAt: 7, index: 1 },
|
||||
{ joinedAt: 8, index: 2 },
|
||||
{ joinedAt: 9, index: 2 },
|
||||
])('joined at epoch: $joinedAt should be index: $index', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: '100000',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const setsA = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: Number(epoch.id) - obj.joinedAt,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(statsA, setsA, program, epoch)
|
||||
useReferralStats(9, stat, set, program)
|
||||
);
|
||||
|
||||
expect(result.current.referralTierIndex).toEqual(obj.index);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ volume: '50', index: -1 },
|
||||
{ volume: '100', index: 0 },
|
||||
{ volume: '150', index: 0 },
|
||||
{ volume: '200', index: 1 },
|
||||
{ volume: '250', index: 1 },
|
||||
{ volume: '300', index: 2 },
|
||||
{ volume: '999', index: 2 },
|
||||
])('volume: $volume should be index: $index', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: obj.volume,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const setsA = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(statsA, setsA, program, epoch)
|
||||
);
|
||||
|
||||
expect(result.current.referralTierIndex).toEqual(obj.index);
|
||||
expect(result.current).toEqual({
|
||||
referralDiscount: Number(stat.discountFactor),
|
||||
referralVolumeInWindow: Number(
|
||||
stat.referralSetRunningNotionalTakerVolume
|
||||
),
|
||||
referralTierIndex: 0,
|
||||
referralTiers: program.benefitTiers,
|
||||
epochsInSet: stat.atEpoch - set.atEpoch,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import compact from 'lodash/compact';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { getReferralBenefitTier } from './utils';
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
import { first } from 'lodash';
|
||||
|
||||
export const useReferralStats = (
|
||||
setStats?: FeesQuery['referralSetStats'],
|
||||
setReferees?: FeesQuery['referralSetReferees'],
|
||||
previousEpoch?: number,
|
||||
referralStats?: NonNullable<
|
||||
FeesQuery['referralSetStats']['edges']['0']
|
||||
>['node'],
|
||||
setReferees?: NonNullable<
|
||||
FeesQuery['referralSetReferees']['edges']['0']
|
||||
>['node'],
|
||||
program?: DiscountProgramsQuery['currentReferralProgram'],
|
||||
epoch?: FeesQuery['epoch'],
|
||||
setIfReferrer?: FeesQuery['referrer'],
|
||||
setIfReferee?: FeesQuery['referee']
|
||||
setIfReferrer?: NonNullable<FeesQuery['referrer']['edges']['0']>['node'],
|
||||
setIfReferee?: NonNullable<FeesQuery['referee']['edges']['0']>['node']
|
||||
) => {
|
||||
const referralTiers = program?.benefitTiers || [];
|
||||
|
||||
if (!setStats || !setReferees || !program || !epoch) {
|
||||
if (
|
||||
!previousEpoch ||
|
||||
referralStats?.atEpoch !== previousEpoch ||
|
||||
!program ||
|
||||
!setReferees
|
||||
) {
|
||||
return {
|
||||
referralDiscount: 0,
|
||||
referralVolumeInWindow: 0,
|
||||
@@ -26,41 +30,22 @@ export const useReferralStats = (
|
||||
};
|
||||
}
|
||||
|
||||
const setIfReferrerData = first(
|
||||
compact(setIfReferrer?.edges).map((e) => e.node)
|
||||
);
|
||||
const setIfRefereeData = first(
|
||||
compact(setIfReferee?.edges).map((e) => e.node)
|
||||
);
|
||||
|
||||
const referralSetsStats = compact(setStats.edges).map((e) => e.node);
|
||||
const referralSets = compact(setReferees.edges).map((e) => e.node);
|
||||
|
||||
const referralSet = maxBy(referralSets, (s) => s.atEpoch);
|
||||
const referralStats = maxBy(referralSetsStats, (s) => s.atEpoch);
|
||||
|
||||
const epochsInSet = referralSet ? Number(epoch.id) - referralSet.atEpoch : 0;
|
||||
|
||||
const referralDiscount = Number(referralStats?.discountFactor || 0);
|
||||
const referralVolumeInWindow = Number(
|
||||
referralStats?.referralSetRunningNotionalTakerVolume || 0
|
||||
);
|
||||
|
||||
const referralTierIndex = referralStats
|
||||
? getReferralBenefitTier(
|
||||
epochsInSet,
|
||||
Number(referralStats.referralSetRunningNotionalTakerVolume),
|
||||
referralTiers
|
||||
)
|
||||
: -1;
|
||||
const referralTierIndex = referralTiers.findIndex(
|
||||
(tier) => tier.referralDiscountFactor === referralStats?.discountFactor
|
||||
);
|
||||
|
||||
return {
|
||||
referralDiscount,
|
||||
referralVolumeInWindow,
|
||||
referralTierIndex,
|
||||
referralTiers,
|
||||
epochsInSet,
|
||||
code: (setIfReferrerData || setIfRefereeData)?.id,
|
||||
isReferrer: Boolean(setIfReferrerData),
|
||||
epochsInSet: referralStats.atEpoch - setReferees.atEpoch,
|
||||
code: (setIfReferrer || setIfReferee)?.id,
|
||||
isReferrer: Boolean(setIfReferrer),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,27 +2,11 @@ import { renderHook } from '@testing-library/react';
|
||||
import { useVolumeStats } from './use-volume-stats';
|
||||
|
||||
describe('useReferralStats', () => {
|
||||
const statsList = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 9,
|
||||
discountFactor: '0.1',
|
||||
runningVolume: '100',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
runningVolume: '200',
|
||||
},
|
||||
},
|
||||
],
|
||||
const stats = {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.05',
|
||||
runningVolume: '200',
|
||||
};
|
||||
|
||||
const program = {
|
||||
@@ -44,7 +28,7 @@ describe('useReferralStats', () => {
|
||||
};
|
||||
|
||||
it('returns correct default values', () => {
|
||||
const { result } = renderHook(() => useVolumeStats());
|
||||
const { result } = renderHook(() => useVolumeStats(10));
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: 0,
|
||||
volumeInWindow: 0,
|
||||
@@ -53,11 +37,18 @@ describe('useReferralStats', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(statsList, program));
|
||||
it('returns default values if no stat is not from previous epoch', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(11, stats, program));
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: 0,
|
||||
volumeInWindow: 0,
|
||||
volumeTierIndex: -1,
|
||||
volumeTiers: program.benefitTiers,
|
||||
});
|
||||
});
|
||||
|
||||
// should use stats from latest epoch
|
||||
const stats = statsList.edges[1].node;
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(10, stats, program));
|
||||
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: Number(stats.discountFactor),
|
||||
@@ -66,30 +57,4 @@ describe('useReferralStats', () => {
|
||||
volumeTiers: program.benefitTiers,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ volume: '100', index: 0 },
|
||||
{ volume: '150', index: 0 },
|
||||
{ volume: '200', index: 1 },
|
||||
{ volume: '250', index: 1 },
|
||||
{ volume: '300', index: 2 },
|
||||
{ volume: '350', index: 2 },
|
||||
])('returns index: $index for the running volume: $volume', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
runningVolume: obj.volume,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useVolumeStats(statsA, program));
|
||||
expect(result.current.volumeTierIndex).toBe(obj.index);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import compact from 'lodash/compact';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { getVolumeTier } from './utils';
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
|
||||
export const useVolumeStats = (
|
||||
stats?: FeesQuery['volumeDiscountStats'],
|
||||
previousEpoch: number,
|
||||
lastEpochStats?: NonNullable<
|
||||
FeesQuery['volumeDiscountStats']['edges']['0']
|
||||
>['node'],
|
||||
program?: DiscountProgramsQuery['currentVolumeDiscountProgram']
|
||||
) => {
|
||||
const volumeTiers = program?.benefitTiers || [];
|
||||
|
||||
if (!stats || !program) {
|
||||
if (!lastEpochStats || lastEpochStats.atEpoch !== previousEpoch || !program) {
|
||||
return {
|
||||
volumeDiscount: 0,
|
||||
volumeTierIndex: -1,
|
||||
@@ -18,11 +18,11 @@ export const useVolumeStats = (
|
||||
};
|
||||
}
|
||||
|
||||
const volumeStats = compact(stats.edges).map((e) => e.node);
|
||||
const lastEpochStats = maxBy(volumeStats, (s) => s.atEpoch);
|
||||
const volumeDiscount = Number(lastEpochStats?.discountFactor || 0);
|
||||
const volumeInWindow = Number(lastEpochStats?.runningVolume || 0);
|
||||
const volumeTierIndex = getVolumeTier(volumeInWindow, volumeTiers);
|
||||
const volumeTierIndex = volumeTiers.findIndex(
|
||||
(tier) => tier.volumeDiscountFactor === lastEpochStats?.discountFactor
|
||||
);
|
||||
|
||||
return {
|
||||
volumeDiscount,
|
||||
|
||||
@@ -20,73 +20,6 @@ export const formatPercentage = (num: number) => {
|
||||
return formatter.format(parseFloat(pct.toFixed(5)));
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the index of the benefit tier for volume discounts. A user
|
||||
* only needs to fulfill a minimum volume requirement for the tier
|
||||
*/
|
||||
export const getVolumeTier = (
|
||||
volume: number,
|
||||
tiers: Array<{
|
||||
minimumRunningNotionalTakerVolume: string;
|
||||
}>
|
||||
) => {
|
||||
return tiers.findIndex((tier, i) => {
|
||||
const nextTier = tiers[i + 1];
|
||||
const validVolume =
|
||||
volume >= Number(tier.minimumRunningNotionalTakerVolume);
|
||||
|
||||
if (nextTier) {
|
||||
return (
|
||||
validVolume &&
|
||||
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
|
||||
);
|
||||
}
|
||||
|
||||
return validVolume;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the index of the benefit tiers for referrals. A user must
|
||||
* fulfill both the minimum epochs in the referral set, and the set
|
||||
* must reach the combined total volume
|
||||
*/
|
||||
export const getReferralBenefitTier = (
|
||||
epochsInSet: number,
|
||||
volume: number,
|
||||
tiers: Array<{
|
||||
minimumRunningNotionalTakerVolume: string;
|
||||
minimumEpochs: number;
|
||||
}>
|
||||
) => {
|
||||
const indexByEpoch = tiers.findIndex((tier, i) => {
|
||||
const nextTier = tiers[i + 1];
|
||||
const validEpochs = epochsInSet >= tier.minimumEpochs;
|
||||
|
||||
if (nextTier) {
|
||||
return validEpochs && epochsInSet < nextTier.minimumEpochs;
|
||||
}
|
||||
|
||||
return validEpochs;
|
||||
});
|
||||
const indexByVolume = tiers.findIndex((tier, i) => {
|
||||
const nextTier = tiers[i + 1];
|
||||
const validVolume =
|
||||
volume >= Number(tier.minimumRunningNotionalTakerVolume);
|
||||
|
||||
if (nextTier) {
|
||||
return (
|
||||
validVolume &&
|
||||
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
|
||||
);
|
||||
}
|
||||
|
||||
return validVolume;
|
||||
});
|
||||
|
||||
return Math.min(indexByEpoch, indexByVolume);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a set of fees and a set of discounts return
|
||||
* the adjusted fee factor
|
||||
|
||||
@@ -65,7 +65,7 @@ export const FundingContainer = ({ marketId }: { marketId: string }) => {
|
||||
if (edge.node.endTime) {
|
||||
acc?.push({
|
||||
endTime: fromNanoSeconds(edge.node.endTime),
|
||||
fundingRate: Number(edge.node.fundingRate) * 100,
|
||||
fundingRate: Number(edge.node.fundingRate),
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
@@ -82,7 +82,8 @@ export const FundingContainer = ({ marketId }: { marketId: string }) => {
|
||||
<LineChart
|
||||
data={values}
|
||||
theme={theme}
|
||||
priceFormat={(fundingRate) => `${fundingRate.toFixed(4)}%`}
|
||||
priceFormat={(fundingRate) => `${(fundingRate * 100).toFixed(4)}%`}
|
||||
yAxisTickFormat="%"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,15 +16,12 @@ export const LedgerContainer = () => {
|
||||
});
|
||||
|
||||
const assets = (data?.party?.accountsConnection?.edges ?? [])
|
||||
.map<PartyAssetFieldsFragment>(
|
||||
(item) => item?.node?.asset ?? ({} as PartyAssetFieldsFragment)
|
||||
)
|
||||
.reduce((aggr, item) => {
|
||||
if ('id' in item && 'symbol' in item) {
|
||||
aggr[item.id as string] = item.symbol as string;
|
||||
}
|
||||
return aggr;
|
||||
}, {} as Record<string, string>);
|
||||
.map((item) => item?.node?.asset)
|
||||
.filter((asset): asset is PartyAssetFieldsFragment => !!asset?.id)
|
||||
.reduce(
|
||||
(aggr, item) => Object.assign(aggr, { [item.id]: item.symbol }),
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
if (!pubKey) {
|
||||
return (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Settings } from './settings';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
describe('Settings', () => {
|
||||
it('should the settings component with all the options', () => {
|
||||
render(<Settings />);
|
||||
expect(screen.getByText('Dark mode')).toBeInTheDocument();
|
||||
expect(screen.getByText('Share usage data')).toBeInTheDocument();
|
||||
expect(screen.getByText('Toast location')).toBeInTheDocument();
|
||||
expect(screen.getByText('Reset to default')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Switch, ToastPositionSetter } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
Dialog,
|
||||
Intent,
|
||||
Switch,
|
||||
ToastPositionSetter,
|
||||
TradingButton,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useT } from '../../lib/use-t';
|
||||
|
||||
@@ -9,6 +15,7 @@ export const Settings = () => {
|
||||
const t = useT();
|
||||
const { theme, setTheme } = useThemeSwitcher();
|
||||
const [isApproved, setIsApproved] = useTelemetryApproval();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<div>
|
||||
<SettingsGroup label={t('Dark mode')}>
|
||||
@@ -33,6 +40,52 @@ export const Settings = () => {
|
||||
<SettingsGroup label={t('Toast location')}>
|
||||
<ToastPositionSetter />
|
||||
</SettingsGroup>
|
||||
<SettingsGroup label={t('Reset to default')}>
|
||||
<TradingButton
|
||||
name="reset-to-defaults"
|
||||
size="small"
|
||||
intent={Intent.None}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{t('Reset')}
|
||||
</TradingButton>
|
||||
<Dialog open={open} title={t('Reset')}>
|
||||
<div className="mb-4">
|
||||
<p>
|
||||
{t(
|
||||
'You will lose all persisted settings and you will be logged out.'
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t('Are you sure you want to reset all settings to default?')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<TradingButton
|
||||
name="reset-to-defaults-cancel"
|
||||
intent={Intent.Primary}
|
||||
onClick={() => {
|
||||
localStorage.clear();
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
{t('Yes, clear cache and refresh')}
|
||||
</TradingButton>
|
||||
<TradingButton
|
||||
name="reset-to-defaults-cancel"
|
||||
intent={Intent.None}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('No, keep settings')}
|
||||
</TradingButton>
|
||||
</div>
|
||||
</Dialog>
|
||||
</SettingsGroup>
|
||||
<SettingsGroup inline={false} label={t('App information')}>
|
||||
<dl className="text-sm grid grid-cols-2 gap-1">
|
||||
{process.env.GIT_TAG && (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -44,4 +44,5 @@ def truncate_middle(market_id, start=6, end=4):
|
||||
def change_keys(page: Page, vega:VegaServiceNull, key_name):
|
||||
page.get_by_test_id("manage-vega-wallet").click()
|
||||
page.get_by_test_id("key-" + vega.wallet.public_key(key_name)).click()
|
||||
page.click(f'data-testid=key-{vega.wallet.public_key(key_name)} >> .inline-flex')
|
||||
page.reload()
|
||||
|
||||
@@ -136,8 +136,11 @@ def init_page(vega: VegaServiceNull, browser: Browser, request: pytest.FixtureRe
|
||||
page.add_init_script(script=window_env)
|
||||
yield page
|
||||
finally:
|
||||
if not os.path.exists("traces"):
|
||||
os.makedirs("traces")
|
||||
try:
|
||||
if not os.path.exists("apps/trading/e2e/traces"):
|
||||
os.makedirs("apps/trading/e2e/traces")
|
||||
except OSError as e:
|
||||
print(f"Failed to create directory '{'apps/trading/e2e/traces'}': {e}")
|
||||
|
||||
# Check whether this test failed or passed
|
||||
outcome = request.config.cache.get(request.node.nodeid, None)
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import pytest
|
||||
from playwright.sync_api import expect, Page
|
||||
|
||||
@pytest.mark.usefixtures("page", "continuous_market", "risk_accepted")
|
||||
def test_see_market_depth_chart(continuous_market, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
# Click on the 'Depth' tab
|
||||
page.get_by_test_id("Depth").click()
|
||||
# Check if the 'Depth' tab and the depth chart are visible
|
||||
# 6006-DEPC-001
|
||||
expect(page.get_by_test_id("tab-depth")).to_be_visible()
|
||||
expect(page.locator('[class^="depth-chart-module_canvas__"]').first).to_be_visible()
|
||||
@@ -29,11 +29,12 @@ def continuous_market(vega):
|
||||
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
|
||||
def test_limit_buy_order_GTT(continuous_market, vega: VegaService, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(tif).select_option("Good 'til Time (GTT)")
|
||||
page.get_by_test_id(order_size).fill("10")
|
||||
page.get_by_test_id(order_price).fill("120")
|
||||
page.get_by_test_id(tif).select_option("Good 'til Time (GTT)")
|
||||
expires_at = datetime.now() + timedelta(days=1)
|
||||
expires_at_input_value = expires_at.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
page.get_by_test_id("date-picker-field").clear()
|
||||
page.get_by_test_id("date-picker-field").fill(expires_at_input_value)
|
||||
# 7002-SORD-011
|
||||
expect(page.get_by_test_id("place-order").locator("span").first).to_have_text(
|
||||
|
||||
@@ -2,10 +2,8 @@ import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.service import VegaService
|
||||
from actions.vega import submit_order
|
||||
from conftest import init_vega
|
||||
from fixtures.market import setup_continuous_market
|
||||
from actions.utils import wait_for_toast_confirmation
|
||||
from wallet_config import MM_WALLET, MM_WALLET2, TERMINATE_WALLET, wallets
|
||||
|
||||
|
||||
stop_order_btn = "order-type-Stop"
|
||||
stop_limit_order_btn = "order-type-StopLimit"
|
||||
@@ -328,98 +326,4 @@ def test_submit_stop_oco_limit_order_cancel(
|
||||
page.locator(".ag-center-cols-container").locator('[col-id="status"]').last
|
||||
).to_have_text("CancelledOCO")
|
||||
|
||||
class TestStopOcoValidation:
|
||||
@pytest.fixture(scope="class")
|
||||
def vega(self, request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def continuous_market(self, vega):
|
||||
return setup_continuous_market(vega)
|
||||
|
||||
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
|
||||
def test_stop_market_order_oco_form_validation(self, continuous_market, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(stop_order_btn).click()
|
||||
page.get_by_test_id(stop_market_order_btn).is_visible()
|
||||
page.get_by_test_id(stop_market_order_btn).click()
|
||||
page.get_by_test_id(oco).click()
|
||||
expect(
|
||||
page.get_by_test_id("sidebar-content").get_by_text("Trigger").last
|
||||
).to_be_visible()
|
||||
# 7002-SORD-084
|
||||
expect(page.locator('[for="triggerDirection-risesAbove-oco"]')).to_have_text(
|
||||
"Rises above"
|
||||
)
|
||||
# 7002-SORD-085
|
||||
expect(page.locator('[for="triggerDirection-fallsBelow-oco"]')).to_have_text(
|
||||
"Falls below"
|
||||
)
|
||||
# 7002-SORD-087
|
||||
expect(page.locator('[for="triggerType-price-oco"]')).to_have_text("Price")
|
||||
expect(page.locator('[for="triggerType-price"]')).to_be_checked
|
||||
# 7002-SORD-088
|
||||
expect(
|
||||
page.locator('[for="triggerType-trailingPercentOffset-oco"]')
|
||||
).to_have_text("Trailing Percent Offset")
|
||||
expect(page.locator('[for="order-size-oco"]')).to_have_text("Size")
|
||||
|
||||
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
|
||||
def test_stop_limit_order_oco_form_validation(self, continuous_market, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(stop_order_btn).click()
|
||||
page.get_by_test_id(stop_market_order_btn).is_visible()
|
||||
page.get_by_test_id(stop_limit_order_btn).click()
|
||||
page.get_by_test_id(oco).click()
|
||||
expect(
|
||||
page.get_by_test_id("sidebar-content").get_by_text("Trigger").last
|
||||
).to_be_visible()
|
||||
# 7002-SORD-099
|
||||
expect(page.locator('[for="triggerDirection-risesAbove-oco"]')).to_have_text(
|
||||
"Rises above"
|
||||
)
|
||||
# 7002-SORD-091
|
||||
expect(page.locator('[for="triggerDirection-fallsBelow-oco"]')).to_have_text(
|
||||
"Falls below"
|
||||
)
|
||||
# 7002-SORD-095
|
||||
expect(page.locator('[for="triggerType-price-oco"]')).to_have_text("Price")
|
||||
expect(page.locator('[for="triggerType-price"]')).to_be_checked
|
||||
# 7002-SORD-095
|
||||
expect(
|
||||
page.locator('[for="triggerType-trailingPercentOffset-oco"]')
|
||||
).to_have_text("Trailing Percent Offset")
|
||||
|
||||
expect(page.locator('[for="order-size-oco"]')).to_have_text("Size")
|
||||
expect(page.locator('[for="order-price-oco"]')).to_have_text("Price")
|
||||
|
||||
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
|
||||
def test_maximum_number_of_active_stop_orders_oco(
|
||||
self, continuous_market, vega: VegaService, page: Page
|
||||
):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(stop_order_btn).click()
|
||||
page.get_by_test_id(stop_limit_order_btn).is_visible()
|
||||
page.get_by_test_id(stop_limit_order_btn).click()
|
||||
page.get_by_test_id(order_side_sell).click()
|
||||
page.locator("label").filter(has_text="Falls below").click()
|
||||
page.get_by_test_id(trigger_price).fill("102")
|
||||
page.get_by_test_id(order_size).fill("3")
|
||||
page.get_by_test_id(order_price).fill("103")
|
||||
page.get_by_test_id(oco).click()
|
||||
page.get_by_test_id(trigger_price_oco).fill("120")
|
||||
page.get_by_test_id(order_size_oco).fill("2")
|
||||
page.get_by_test_id(order_limit_price_oco).fill("99")
|
||||
for i in range(2):
|
||||
page.get_by_test_id(submit_stop_order).click()
|
||||
wait_for_toast_confirmation(page)
|
||||
vega.wait_fn(1)
|
||||
vega.forward("20s")
|
||||
vega.wait_for_total_catchup()
|
||||
if page.get_by_test_id(close_toast).is_visible():
|
||||
page.get_by_test_id(close_toast).click()
|
||||
# 7002-SORD-011
|
||||
expect(page.get_by_test_id("stop-order-warning-limit")).to_have_text(
|
||||
"There is a limit of 4 active stop orders per market. Orders submitted above the limit will be immediately rejected."
|
||||
)
|
||||
|
||||
@@ -47,48 +47,6 @@ class TestIcebergOrdersValidations:
|
||||
(page.get_by_role("row").locator('[col-id="type"]')).nth(1)
|
||||
).to_have_text("Limit (Iceberg)")
|
||||
|
||||
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
|
||||
def test_iceberg_tooltips(self, continuous_market, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id("iceberg").hover()
|
||||
expect(page.get_by_role("tooltip")).to_be_visible()
|
||||
page.get_by_test_id("iceberg").click()
|
||||
hover_and_assert_tooltip(page, "Peak size")
|
||||
hover_and_assert_tooltip(page, "Minimum size")
|
||||
|
||||
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
|
||||
def test_iceberg_validations(self, continuous_market, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id("iceberg").click()
|
||||
page.get_by_test_id("place-order").click()
|
||||
expect(page.get_by_test_id("deal-ticket-peak-error-message")).to_be_visible()
|
||||
expect(page.get_by_test_id("deal-ticket-peak-error-message")).to_have_text(
|
||||
"You need to provide a peak size"
|
||||
)
|
||||
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_be_visible()
|
||||
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_have_text(
|
||||
"You need to provide a minimum visible size"
|
||||
)
|
||||
page.get_by_test_id("order-peak-size").clear()
|
||||
page.get_by_test_id("order-peak-size").type("1")
|
||||
page.get_by_test_id("order-minimum-size").clear()
|
||||
page.get_by_test_id("order-minimum-size").type("2")
|
||||
expect(page.get_by_test_id("deal-ticket-peak-error-message")).to_be_visible()
|
||||
expect(page.get_by_test_id("deal-ticket-peak-error-message")).to_have_text(
|
||||
"Peak size cannot be greater than the size (0)"
|
||||
)
|
||||
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_be_visible()
|
||||
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_have_text(
|
||||
"Minimum visible size cannot be greater than the peak size (1)"
|
||||
)
|
||||
page.get_by_test_id("order-minimum-size").clear()
|
||||
page.get_by_test_id("order-minimum-size").type("0.1")
|
||||
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_be_visible()
|
||||
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_have_text(
|
||||
"Minimum visible size cannot be lower than 1"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("vega", "page", "continuous_market", "auth", "risk_accepted")
|
||||
def test_iceberg_open_order(continuous_market, vega: VegaService, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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
|
||||
@@ -10,6 +11,8 @@ 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 +21,32 @@ def test_market_lifecycle(proposed_market, vega: VegaService, page: Page):
|
||||
# setup market in proposed step, without liquidity provided
|
||||
market_id = proposed_market
|
||||
page.goto(f"/#/markets/{market_id}")
|
||||
|
||||
# 6002-MDET-001
|
||||
expect(page.get_by_test_id("header-title")).to_have_text("BTC:DAI_2023Futr")
|
||||
# 6002-MDET-002
|
||||
expect(page.get_by_test_id("market-expiry")).to_have_text("ExpiryNot time-based")
|
||||
page.get_by_test_id("market-expiry").hover()
|
||||
expect(page.get_by_test_id("expiry-tooltip").first).to_have_text("This market expires when triggered by its oracle, not on a set date.View oracle specification")
|
||||
expect(page.get_by_test_id("expiry-tooltip").first.get_by_test_id("link")).to_have_attribute("href", re.compile('.*'))
|
||||
# 6002-MDET-003
|
||||
expect(page.get_by_test_id("market-price")).to_have_text("Mark Price0.00")
|
||||
# 6002-MDET-004
|
||||
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
|
||||
# 6002-MDET-005
|
||||
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
|
||||
# 6002-MDET-008
|
||||
expect(page.get_by_test_id("market-settlement-asset")).to_have_text("Settlement assettDAI")
|
||||
expect(page.get_by_test_id("liquidity-supplied")).to_have_text("Liquidity supplied 0.00 (0.00%)")
|
||||
page.get_by_test_id("liquidity-supplied").hover()
|
||||
expect(page.get_by_test_id("liquidity-supplied-tooltip").first).to_have_text("Supplied stake0.00Target stake0.00View liquidity provision tableLearn about providing liquidity")
|
||||
expect(page.get_by_test_id("liquidity-supplied-tooltip").first.get_by_test_id("link").first).to_have_text("View liquidity provision table")
|
||||
# check that market is in proposed state
|
||||
# 6002-MDET-006
|
||||
# 6002-MDET-007
|
||||
# 7002-SORD-061
|
||||
expect(trading_mode).to_have_text("No trading")
|
||||
trading_mode.hover()
|
||||
expect(page.get_by_test_id("trading-mode-tooltip").first).to_have_text("No trading enabled for this market.")
|
||||
expect(market_state).to_have_text("Proposed")
|
||||
|
||||
# approve market
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import pytest
|
||||
import pytest
|
||||
import re
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.service import VegaService
|
||||
@@ -20,23 +20,23 @@ def test_transfer_submit(continuous_market, vega: VegaService, page: Page):
|
||||
# 1003-TRAN-010
|
||||
# 1003-TRAN-023
|
||||
page.goto('/#/portfolio')
|
||||
|
||||
|
||||
expect(page.get_by_test_id('transfer-form')).to_be_visible
|
||||
page.get_by_test_id('select-asset').click()
|
||||
expect(page.get_by_test_id('rich-select-option')).to_have_count(1)
|
||||
|
||||
|
||||
page.get_by_test_id('rich-select-option').click()
|
||||
page.select_option('[data-testid=transfer-form] [name="toVegaKey"]', index=2)
|
||||
page.select_option('[data-testid=transfer-form] [name="fromAccount"]', index=1)
|
||||
|
||||
expected_asset_text = re.compile(r"tDAI tDAI999,991.49731 tDAI.{6}….{4}")
|
||||
|
||||
expected_asset_text = re.compile(r"tDAI tDAI999991.49731 tDAI.{6}….{4}")
|
||||
actual_asset_text = page.get_by_test_id('select-asset').text_content().strip()
|
||||
|
||||
|
||||
assert expected_asset_text.search(actual_asset_text), f"Expected pattern not found in {actual_asset_text}"
|
||||
|
||||
|
||||
page.locator('[data-testid=transfer-form] input[name="amount"]').fill('1')
|
||||
expect(page.locator('[data-testid=transfer-form] input[name="amount"]')).not_to_be_empty()
|
||||
|
||||
|
||||
page.locator('[data-testid=transfer-form] [type="submit"]').click()
|
||||
wait_for_toast_confirmation(page)
|
||||
vega.forward("10s")
|
||||
@@ -45,15 +45,15 @@ def test_transfer_submit(continuous_market, vega: VegaService, page: Page):
|
||||
expected_confirmation_text = re.compile(r"Transfer completeYour transaction has been confirmedView in block explorerTransferTo .{6}….{6}1\.00 tDAI")
|
||||
actual_confirmation_text = page.get_by_test_id('toast-content').text_content()
|
||||
assert expected_confirmation_text.search(actual_confirmation_text), f"Expected pattern not found in {actual_confirmation_text}"
|
||||
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
|
||||
def test_transfer_vesting_below_minimum(continuous_market, vega: VegaService, page: Page):
|
||||
vega.update_network_parameter(
|
||||
"market_maker", parameter="transfer.minTransferQuantumMultiple", new_value="100000"
|
||||
"market_maker", parameter="transfer.minTransferQuantumMultiple", new_value="100000"
|
||||
)
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
|
||||
create_and_faucet_wallet(vega=vega, wallet=PARTY_A, amount=1e3)
|
||||
create_and_faucet_wallet(vega=vega, wallet=PARTY_B, amount=1e5)
|
||||
create_and_faucet_wallet(vega=vega, wallet=PARTY_C, amount=1e5)
|
||||
@@ -96,11 +96,11 @@ def test_transfer_vesting_below_minimum(continuous_market, vega: VegaService, pa
|
||||
next_epoch(vega=vega)
|
||||
page.goto('/#/portfolio')
|
||||
expect(page.get_by_test_id('transfer-form')).to_be_visible
|
||||
|
||||
|
||||
change_keys(page, vega, "party_b")
|
||||
page.get_by_test_id('select-asset').click()
|
||||
page.get_by_test_id('rich-select-option').click()
|
||||
|
||||
|
||||
option_value = page.locator('[data-testid="transfer-form"] [name="fromAccount"] option[value^="ACCOUNT_TYPE_VESTED_REWARDS"]').first.get_attribute("value")
|
||||
|
||||
page.select_option('[data-testid="transfer-form"] [name="fromAccount"]', option_value)
|
||||
@@ -120,7 +120,7 @@ def test_transfer_vesting_below_minimum(continuous_market, vega: VegaService, pa
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(10)
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
|
||||
page.get_by_text("Use max").first.click()
|
||||
page.locator('[data-testid=transfer-form] [type="submit"]').click()
|
||||
wait_for_toast_confirmation(page)
|
||||
@@ -129,4 +129,4 @@ def test_transfer_vesting_below_minimum(continuous_market, vega: VegaService, pa
|
||||
vega.wait_for_total_catchup()
|
||||
expected_confirmation_text = re.compile(r"Transfer completeYour transaction has been confirmedView in block explorerTransferTo .{6}….{6}0\.00001 tDAI")
|
||||
actual_confirmation_text = page.get_by_test_id('toast-content').text_content()
|
||||
assert expected_confirmation_text.search(actual_confirmation_text), f"Expected pattern not found in {actual_confirmation_text}"
|
||||
assert expected_confirmation_text.search(actual_confirmation_text), f"Expected pattern not found in {actual_confirmation_text}"
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import { act, render, screen, within } from '@testing-library/react';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import type { AccountFields } from './accounts-data-provider';
|
||||
import { getAccountData } from './accounts-data-provider';
|
||||
import { AccountTable } from './accounts-table';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const asset1 = {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-1',
|
||||
symbol: 'tBTC',
|
||||
decimals: 5,
|
||||
name: 'T BTC',
|
||||
};
|
||||
const asset2 = {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-2',
|
||||
symbol: 'aBTC',
|
||||
decimals: 5,
|
||||
name: 'A BTC',
|
||||
};
|
||||
const singleRow = {
|
||||
__typename: 'AccountBalance',
|
||||
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
@@ -13,12 +26,7 @@ const singleRow = {
|
||||
__typename: 'Market',
|
||||
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
decimals: 5,
|
||||
},
|
||||
asset: asset1,
|
||||
available: '125600000',
|
||||
used: '125600000',
|
||||
total: '251200000',
|
||||
@@ -33,12 +41,7 @@ const secondRow = {
|
||||
__typename: 'Market',
|
||||
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'aBTC',
|
||||
decimals: 5,
|
||||
},
|
||||
asset: asset2,
|
||||
available: '125600001',
|
||||
used: '125600001',
|
||||
total: '251200002',
|
||||
@@ -134,7 +137,7 @@ describe('AccountsTable', () => {
|
||||
|
||||
it('should sort assets', async () => {
|
||||
// 7001-COLL-010
|
||||
const { container } = render(
|
||||
render(
|
||||
<AccountTable
|
||||
rowData={multiRowData}
|
||||
onClickAsset={() => null}
|
||||
@@ -142,13 +145,13 @@ describe('AccountsTable', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
const headerCell = screen.getByText('Asset');
|
||||
await userEvent.click(headerCell);
|
||||
const rows = container.querySelectorAll(
|
||||
'.ag-center-cols-container .ag-row'
|
||||
);
|
||||
expect(rows[0].textContent).toContain('aBTC');
|
||||
expect(rows[1].textContent).toContain('tBTC');
|
||||
const headerCell = screen
|
||||
.getAllByRole('columnheader')
|
||||
.find((h) => h?.getAttribute('col-id') === 'asset.symbol') as HTMLElement;
|
||||
|
||||
await userEvent.click(within(headerCell).getByText(/asset/i));
|
||||
|
||||
expect(headerCell).toHaveAttribute('aria-sort', 'ascending');
|
||||
});
|
||||
|
||||
it('should apply correct formatting in view as user mode', async () => {
|
||||
@@ -176,12 +179,7 @@ describe('AccountsTable', () => {
|
||||
rowData={singleRowData}
|
||||
onClickAsset={() => null}
|
||||
isReadOnly={false}
|
||||
pinnedAsset={{
|
||||
decimals: 5,
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
name: 'tBTC',
|
||||
}}
|
||||
pinnedAsset={asset1}
|
||||
/>
|
||||
);
|
||||
await screen.findAllByRole('rowgroup');
|
||||
@@ -213,23 +211,13 @@ describe('AccountsTable', () => {
|
||||
const result = getAccountData([singleRow]);
|
||||
const expected = [
|
||||
{
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
},
|
||||
asset: asset1,
|
||||
available: '0',
|
||||
balance: '0',
|
||||
breakdown: [
|
||||
{
|
||||
__typename: 'AccountBalance',
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
},
|
||||
asset: asset1,
|
||||
available: '0',
|
||||
balance: '125600000',
|
||||
total: '125600000',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {
|
||||
@@ -7,7 +13,7 @@ import {
|
||||
TransferForm,
|
||||
type TransferFormProps,
|
||||
} from './transfer-form';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
|
||||
import { removeDecimal } from '@vegaprotocol/utils';
|
||||
|
||||
describe('TransferForm', () => {
|
||||
@@ -39,8 +45,7 @@ describe('TransferForm', () => {
|
||||
};
|
||||
|
||||
const amount = '100';
|
||||
const pubKey =
|
||||
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
|
||||
const pubKey = '1'.repeat(64);
|
||||
const asset = {
|
||||
id: 'eur',
|
||||
symbol: '€',
|
||||
@@ -50,10 +55,7 @@ describe('TransferForm', () => {
|
||||
};
|
||||
const props = {
|
||||
pubKey,
|
||||
pubKeys: [
|
||||
pubKey,
|
||||
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
|
||||
],
|
||||
pubKeys: [pubKey, '2'.repeat(64)],
|
||||
feeFactor: '0.001',
|
||||
submitTransfer: jest.fn(),
|
||||
accounts: [
|
||||
@@ -71,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',
|
||||
@@ -177,7 +201,7 @@ describe('TransferForm', () => {
|
||||
|
||||
// Test use max button
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue('1000');
|
||||
expect(amountInput).toHaveValue('1000.00');
|
||||
|
||||
// Test amount validation
|
||||
await userEvent.clear(amountInput);
|
||||
@@ -262,7 +286,7 @@ describe('TransferForm', () => {
|
||||
|
||||
// Test use max button
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue('100');
|
||||
expect(amountInput).toHaveValue('100.00');
|
||||
|
||||
// If transfering from a vested account 'include fees' checkbox should
|
||||
// be disabled and fees should be 0
|
||||
@@ -291,6 +315,88 @@ describe('TransferForm', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles lots of decimal places', async () => {
|
||||
const balance = '904195168829277777';
|
||||
const expectedBalance = '0.904195168829277777';
|
||||
|
||||
const longDecimalAsset = {
|
||||
id: 'assetId',
|
||||
symbol: 'VEGA',
|
||||
name: 'VEGA',
|
||||
decimals: 18,
|
||||
quantum: '1',
|
||||
};
|
||||
|
||||
const account = {
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
asset: longDecimalAsset,
|
||||
balance,
|
||||
};
|
||||
|
||||
const mockSubmit = jest.fn();
|
||||
|
||||
renderComponent({
|
||||
...props,
|
||||
accounts: [account],
|
||||
submitTransfer: mockSubmit,
|
||||
minQuantumMultiple: '100000',
|
||||
});
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('To Vega key'),
|
||||
props.pubKeys[1] // Use not current pubkey so we can check it switches to current pubkey later
|
||||
);
|
||||
|
||||
// Select asset
|
||||
await selectAsset(longDecimalAsset);
|
||||
|
||||
const accountSelect = screen.getByLabelText('From account');
|
||||
const option = within(accountSelect)
|
||||
.getAllByRole('option')
|
||||
.find(
|
||||
(o) => o.getAttribute('value') === `${account.type}-${account.asset.id}`
|
||||
);
|
||||
// plus one for disabled 'please select' option
|
||||
|
||||
expect(option).toHaveTextContent(
|
||||
`${AccountTypeMapping[account.type]} (${expectedBalance} ${
|
||||
account.asset.symbol
|
||||
})`
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
accountSelect,
|
||||
`${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${longDecimalAsset.id}`
|
||||
);
|
||||
|
||||
expect(accountSelect).toHaveValue(
|
||||
`${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${longDecimalAsset.id}`
|
||||
);
|
||||
|
||||
// Check switch back to connected key
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
// Test use max button
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue(expectedBalance);
|
||||
|
||||
await submit();
|
||||
|
||||
await waitFor(() => {
|
||||
// 1003-TRAN-023
|
||||
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(mockSubmit).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKey,
|
||||
asset: longDecimalAsset.id,
|
||||
amount: balance,
|
||||
oneOff: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('IncludeFeesCheckbox', () => {
|
||||
it('validates fields and submits when checkbox is checked', async () => {
|
||||
const mockSubmit = jest.fn();
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
useVegaPublicKey,
|
||||
addDecimal,
|
||||
formatNumber,
|
||||
addDecimalsFormatNumber,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useT } from './use-t';
|
||||
@@ -46,6 +45,7 @@ interface Asset {
|
||||
export interface TransferFormProps {
|
||||
pubKey: string | null;
|
||||
pubKeys: string[] | null;
|
||||
isReadOnly?: boolean;
|
||||
accounts: Array<{
|
||||
type: AccountType;
|
||||
balance: string;
|
||||
@@ -60,6 +60,7 @@ export interface TransferFormProps {
|
||||
export const TransferForm = ({
|
||||
pubKey,
|
||||
pubKeys,
|
||||
isReadOnly,
|
||||
assetId: initialAssetId,
|
||||
feeFactor,
|
||||
submitTransfer,
|
||||
@@ -202,32 +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={formatNumber(a.balance, a.decimals)}
|
||||
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">
|
||||
@@ -255,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]} (
|
||||
{addDecimalsFormatNumber(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">
|
||||
@@ -420,7 +434,7 @@ export const TransferForm = ({
|
||||
type="button"
|
||||
className="absolute right-0 top-0 ml-auto text-xs underline"
|
||||
onClick={() =>
|
||||
setValue('amount', parseFloat(accountBalance).toString(), {
|
||||
setValue('amount', accountBalance, {
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
@@ -460,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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { getAsset, getProductType, getQuoteName } from '@vegaprotocol/markets';
|
||||
import { getAsset, getQuoteName } from '@vegaprotocol/markets';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
|
||||
@@ -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}
|
||||
@@ -297,134 +292,120 @@ export const DealTicketMarginDetails = ({
|
||||
);
|
||||
|
||||
const quoteName = getQuoteName(market);
|
||||
const productType = getProductType(market);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-2 pt-2">
|
||||
{/*
|
||||
TODO: remove this conditional check once the following PRs are deployed
|
||||
and the estimatePosition query is working for perps
|
||||
|
||||
- https://github.com/vegaprotocol/vega/pull/10119
|
||||
- https://github.com/vegaprotocol/vega/pull/10122
|
||||
*/}
|
||||
{productType === 'Future' && (
|
||||
<>
|
||||
<Accordion>
|
||||
<AccordionPanel
|
||||
itemId="margin"
|
||||
trigger={
|
||||
<AccordionPrimitive.Trigger
|
||||
data-testid="accordion-toggle"
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'flex items-center gap-2 text-xs',
|
||||
'group'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-testid={`deal-ticket-fee-margin-required`}
|
||||
key={'value-dropdown'}
|
||||
className="flex items-center justify-between w-full gap-2"
|
||||
>
|
||||
<div className="flex items-center text-left gap-1">
|
||||
<Tooltip
|
||||
description={t(
|
||||
'MARGIN_DIFF_TOOLTIP_TEXT',
|
||||
MARGIN_DIFF_TOOLTIP_TEXT,
|
||||
{ assetSymbol }
|
||||
)}
|
||||
>
|
||||
<span className="text-muted">
|
||||
{t('Margin required')}
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
<AccordionChevron size={10} />
|
||||
</div>
|
||||
<Tooltip
|
||||
description={
|
||||
formatRange(
|
||||
marginRequiredBestCase,
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals
|
||||
) ?? '-'
|
||||
}
|
||||
>
|
||||
<div className="font-mono text-right">
|
||||
{formatValue(
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}{' '}
|
||||
{assetSymbol || ''}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</AccordionPrimitive.Trigger>
|
||||
}
|
||||
<Accordion>
|
||||
<AccordionPanel
|
||||
itemId="margin"
|
||||
trigger={
|
||||
<AccordionPrimitive.Trigger
|
||||
data-testid="accordion-toggle"
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'flex items-center gap-2 text-xs',
|
||||
'group'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
<KeyValue
|
||||
label={t('Total margin available')}
|
||||
indent
|
||||
value={formatValue(totalMarginAvailable, assetDecimals)}
|
||||
formattedValue={formatValue(
|
||||
totalMarginAvailable,
|
||||
<div
|
||||
data-testid={`deal-ticket-fee-margin-required`}
|
||||
key={'value-dropdown'}
|
||||
className="flex items-center justify-between w-full gap-2"
|
||||
>
|
||||
<div className="flex items-center text-left gap-1">
|
||||
<Tooltip
|
||||
description={t(
|
||||
'MARGIN_DIFF_TOOLTIP_TEXT',
|
||||
MARGIN_DIFF_TOOLTIP_TEXT,
|
||||
{ assetSymbol }
|
||||
)}
|
||||
>
|
||||
<span className="text-muted">{t('Margin required')}</span>
|
||||
</Tooltip>
|
||||
|
||||
<AccordionChevron size={10} />
|
||||
</div>
|
||||
<Tooltip
|
||||
description={
|
||||
formatRange(
|
||||
marginRequiredBestCase,
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals
|
||||
) ?? '-'
|
||||
}
|
||||
>
|
||||
<div className="font-mono text-right">
|
||||
{formatValue(
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}{' '}
|
||||
{assetSymbol || ''}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</AccordionPrimitive.Trigger>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
<KeyValue
|
||||
label={t('Total margin available')}
|
||||
indent
|
||||
value={formatValue(totalMarginAvailable, assetDecimals)}
|
||||
formattedValue={formatValue(
|
||||
totalMarginAvailable,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={t(
|
||||
'TOTAL_MARGIN_AVAILABLE',
|
||||
TOTAL_MARGIN_AVAILABLE,
|
||||
{
|
||||
generalAccountBalance: formatValue(
|
||||
generalAccountBalance,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={t(
|
||||
'TOTAL_MARGIN_AVAILABLE',
|
||||
TOTAL_MARGIN_AVAILABLE,
|
||||
{
|
||||
generalAccountBalance: formatValue(
|
||||
generalAccountBalance,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
marginAccountBalance: formatValue(
|
||||
marginAccountBalance,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
marginMaintenance: formatValue(
|
||||
currentMargins?.maintenanceLevel,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
assetSymbol,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
{deductionFromCollateral}
|
||||
<KeyValue
|
||||
label={t('Current margin allocation')}
|
||||
indent
|
||||
onClick={
|
||||
generalAccountBalance
|
||||
? () => setBreakdownDialog(true)
|
||||
: undefined
|
||||
}
|
||||
value={formatValue(marginAccountBalance, assetDecimals)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={t(
|
||||
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT
|
||||
)}
|
||||
formattedValue={formatValue(
|
||||
),
|
||||
marginAccountBalance: formatValue(
|
||||
marginAccountBalance,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
{projectedMargin}
|
||||
</>
|
||||
)}
|
||||
),
|
||||
marginMaintenance: formatValue(
|
||||
currentMargins?.maintenanceLevel,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
assetSymbol,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
{deductionFromCollateral}
|
||||
<KeyValue
|
||||
label={t('Current margin allocation')}
|
||||
indent
|
||||
onClick={
|
||||
generalAccountBalance
|
||||
? () => setBreakdownDialog(true)
|
||||
: undefined
|
||||
}
|
||||
value={formatValue(marginAccountBalance, assetDecimals)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={t(
|
||||
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT
|
||||
)}
|
||||
formattedValue={formatValue(
|
||||
marginAccountBalance,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
{projectedMargin}
|
||||
<KeyValue
|
||||
label={t('Liquidation')}
|
||||
value={liquidationPriceEstimateRange}
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('StopOrder', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should display ticket defaults', async () => {
|
||||
it('should display ticket defaults limit order', async () => {
|
||||
render(generateJsx());
|
||||
// place order button should always be enabled
|
||||
expect(screen.getByTestId(submitButton)).toBeEnabled();
|
||||
@@ -131,6 +131,47 @@ describe('StopOrder', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should display ticket defaults market order', async () => {
|
||||
render(generateJsx());
|
||||
// place order button should always be enabled
|
||||
expect(screen.getByTestId(submitButton)).toBeEnabled();
|
||||
// Assert defaults are used
|
||||
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||
await userEvent.click(screen.getByTestId(orderTypeMarket));
|
||||
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||
expect(screen.getByTestId(orderTypeLimit).dataset.state).toEqual(
|
||||
'unchecked'
|
||||
);
|
||||
expect(screen.getByTestId(orderTypeMarket).dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
await userEvent.click(screen.getByTestId(orderTypeMarket));
|
||||
expect(screen.getByTestId(orderSideBuy).dataset.state).toEqual('checked');
|
||||
expect(screen.getByTestId(sizeInput)).toHaveDisplayValue('0');
|
||||
expect(screen.getByTestId(timeInForce)).toHaveValue(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
|
||||
);
|
||||
// 7002-SORD-084
|
||||
expect(
|
||||
screen.getByTestId(triggerDirectionRisesAbove).dataset.state
|
||||
).toEqual('checked');
|
||||
// 7002-SORD-085
|
||||
expect(
|
||||
screen.getByTestId(triggerDirectionFallsBelow).dataset.state
|
||||
).toEqual('unchecked');
|
||||
expect(screen.getByTestId(triggerTypePrice).dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
expect(screen.getByTestId(expire).dataset.state).toEqual('unchecked');
|
||||
expect(screen.getByTestId(oco).dataset.state).toEqual('unchecked');
|
||||
await userEvent.click(screen.getByTestId(expire));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual(
|
||||
'checked'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('calculate notional for market limit', async () => {
|
||||
render(generateJsx());
|
||||
await userEvent.type(screen.getByTestId(sizeInput), '10');
|
||||
@@ -239,33 +280,43 @@ describe('StopOrder', () => {
|
||||
it.each([
|
||||
{ fieldName: 'size', ocoValue: false },
|
||||
{ fieldName: 'ocoSize', ocoValue: true },
|
||||
])('validates $fieldName field', async ({ ocoValue }) => {
|
||||
render(generateJsx());
|
||||
if (ocoValue) {
|
||||
await userEvent.click(screen.getByTestId(oco));
|
||||
}
|
||||
await userEvent.click(screen.getByTestId(submitButton));
|
||||
const getByTestId = (id: string) =>
|
||||
screen.getByTestId(ocoPostfix(id, ocoValue));
|
||||
const queryByTestId = (id: string) =>
|
||||
screen.queryByTestId(ocoPostfix(id, ocoValue));
|
||||
// default value should be invalid
|
||||
expect(getByTestId(sizeErrorMessage)).toBeInTheDocument();
|
||||
// to small value should be invalid
|
||||
await userEvent.type(getByTestId(sizeInput), '0.01');
|
||||
expect(getByTestId(sizeErrorMessage)).toBeInTheDocument();
|
||||
{ fieldName: 'size', ocoValue: false, orderTypeMarketValue: true },
|
||||
{ fieldName: 'ocoSize', ocoValue: true, orderTypeMarketValue: true },
|
||||
])(
|
||||
'validates $fieldName field',
|
||||
async ({ ocoValue, orderTypeMarketValue }) => {
|
||||
render(generateJsx());
|
||||
if (orderTypeMarketValue) {
|
||||
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||
await userEvent.click(screen.getByTestId(orderTypeMarket));
|
||||
}
|
||||
if (ocoValue) {
|
||||
await userEvent.click(screen.getByTestId(oco));
|
||||
}
|
||||
await userEvent.click(screen.getByTestId(submitButton));
|
||||
const getByTestId = (id: string) =>
|
||||
screen.getByTestId(ocoPostfix(id, ocoValue));
|
||||
const queryByTestId = (id: string) =>
|
||||
screen.queryByTestId(ocoPostfix(id, ocoValue));
|
||||
// default value should be invalid
|
||||
expect(getByTestId(sizeErrorMessage)).toBeInTheDocument();
|
||||
// to small value should be invalid
|
||||
await userEvent.type(getByTestId(sizeInput), '0.01');
|
||||
expect(getByTestId(sizeErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// clear and fill using valid value
|
||||
await userEvent.clear(getByTestId(sizeInput));
|
||||
await userEvent.type(getByTestId(sizeInput), '0.1');
|
||||
expect(queryByTestId(sizeErrorMessage)).toBeNull();
|
||||
});
|
||||
// clear and fill using valid value
|
||||
await userEvent.clear(getByTestId(sizeInput));
|
||||
await userEvent.type(getByTestId(sizeInput), '0.1');
|
||||
expect(queryByTestId(sizeErrorMessage)).toBeNull();
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ fieldName: 'price', ocoValue: false },
|
||||
{ fieldName: 'ocoPrice', ocoValue: true },
|
||||
])('validates $fieldName field', async ({ ocoValue }) => {
|
||||
render(generateJsx());
|
||||
|
||||
if (ocoValue) {
|
||||
await userEvent.click(screen.getByTestId(oco));
|
||||
}
|
||||
@@ -275,7 +326,7 @@ describe('StopOrder', () => {
|
||||
screen.getByTestId(ocoPostfix(id, ocoValue));
|
||||
const queryByTestId = (id: string) =>
|
||||
screen.queryByTestId(ocoPostfix(id, ocoValue));
|
||||
|
||||
// 7002-SORD-095
|
||||
expect(getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||
await userEvent.type(getByTestId(priceInput), '0.001');
|
||||
expect(getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||
@@ -305,48 +356,77 @@ describe('StopOrder', () => {
|
||||
it.each([
|
||||
{ fieldName: 'triggerPrice', ocoValue: false },
|
||||
{ fieldName: 'ocoTriggerPrice', ocoValue: true },
|
||||
])('validates $fieldName field', async ({ ocoValue }) => {
|
||||
render(generateJsx());
|
||||
{ fieldName: 'triggerPrice', ocoValue: false, orderTypeMarketValue: true },
|
||||
{
|
||||
fieldName: 'ocoTriggerPrice',
|
||||
ocoValue: true,
|
||||
orderTypeMarketValue: true,
|
||||
},
|
||||
])(
|
||||
'validates $fieldName field',
|
||||
async ({ ocoValue, orderTypeMarketValue }) => {
|
||||
render(generateJsx());
|
||||
if (orderTypeMarketValue) {
|
||||
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||
await userEvent.click(screen.getByTestId(orderTypeMarket));
|
||||
}
|
||||
if (ocoValue) {
|
||||
await userEvent.click(screen.getByTestId(oco));
|
||||
await userEvent.click(screen.getByTestId(triggerDirectionFallsBelow));
|
||||
}
|
||||
await userEvent.click(screen.getByTestId(submitButton));
|
||||
const getByTestId = (id: string) =>
|
||||
screen.getByTestId(ocoPostfix(id, ocoValue));
|
||||
const queryByTestId = (id: string) =>
|
||||
screen.queryByTestId(ocoPostfix(id, ocoValue));
|
||||
// 7002-SORD-095
|
||||
// 7002-SORD-087
|
||||
|
||||
if (ocoValue) {
|
||||
await userEvent.click(screen.getByTestId(oco));
|
||||
await userEvent.click(screen.getByTestId(triggerDirectionFallsBelow));
|
||||
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// switch to trailing percentage offset trigger type
|
||||
await userEvent.click(getByTestId(triggerTypeTrailingPercentOffset));
|
||||
expect(queryByTestId(triggerPriceErrorMessage)).toBeNull();
|
||||
|
||||
// switch back to price trigger type
|
||||
await userEvent.click(getByTestId(triggerTypePrice));
|
||||
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// to small value should be invalid
|
||||
await userEvent.type(getByTestId(triggerPriceInput), '0.001');
|
||||
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// clear and fill using value causing immediate trigger
|
||||
await userEvent.clear(getByTestId(triggerPriceInput));
|
||||
await userEvent.type(getByTestId(triggerPriceInput), '0.01');
|
||||
expect(queryByTestId(triggerPriceErrorMessage)).toBeNull();
|
||||
expect(queryByTestId(triggerPriceWarningMessage)).toBeInTheDocument();
|
||||
|
||||
// change to correct value
|
||||
await userEvent.type(getByTestId(triggerPriceInput), '2');
|
||||
expect(queryByTestId(triggerPriceWarningMessage)).toBeNull();
|
||||
}
|
||||
await userEvent.click(screen.getByTestId(submitButton));
|
||||
const getByTestId = (id: string) =>
|
||||
screen.getByTestId(ocoPostfix(id, ocoValue));
|
||||
const queryByTestId = (id: string) =>
|
||||
screen.queryByTestId(ocoPostfix(id, ocoValue));
|
||||
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// switch to trailing percentage offset trigger type
|
||||
await userEvent.click(getByTestId(triggerTypeTrailingPercentOffset));
|
||||
expect(queryByTestId(triggerPriceErrorMessage)).toBeNull();
|
||||
|
||||
// switch back to price trigger type
|
||||
await userEvent.click(getByTestId(triggerTypePrice));
|
||||
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// to small value should be invalid
|
||||
await userEvent.type(getByTestId(triggerPriceInput), '0.001');
|
||||
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||
|
||||
// clear and fill using value causing immediate trigger
|
||||
await userEvent.clear(getByTestId(triggerPriceInput));
|
||||
await userEvent.type(getByTestId(triggerPriceInput), '0.01');
|
||||
expect(queryByTestId(triggerPriceErrorMessage)).toBeNull();
|
||||
expect(queryByTestId(triggerPriceWarningMessage)).toBeInTheDocument();
|
||||
|
||||
// change to correct value
|
||||
await userEvent.type(getByTestId(triggerPriceInput), '2');
|
||||
expect(queryByTestId(triggerPriceWarningMessage)).toBeNull();
|
||||
});
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ fieldName: 'trailingPercentageOffset', ocoValue: false },
|
||||
{ fieldName: 'ocoTrailingPercentageOffset', ocoValue: true },
|
||||
{
|
||||
fieldName: 'trailingPercentageOffset',
|
||||
ocoValue: false,
|
||||
orderTypeMarket: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'ocoTrailingPercentageOffset',
|
||||
ocoValue: true,
|
||||
orderTypeMarket: true,
|
||||
},
|
||||
])('validates $fieldName field', async ({ ocoValue }) => {
|
||||
render(generateJsx());
|
||||
if (orderTypeMarket) {
|
||||
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||
await userEvent.click(screen.getByTestId(orderTypeMarket));
|
||||
}
|
||||
if (ocoValue) {
|
||||
await userEvent.click(screen.getByTestId(oco));
|
||||
}
|
||||
@@ -401,9 +481,11 @@ describe('StopOrder', () => {
|
||||
it('sync oco trigger', async () => {
|
||||
render(generateJsx());
|
||||
await userEvent.click(screen.getByTestId(oco));
|
||||
// 7002-SORD-099
|
||||
expect(
|
||||
screen.getByTestId(triggerDirectionRisesAbove).dataset.state
|
||||
).toEqual('checked');
|
||||
// 7002-SORD-091
|
||||
expect(
|
||||
screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow)).dataset.state
|
||||
).toEqual('checked');
|
||||
@@ -481,6 +563,7 @@ describe('StopOrder', () => {
|
||||
expect(mockDataProvider.mock.lastCall?.[0].skip).toBe(true);
|
||||
await userEvent.type(screen.getByTestId(sizeInput), '0.01');
|
||||
expect(mockDataProvider.mock.lastCall?.[0].skip).toBe(false);
|
||||
// 7002-SORD-011
|
||||
expect(screen.getByTestId(numberOfActiveOrdersLimit)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -367,6 +367,29 @@ describe('DealTicket', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should see an explanation of peak size', async () => {
|
||||
render(generateJsx());
|
||||
await userEvent.click(screen.getByTestId('iceberg'));
|
||||
await userEvent.hover(screen.getByText('Peak size'));
|
||||
await waitFor(() => {
|
||||
const tooltips = screen.getAllByTestId('tooltip-content');
|
||||
expect(tooltips[0]).toHaveTextContent(
|
||||
`The maximum volume that can be traded at once. Must be less than the total size of the order.`
|
||||
);
|
||||
});
|
||||
});
|
||||
it('should see an explanation of minimum size', async () => {
|
||||
render(generateJsx());
|
||||
await userEvent.click(screen.getByTestId('iceberg'));
|
||||
await userEvent.hover(screen.getByText('Minimum size'));
|
||||
await waitFor(() => {
|
||||
const tooltips = screen.getAllByTestId('tooltip-content');
|
||||
expect(tooltips[0]).toHaveTextContent(
|
||||
`When the order trades and its size falls below this threshold, it will be reset to the peak size and moved to the back of the priority order. Must be less than or equal to peak size, and greater than 0.`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should see an explanation of reduce only', async () => {
|
||||
render(generateJsx());
|
||||
userEvent.hover(screen.getByText('Reduce only'));
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -85,6 +85,7 @@ export const DocsLinks = VEGA_DOCS_URL
|
||||
ICEBERG_ORDERS: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#iceberg-order`,
|
||||
POST_REDUCE_ONLY: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#conditional-order-parameters`,
|
||||
QUANTUM: `${VEGA_DOCS_URL}/concepts/assets/asset-framework#quantum`,
|
||||
REFERRALS: `${VEGA_DOCS_URL}/tutorials/proposals/referral-program-proposal`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -81,6 +81,7 @@ const COSMIC_ELEVATOR_FLAGS = {
|
||||
UPDATE_MARKET_STATE: z.optional(z.boolean()),
|
||||
GOVERNANCE_TRANSFERS: z.optional(z.boolean()),
|
||||
VOLUME_DISCOUNTS: z.optional(z.boolean()),
|
||||
DISABLE_CLOSE_POSITION: z.optional(z.boolean()),
|
||||
};
|
||||
|
||||
const EXPLORER_FLAGS = {
|
||||
|
||||
@@ -4,14 +4,8 @@ import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type { Trade } from './fills-data-provider';
|
||||
import {
|
||||
FeesDiscountBreakdownTooltip,
|
||||
FillsTable,
|
||||
getFeesBreakdown,
|
||||
getTotalFeesDiscounts,
|
||||
} from './fills-table';
|
||||
import { FeesDiscountBreakdownTooltip, FillsTable } from './fills-table';
|
||||
import { generateFill } from './test-helpers';
|
||||
import type { TradeFeeFieldsFragment } from './__generated__/Fills';
|
||||
|
||||
const partyId = 'party-id';
|
||||
const defaultFill: PartialDeep<Trade> = {
|
||||
@@ -35,6 +29,7 @@ const defaultFill: PartialDeep<Trade> = {
|
||||
},
|
||||
createdAt: new Date('2022-02-02T14:00:00').toISOString(),
|
||||
};
|
||||
|
||||
describe('FillsTable', () => {
|
||||
it('correct columns are rendered', async () => {
|
||||
// 7005-FILL-001
|
||||
@@ -65,7 +60,7 @@ describe('FillsTable', () => {
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
|
||||
});
|
||||
|
||||
it('formats cells correctly for buyer fill', async () => {
|
||||
it('formats cells correctly for buyer fill for maker', async () => {
|
||||
const buyerFill = generateFill({
|
||||
...defaultFill,
|
||||
buyer: {
|
||||
@@ -89,7 +84,7 @@ describe('FillsTable', () => {
|
||||
'3.00 BTC',
|
||||
'Maker',
|
||||
'2.00 BTC',
|
||||
'0.27 BTC',
|
||||
'0.09 BTC',
|
||||
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
|
||||
'', // action column
|
||||
];
|
||||
@@ -271,96 +266,48 @@ describe('FillsTable', () => {
|
||||
.find((c) => c.getAttribute('col-id') === 'size');
|
||||
expect(sizeCell).toHaveTextContent('3,000,000,000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FeesDiscountBreakdownTooltip', () => {
|
||||
it('shows all discounts', () => {
|
||||
const data = generateFill({
|
||||
...defaultFill,
|
||||
buyer: {
|
||||
id: partyId,
|
||||
},
|
||||
});
|
||||
const props = {
|
||||
data,
|
||||
partyId,
|
||||
value: data.market,
|
||||
} as Parameters<typeof FeesDiscountBreakdownTooltip>['0'];
|
||||
const { container } = render(<FeesDiscountBreakdownTooltip {...props} />);
|
||||
const dt = container.querySelectorAll('dt');
|
||||
const dd = container.querySelectorAll('dd');
|
||||
const expectedDt = [
|
||||
'Infrastructure Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Liquidity Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Maker Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
];
|
||||
const expectedDD = [
|
||||
'0.05 BTC',
|
||||
'0.06 BTC',
|
||||
'0.01 BTC',
|
||||
'0.02 BTC',
|
||||
'0.03 BTC',
|
||||
'0.04 BTC',
|
||||
];
|
||||
expectedDt.forEach((label, i) => {
|
||||
expect(dt[i]).toHaveTextContent(label);
|
||||
});
|
||||
expectedDD.forEach((label, i) => {
|
||||
expect(dd[i]).toHaveTextContent(label);
|
||||
describe('FeesDiscountBreakdownTooltip', () => {
|
||||
it('shows all discounts', () => {
|
||||
const data = generateFill({
|
||||
...defaultFill,
|
||||
buyer: {
|
||||
id: partyId,
|
||||
},
|
||||
});
|
||||
const props = {
|
||||
data,
|
||||
partyId,
|
||||
value: data.market,
|
||||
} as Parameters<typeof FeesDiscountBreakdownTooltip>['0'];
|
||||
const { container } = render(<FeesDiscountBreakdownTooltip {...props} />);
|
||||
const dt = container.querySelectorAll('dt');
|
||||
const dd = container.querySelectorAll('dd');
|
||||
const expectedDt = [
|
||||
'Infrastructure Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Liquidity Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Maker Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
];
|
||||
const expectedDD = [
|
||||
'0.05 BTC',
|
||||
'0.06 BTC',
|
||||
'0.01 BTC',
|
||||
'0.02 BTC',
|
||||
'0.03 BTC',
|
||||
'0.04 BTC',
|
||||
];
|
||||
expectedDt.forEach((label, i) => {
|
||||
expect(dt[i]).toHaveTextContent(label);
|
||||
});
|
||||
expectedDD.forEach((label, i) => {
|
||||
expect(dd[i]).toHaveTextContent(label);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFeesBreakdown', () => {
|
||||
it('should return correct fees breakdown for a taker', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '1000',
|
||||
totalFee: '6000',
|
||||
};
|
||||
expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a maker', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '-1000',
|
||||
totalFee: '4000',
|
||||
};
|
||||
expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTotalFeesDiscounts', () => {
|
||||
it('should return correct total value', () => {
|
||||
const fees = {
|
||||
infrastructureFeeReferralDiscount: '1',
|
||||
infrastructureFeeVolumeDiscount: '2',
|
||||
liquidityFeeReferralDiscount: '3',
|
||||
liquidityFeeVolumeDiscount: '4',
|
||||
makerFeeReferralDiscount: '5',
|
||||
makerFeeVolumeDiscount: '6',
|
||||
};
|
||||
expect(getTotalFeesDiscounts(fees as TradeFeeFieldsFragment)).toEqual(
|
||||
(1 + 2 + 3 + 4 + 5 + 6).toString()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user