Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3588be1a32 | ||
|
|
067fe3b8ba | ||
|
|
3cd393dac0 | ||
|
|
a52e60d6a2 | ||
|
|
bc13f1b359 | ||
|
|
eb81f4ae44 | ||
|
|
51ab02a2e2 | ||
|
|
ffada1b93d | ||
|
|
9dda3f712b | ||
|
|
df20dbeee0 | ||
|
|
cdfd8a2d00 | ||
|
|
1e5c523bc4 | ||
|
|
37cd69ba6e | ||
|
|
2c11045dd9 | ||
|
|
9aef41a119 | ||
|
|
80ab8821d0 | ||
|
|
8a3657a9b9 | ||
|
|
7100b0e9fc | ||
|
|
614a83b7d6 | ||
|
|
3dc77b0eff | ||
|
|
a59f7dfd29 | ||
|
|
61471228aa | ||
|
|
127e784ceb | ||
|
|
70d748fb15 | ||
|
|
e06f4818fc | ||
|
|
8182da3b31 | ||
|
|
c8c56307bb | ||
|
|
a8cd7f157f | ||
|
|
4f18caa486 | ||
|
|
5c7c626bbc | ||
|
|
e4c4c20631 | ||
|
|
0697302d07 | ||
|
|
2d926c0ce0 | ||
|
|
f57d6a7c7b | ||
|
|
15f905046f | ||
|
|
4f7918f64e | ||
|
|
52ab0562b0 | ||
|
|
4e2b0d1b1d | ||
|
|
eac26c1966 |
@@ -12,7 +12,8 @@ import { type VegaICellRendererParams } from '@vegaprotocol/datagrid';
|
||||
import { useRef, useLayoutEffect } from 'react';
|
||||
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { type RowClickedEvent, ColDef } from 'ag-grid-community';
|
||||
import { type ColDef } from 'ag-grid-community';
|
||||
import type { RowClickedEvent } from 'ag-grid-community';
|
||||
|
||||
type AssetsTableProps = {
|
||||
data: AssetFieldsFragment[] | null;
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
type VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { type RowClickedEvent, ColDef } from 'ag-grid-community';
|
||||
import { type ColDef } from 'ag-grid-community';
|
||||
import type { RowClickedEvent } from 'ag-grid-community';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useState } from 'react';
|
||||
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
|
||||
import {
|
||||
CopyWithTooltip,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
interface SignatureProps {
|
||||
signature: BlockExplorerTransactionResult['signature'];
|
||||
}
|
||||
|
||||
const valueClass =
|
||||
'font-mono px-2.5 py-0.5 text-xs max-w-[200px] cursor-pointer';
|
||||
const valueClassClosed = 'text-ellipsis overflow-hidden';
|
||||
const valueClassOpen = 'break-words text-left';
|
||||
|
||||
/**
|
||||
* Viewer component for a vega signature. Featuers copy and pasting, truncation
|
||||
*
|
||||
* @param signature
|
||||
*/
|
||||
export const Signature = ({ signature }: SignatureProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
if (!signature || !signature.value || !signature.version || !signature.algo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="inline-flex border rounded signature-component relative pr-[20px]">
|
||||
<span
|
||||
className="bg-gray-100 px-2.5 py-0.5 text-xs text-gray-500 select-none cursor-default"
|
||||
title={`Version ${signature.version}`}
|
||||
>
|
||||
{signature.algo}
|
||||
</span>
|
||||
<div
|
||||
className={
|
||||
isOpen
|
||||
? `${valueClass} ${valueClassOpen}`
|
||||
: `${valueClass} ${valueClassClosed}`
|
||||
}
|
||||
>
|
||||
<CopyWithTooltip text={signature.value}>
|
||||
<span title={signature.value}>{signature.value}</span>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="absolute top-[-3px] right-0 pr-2"
|
||||
title={t('Show full signature')}
|
||||
>
|
||||
<VegaIcon name={isOpen ? VegaIconNames.EYE_OFF : VegaIconNames.EYE} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { Time } from '../../../time';
|
||||
import { ChainResponseCode } from '../chain-response-code/chain-reponse.code';
|
||||
import { TxDataView } from '../../tx-data-view';
|
||||
import Hash from '../../../links/hash';
|
||||
import { Signature } from '../../../signature/signature';
|
||||
|
||||
interface TxDetailsSharedProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@@ -75,6 +76,12 @@ export const TxDetailsShared = ({
|
||||
<BlockLink height={height} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell {...sharedHeaderProps}>{t('Signature')}</TableCell>
|
||||
<TableCell>
|
||||
<Signature signature={txData.signature} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell {...sharedHeaderProps}>{t('Time')}</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@@ -98,6 +98,8 @@ describe('TxDetailsTransfer', () => {
|
||||
},
|
||||
},
|
||||
signature: {
|
||||
version: '1',
|
||||
algo: 'vega/ed25519',
|
||||
value:
|
||||
'610c2e196a7d4fed4413b9e82af267b1ff3e30e943df3a3d28096fd60604d430d752fbaf6dd4f84d496be78885bb6118f40560bff7832c06bd7a3d67b718b700',
|
||||
},
|
||||
|
||||
@@ -20,6 +20,8 @@ const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
|
||||
'4b782482f587d291e8614219eb9a5ee9280fa2c58982dee71d976782a9be1964',
|
||||
type: 'Submit Order',
|
||||
signature: {
|
||||
version: '1',
|
||||
algo: 'vega/ed25519',
|
||||
value: '123',
|
||||
},
|
||||
code: 0,
|
||||
|
||||
@@ -23,6 +23,8 @@ const txData: BlockExplorerTransactionResult = {
|
||||
type: 'type',
|
||||
command: {} as ValidatorHeartbeat,
|
||||
signature: {
|
||||
version: '1',
|
||||
algo: 'vega/ed25519',
|
||||
value: '123',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface BlockExplorerTransactionResult {
|
||||
cursor: string;
|
||||
command: components['schemas']['blockexplorerv1transaction'];
|
||||
signature: {
|
||||
version: string;
|
||||
algo: string;
|
||||
value: string;
|
||||
};
|
||||
error?: string;
|
||||
|
||||
@@ -23,10 +23,13 @@ export const Heading = ({
|
||||
})}
|
||||
>
|
||||
<h1
|
||||
className={classNames('font-alpha calt text-5xl break-words', {
|
||||
'mt-0': !marginTop,
|
||||
'mb-0': !marginBottom,
|
||||
})}
|
||||
className={classNames(
|
||||
'font-alpha calt text-5xl [word-break:break-word]',
|
||||
{
|
||||
'mt-0': !marginTop,
|
||||
'mb-0': !marginBottom,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
+3
@@ -7,8 +7,10 @@ import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
|
||||
export const ProposalAssetDetails = ({
|
||||
asset,
|
||||
originalAsset,
|
||||
}: {
|
||||
asset: AssetFieldsFragment;
|
||||
originalAsset?: AssetFieldsFragment;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [showAssetDetails, setShowAssetDetails] = useState(false);
|
||||
@@ -27,6 +29,7 @@ export const ProposalAssetDetails = ({
|
||||
<div className="mb-10 pb-4">
|
||||
<AssetDetailsTable
|
||||
asset={asset}
|
||||
originalAsset={originalAsset}
|
||||
omitRows={[
|
||||
AssetDetail.STATUS,
|
||||
AssetDetail.INFRASTRUCTURE_FEE_ACCOUNT_BALANCE,
|
||||
|
||||
@@ -65,10 +65,13 @@ export const Proposal = ({
|
||||
? removePaginationWrapper(assetData.assetsConnection?.edges)[0]
|
||||
: undefined;
|
||||
|
||||
const originalAsset = asset;
|
||||
|
||||
if (proposal.terms.change.__typename === 'UpdateAsset' && asset) {
|
||||
asset = {
|
||||
...asset,
|
||||
quantum: proposal.terms.change.quantum,
|
||||
source: { ...asset.source },
|
||||
};
|
||||
|
||||
if (asset.source.__typename === 'ERC20') {
|
||||
@@ -228,7 +231,7 @@ export const Proposal = ({
|
||||
proposal.terms.change.__typename === 'UpdateAsset') &&
|
||||
asset && (
|
||||
<div className="mb-4">
|
||||
<ProposalAssetDetails asset={asset} />
|
||||
<ProposalAssetDetails asset={asset} originalAsset={originalAsset} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const expirtyTooltip = 'expiry-tooltip';
|
||||
const externalLink = 'external-link';
|
||||
const itemHeader = 'item-header';
|
||||
const itemValue = 'item-value';
|
||||
const link = 'link';
|
||||
const liquidityLink = 'view-liquidity-link';
|
||||
const liquiditySupplied = 'liquidity-supplied';
|
||||
const liquiditySuppliedTooltip = 'liquidity-supplied-tooltip';
|
||||
const marketChange = 'market-change';
|
||||
const marketExpiry = 'market-expiry';
|
||||
const marketMode = 'market-trading-mode';
|
||||
const marketName = 'header-title';
|
||||
const marketPrice = 'market-price';
|
||||
const marketSettlement = 'market-settlement-asset';
|
||||
const marketSummaryBlock = 'header-summary';
|
||||
const marketVolume = 'market-volume';
|
||||
const percentageValue = 'price-change-percentage';
|
||||
const priceChangeValue = 'price-change';
|
||||
const tradingModeTooltip = 'trading-mode-tooltip';
|
||||
|
||||
describe('Market trading page', () => {
|
||||
before(() => {
|
||||
cy.clearAllLocalStorage();
|
||||
cy.mockTradingPage(
|
||||
Schema.MarketState.STATE_ACTIVE,
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
|
||||
);
|
||||
cy.mockSubscription();
|
||||
cy.setOnBoardingViewed();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@MarketData');
|
||||
cy.getByTestId(marketSummaryBlock).should('be.visible');
|
||||
});
|
||||
|
||||
describe('Market summary', { tags: '@smoke' }, () => {
|
||||
// 7002-SORD-001
|
||||
// 7002-SORD-002
|
||||
it('must display market name', () => {
|
||||
// 6002-MDET-001
|
||||
cy.getByTestId(marketName).should('not.be.empty');
|
||||
});
|
||||
|
||||
it('must see market expiry', () => {
|
||||
// 6002-MDET-002
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketExpiry).within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Expiry');
|
||||
cy.getByTestId(itemValue).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('must see market price', () => {
|
||||
// 6002-MDET-003
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketPrice).within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Mark Price');
|
||||
cy.getByTestId(itemValue).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('must see market change', () => {
|
||||
// 6002-MDET-004
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketChange).within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Change (24h)');
|
||||
cy.getByTestId(percentageValue).should('not.be.empty');
|
||||
cy.getByTestId(priceChangeValue).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('must see market volume', () => {
|
||||
// 6002-MDET-005
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketVolume).within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Volume (24h)');
|
||||
cy.getByTestId(itemValue).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('must see market settlement', () => {
|
||||
// 6002-MDET-008
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketSettlement).within(() => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Settlement asset');
|
||||
cy.getByTestId(itemValue).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Market tooltips', { tags: '@smoke' }, () => {
|
||||
it('should see expiry tooltip', () => {
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketExpiry).within(() => {
|
||||
cy.getByTestId(itemValue)
|
||||
.should('have.text', 'Not time-based')
|
||||
.realHover();
|
||||
});
|
||||
});
|
||||
cy.getByTestId(expirtyTooltip)
|
||||
.eq(0)
|
||||
.should(
|
||||
'contain.text',
|
||||
'This market expires when triggered by its oracle, not on a set date.'
|
||||
)
|
||||
.within(() => {
|
||||
cy.getByTestId(link)
|
||||
.should('have.attr', 'href')
|
||||
.and('include', Cypress.env('EXPLORER_URL'));
|
||||
});
|
||||
});
|
||||
|
||||
it('should see trading conditions tooltip', () => {
|
||||
const toolTipLabel = 'tooltip-label';
|
||||
const toolTipValue = 'tooltip-value';
|
||||
const auctionToolTipLabels = [
|
||||
'Auction start',
|
||||
'Est. auction end',
|
||||
'Target liquidity',
|
||||
'Current liquidity',
|
||||
'Est. uncrossing price',
|
||||
'Est. uncrossing vol',
|
||||
];
|
||||
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(marketMode).within(() => {
|
||||
cy.getByTestId(itemValue)
|
||||
.should('contain.text', 'Monitoring auction')
|
||||
.and('contain.text', 'liquidity')
|
||||
.realHover();
|
||||
});
|
||||
});
|
||||
cy.getByTestId(tradingModeTooltip)
|
||||
.should(
|
||||
'contain.text',
|
||||
'This market is in auction until it reaches sufficient liquidity.'
|
||||
)
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.getByTestId(externalLink)
|
||||
.should('have.attr', 'href')
|
||||
.and('include', Cypress.env('TRADING_MODE_LINK'));
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
cy.getByTestId(toolTipLabel)
|
||||
.eq(i)
|
||||
.should('have.text', auctionToolTipLabels[i]);
|
||||
cy.getByTestId(toolTipValue).eq(i).should('not.be.empty');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should see liquidity supplied tooltip', () => {
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId(liquiditySupplied).within(() => {
|
||||
cy.getByTestId(itemValue).realHover();
|
||||
});
|
||||
});
|
||||
cy.getByTestId(liquiditySuppliedTooltip)
|
||||
.should('contain.text', 'Supplied stake')
|
||||
.and('contain.text', 'Target stake')
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.getByTestId(liquidityLink).should(
|
||||
'have.text',
|
||||
'View liquidity provision table'
|
||||
);
|
||||
cy.getByTestId(externalLink).should(
|
||||
'have.text',
|
||||
'Learn about providing liquidity'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,103 +0,0 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||
import { createOrder } from '../support/create-order';
|
||||
|
||||
describe(
|
||||
'vega wallet - prompt',
|
||||
{ tags: '@regression', testIsolation: true },
|
||||
() => {
|
||||
describe('must submit order', { tags: '@smoke' }, () => {
|
||||
// 7002-SORD-039
|
||||
beforeEach(() => {
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
it('must see a prompt to check connected vega wallet to approve transaction', () => {
|
||||
// 0003-WTXN-002
|
||||
cy.mockVegaWalletTransaction(1000);
|
||||
const order: OrderSubmission = {
|
||||
marketId: 'market-0',
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
size: '100',
|
||||
};
|
||||
createOrder(order);
|
||||
cy.getByTestId('toast-content').should(
|
||||
'contain.text',
|
||||
'Please go to your Vega wallet application and approve or reject the transaction.'
|
||||
);
|
||||
});
|
||||
|
||||
it('must show error returned by wallet ', () => {
|
||||
// 0003-WTXN-009
|
||||
// 0003-WTXN-011
|
||||
// 0002-WCON-016
|
||||
// 0003-WTXN-008
|
||||
|
||||
//trigger error from the wallet
|
||||
cy.intercept('POST', 'http://localhost:1789/api/v2/requests', (req) => {
|
||||
req.on('response', (res) => {
|
||||
res.send({
|
||||
jsonrpc: '2.0',
|
||||
id: '1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const order: OrderSubmission = {
|
||||
marketId: 'market-0',
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
size: '100',
|
||||
};
|
||||
createOrder(order);
|
||||
cy.getByTestId('toast-content').should(
|
||||
'contain.text',
|
||||
'The connection to your Vega Wallet has been lost.'
|
||||
);
|
||||
cy.getByTestId('connect-vega-wallet').click();
|
||||
cy.getByTestId('dialog-content').should('be.visible');
|
||||
});
|
||||
|
||||
it('must see that the order was rejected by the connected wallet', () => {
|
||||
// 0003-WTXN-007
|
||||
|
||||
//trigger rejection error from the wallet
|
||||
cy.intercept('POST', 'http://localhost:1789/api/v2/requests', (req) => {
|
||||
req.alias = 'client.send_transaction';
|
||||
req.reply({
|
||||
statusCode: 400,
|
||||
body: {
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: 3001,
|
||||
data: 'the user rejected the wallet connection',
|
||||
message: 'User error',
|
||||
},
|
||||
id: '0',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const order: OrderSubmission = {
|
||||
marketId: 'market-0',
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
size: '100',
|
||||
};
|
||||
createOrder(order);
|
||||
cy.getByTestId('toast-content').should(
|
||||
'contain.text',
|
||||
'Error occurredthe user rejected the wallet connection'
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -26,18 +26,6 @@ describe('ethereum wallet', { tags: '@smoke', testIsolation: true }, () => {
|
||||
cy.getByTestId('tab-deposits').should('not.be.empty');
|
||||
});
|
||||
|
||||
it.skip('should see QR code modal for WalletConnect', () => {
|
||||
// 0004-EWAL-003
|
||||
|
||||
cy.getByTestId('Deposits').click();
|
||||
cy.getByTestId('deposit-button').click();
|
||||
cy.getByTestId('connect-eth-wallet-btn').click();
|
||||
cy.getByTestId('web3-connector-list').should('exist');
|
||||
cy.getByTestId('web3-connector-WalletConnect').click();
|
||||
// testing if exists rather than visible because of the long loading time
|
||||
cy.get('#w3m-modal').should('exist');
|
||||
});
|
||||
|
||||
it('able to disconnect eth wallet', () => {
|
||||
// 0004-EWAL-004
|
||||
// 0004-EWAL-005
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import {
|
||||
mockConnectWallet,
|
||||
mockConnectWalletWithUserError,
|
||||
} from '@vegaprotocol/cypress';
|
||||
|
||||
const connectVegaBtn = 'connect-vega-wallet';
|
||||
const manageVegaBtn = 'manage-vega-wallet';
|
||||
const dialogContent = 'dialog-content';
|
||||
|
||||
describe('connect vega wallet', { tags: '@smoke', testIsolation: true }, () => {
|
||||
beforeEach(() => {
|
||||
// Using portfolio page as it requires vega wallet connection
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.setOnBoardingViewed();
|
||||
cy.visit('/#/portfolio');
|
||||
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
|
||||
});
|
||||
|
||||
it('can connect', () => {
|
||||
// 0002-WCON-002
|
||||
// 0002-WCON-005
|
||||
// 0002-WCON-007
|
||||
// 0002-WCON-009
|
||||
|
||||
mockConnectWallet();
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-jsonRpc"]')
|
||||
.click();
|
||||
cy.wait('@walletReq');
|
||||
cy.getByTestId(dialogContent).should(
|
||||
'contain.text',
|
||||
'Approve the connection from your Vega wallet app.'
|
||||
);
|
||||
cy.getByTestId(dialogContent).should('not.exist');
|
||||
cy.getByTestId(manageVegaBtn).should('exist');
|
||||
});
|
||||
|
||||
it('can not connect', () => {
|
||||
// 0002-WCON-002
|
||||
// 0002-WCON-005
|
||||
// 0002-WCON-007
|
||||
// 0002-WCON-015
|
||||
|
||||
mockConnectWalletWithUserError();
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-jsonRpc"]')
|
||||
.click();
|
||||
cy.getByTestId('dialog-content')
|
||||
.should('contain.text', 'User error')
|
||||
.and('contain.text', 'the user rejected the wallet connection');
|
||||
});
|
||||
|
||||
it('can change selected public key and disconnect', () => {
|
||||
// 0002-WCON-022
|
||||
// 0002-WCON-023
|
||||
// 0002-WCON-025
|
||||
// 0002-WCON-026
|
||||
// 0002-WCON-021
|
||||
// 0002-WCON-027
|
||||
// 0002-WCON-030
|
||||
// 0002-WCON-029
|
||||
// 0002-WCON-008
|
||||
// 0002-WCON-035
|
||||
// 0002-WCON-014
|
||||
// 0002-WCON-010
|
||||
// 0003-WTXN-004
|
||||
|
||||
mockConnectWallet();
|
||||
const key2 = Cypress.env('VEGA_PUBLIC_KEY2');
|
||||
const truncatedKey2 = Cypress.env('TRUNCATED_VEGA_PUBLIC_KEY2');
|
||||
cy.connectVegaWallet();
|
||||
cy.getByTestId('manage-vega-wallet').click();
|
||||
cy.getByTestId('keypair-list').should('exist');
|
||||
cy.getByTestId(`key-${key2}`).should('contain.text', truncatedKey2);
|
||||
cy.getByTestId(`key-${key2}`)
|
||||
.find('[data-testid="copy-vega-public-key"]')
|
||||
.should('be.visible');
|
||||
cy.get(`[data-testid="key-${key2}"] > .mr-2`).click();
|
||||
cy.getByTestId('keypair-list')
|
||||
.find('[data-state="checked"]')
|
||||
.should('be.visible');
|
||||
cy.getByTestId('disconnect').click();
|
||||
cy.getByTestId('connect-vega-wallet').should('exist');
|
||||
cy.getByTestId('manage-vega-wallet').should('not.exist');
|
||||
cy.getByTestId('connect-vega-wallet').click();
|
||||
cy.contains('Enter a custom wallet location');
|
||||
});
|
||||
});
|
||||
@@ -28,3 +28,4 @@ NX_REFERRALS=false
|
||||
|
||||
NX_TENDERMINT_URL=https://be.vega.community
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||
NX_DISABLE_CLOSE_POSITION=true
|
||||
|
||||
@@ -1,34 +1,11 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
import { useTopTradedMarkets } from '../../lib/hooks/use-top-traded-markets';
|
||||
import { Links } from '../../lib/links';
|
||||
import { useNavigateToLastMarket } from '../../lib/hooks/use-navigate-to-last-market';
|
||||
|
||||
// The home pages only purpose is to redirect to the users last market,
|
||||
// the top traded if they are new, or fall back to the list of markets.
|
||||
// Thats why we just render a loader here
|
||||
export const Home = () => {
|
||||
const navigate = useNavigate();
|
||||
const { data } = useTopTradedMarkets();
|
||||
const marketId = useGlobalStore((store) => store.marketId);
|
||||
|
||||
useEffect(() => {
|
||||
if (marketId) {
|
||||
navigate(Links.MARKET(marketId), {
|
||||
replace: true,
|
||||
});
|
||||
} else if (data) {
|
||||
const marketDataId = data[0]?.id;
|
||||
if (marketDataId) {
|
||||
navigate(Links.MARKET(marketDataId), {
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
navigate(Links.MARKETS());
|
||||
}
|
||||
}
|
||||
}, [marketId, data, navigate]);
|
||||
useNavigateToLastMarket();
|
||||
|
||||
return (
|
||||
<Splash>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import { DocsLinks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { ButtonLink, ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { MarketProposalNotification } from '@vegaprotocol/proposals';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
@@ -145,7 +144,6 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
||||
/>
|
||||
</HeaderStat>
|
||||
)}
|
||||
<MarketProposalNotification marketId={market.id} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { act, render, screen, waitFor, within } from '@testing-library/react';
|
||||
// import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Closed } from './closed';
|
||||
import { MarketStateMapping, PropertyKeyType } from '@vegaprotocol/types';
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
marketsDataQuery,
|
||||
createMarketsDataFragment,
|
||||
} from '@vegaprotocol/mock';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('Closed', () => {
|
||||
let originalNow: typeof Date.now;
|
||||
@@ -168,14 +170,11 @@ describe('Closed', () => {
|
||||
Date.now = originalNow;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('renders correctly formatted and filtered rows', async () => {
|
||||
const renderComponent = async (mocks: MockedResponse[]) => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[marketsMock, marketsDataMock, oracleDataMock]}
|
||||
>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
@@ -185,6 +184,10 @@ describe('Closed', () => {
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
it('renders correct headers', async () => {
|
||||
await renderComponent([marketsMock, marketsDataMock, oracleDataMock]);
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
const expectedHeaders = [
|
||||
@@ -200,6 +203,10 @@ describe('Closed', () => {
|
||||
];
|
||||
expect(headers).toHaveLength(expectedHeaders.length);
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
|
||||
});
|
||||
|
||||
it('renders correctly formatted and filtered rows', async () => {
|
||||
await renderComponent([marketsMock, marketsDataMock, oracleDataMock]);
|
||||
|
||||
const assetSymbol = getAsset(market).symbol;
|
||||
|
||||
@@ -273,21 +280,8 @@ describe('Closed', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[mixedMarketsMock, marketsDataMock, oracleDataMock]}
|
||||
>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
<Closed />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
|
||||
await renderComponent([mixedMarketsMock, marketsDataMock, oracleDataMock]);
|
||||
|
||||
// check that the number of rows in datagrid is 2
|
||||
const container = within(
|
||||
@@ -319,8 +313,67 @@ describe('Closed', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('successor marked should be visible', async () => {
|
||||
it('display market actions', async () => {
|
||||
// Use market with a succcessor Id as the actions dropdown will optionally
|
||||
// show a link to the successor market
|
||||
const marketsWithSuccessorAndParent = [
|
||||
{
|
||||
__typename: 'MarketEdge' as const,
|
||||
node: createMarketFragment({
|
||||
id: 'include-0',
|
||||
state: MarketState.STATE_SETTLED,
|
||||
successorMarketID: 'successor',
|
||||
parentMarketID: 'parent',
|
||||
}),
|
||||
},
|
||||
];
|
||||
const mockWithSuccessorAndParent: MockedResponse<MarketsQuery> = {
|
||||
request: {
|
||||
query: MarketsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
__typename: 'MarketConnection',
|
||||
edges: marketsWithSuccessorAndParent,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await renderComponent([
|
||||
mockWithSuccessorAndParent,
|
||||
marketsDataMock,
|
||||
oracleDataMock,
|
||||
]);
|
||||
|
||||
const actionCell = screen
|
||||
.getAllByRole('gridcell')
|
||||
.find((el) => el.getAttribute('col-id') === 'market-actions');
|
||||
|
||||
await userEvent.click(
|
||||
within(actionCell as HTMLElement).getByTestId('dropdown-menu')
|
||||
);
|
||||
|
||||
expect(screen.getByRole('menu')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'Copy Market ID' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'View on Explorer' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'View settlement asset details' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'View parent market' })
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('menuitem', { name: 'View successor market' })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('successor market should be visible', async () => {
|
||||
const marketsWithSuccessorID = [
|
||||
{
|
||||
__typename: 'MarketEdge' as const,
|
||||
@@ -345,21 +398,11 @@ describe('Closed', () => {
|
||||
},
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[mockWithSuccessors, marketsDataMock, oracleDataMock]}
|
||||
>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
<Closed />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
await renderComponent([
|
||||
mockWithSuccessors,
|
||||
marketsDataMock,
|
||||
oracleDataMock,
|
||||
]);
|
||||
|
||||
const container = within(
|
||||
document.querySelector('.ag-center-cols-container') as HTMLElement
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { act, render, screen, within } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { OpenMarkets } from './open-markets';
|
||||
import { Interval } from '@vegaprotocol/types';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type {
|
||||
MarketsDataQuery,
|
||||
MarketsQuery,
|
||||
MarketCandlesQuery,
|
||||
MarketFieldsFragment,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
MarketsDataDocument,
|
||||
MarketsDocument,
|
||||
MarketsCandlesDocument,
|
||||
} from '@vegaprotocol/markets';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
marketsQuery,
|
||||
marketsDataQuery,
|
||||
marketsCandlesQuery,
|
||||
} from '@vegaprotocol/mock';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('Open', () => {
|
||||
let originalNow: typeof Date.now;
|
||||
const mockNowTimestamp = 1672531200000;
|
||||
const pubKey = 'pubKey';
|
||||
|
||||
const marketsQueryData = marketsQuery();
|
||||
const marketsMock: MockedResponse<MarketsQuery> = {
|
||||
request: {
|
||||
query: MarketsDocument,
|
||||
},
|
||||
result: {
|
||||
data: marketsQueryData,
|
||||
},
|
||||
};
|
||||
|
||||
const marketsCandlesQueryData = marketsCandlesQuery();
|
||||
const marketsCandlesMock: MockedResponse<MarketCandlesQuery> = {
|
||||
request: {
|
||||
query: MarketsCandlesDocument,
|
||||
variables: {
|
||||
interval: Interval.INTERVAL_I1H,
|
||||
since: '2022-12-31T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: marketsCandlesQueryData,
|
||||
},
|
||||
};
|
||||
|
||||
const marketsDataQueryData = marketsDataQuery();
|
||||
|
||||
const marketsDataMock: MockedResponse<MarketsDataQuery> = {
|
||||
request: {
|
||||
query: MarketsDataDocument,
|
||||
},
|
||||
result: {
|
||||
data: marketsDataQueryData,
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
originalNow = Date.now;
|
||||
Date.now = jest.fn().mockReturnValue(mockNowTimestamp);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Date.now = originalNow;
|
||||
});
|
||||
|
||||
const renderComponent = async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider
|
||||
mocks={[marketsMock, marketsCandlesMock, marketsDataMock]}
|
||||
>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ pubKey } as VegaWalletContextShape}
|
||||
>
|
||||
<OpenMarkets />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
it('renders correct headers', async () => {
|
||||
await renderComponent();
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
const expectedHeaders = [
|
||||
'Market',
|
||||
'Description',
|
||||
'Settlement asset',
|
||||
'Trading mode',
|
||||
'Status',
|
||||
'Mark price',
|
||||
'24h volume',
|
||||
'Open Interest',
|
||||
'Spread',
|
||||
'', // Action row
|
||||
];
|
||||
expect(headers).toHaveLength(expectedHeaders.length);
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
|
||||
});
|
||||
|
||||
it('sort columns', async () => {
|
||||
await renderComponent();
|
||||
|
||||
const headers = screen.getAllByRole('columnheader');
|
||||
const marketHeader = headers.find(
|
||||
(h) => h.getAttribute('col-id') === 'tradableInstrument.instrument.code'
|
||||
);
|
||||
if (!marketHeader) {
|
||||
throw new Error('No market header found');
|
||||
}
|
||||
expect(marketHeader).toHaveAttribute('aria-sort', 'none');
|
||||
await userEvent.click(within(marketHeader).getByText(/market/i));
|
||||
// 6001-MARK-064
|
||||
expect(marketHeader).toHaveAttribute('aria-sort', 'ascending');
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests, jest/expect-expect
|
||||
it('renders row', async () => {
|
||||
await renderComponent();
|
||||
|
||||
const container = within(
|
||||
document.querySelector('.ag-center-cols-container') as HTMLElement
|
||||
);
|
||||
|
||||
const markets = marketsQueryData.marketsConnection?.edges.map(
|
||||
(e) => e.node
|
||||
) as MarketFieldsFragment[];
|
||||
|
||||
const rows = container.getAllByRole('row');
|
||||
expect(rows).toHaveLength(markets.length);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import minBy from 'lodash/minBy';
|
||||
import { CodeTile, StatTile } from './tile';
|
||||
import {
|
||||
VegaIcon,
|
||||
@@ -28,7 +29,6 @@ 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';
|
||||
@@ -127,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
|
||||
|
||||
@@ -222,13 +222,13 @@ const TiersTable = ({
|
||||
...d,
|
||||
className: classNames({
|
||||
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
|
||||
d.tier === 1,
|
||||
d.tier >= 3,
|
||||
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
|
||||
d.tier === 2,
|
||||
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
|
||||
d.tier === 3,
|
||||
d.tier === 1,
|
||||
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
|
||||
d.tier > 3,
|
||||
d.tier == 0,
|
||||
}),
|
||||
}))}
|
||||
/>
|
||||
|
||||
@@ -16,18 +16,11 @@ query DiscountPrograms {
|
||||
}
|
||||
}
|
||||
|
||||
query Fees(
|
||||
$partyId: ID!
|
||||
$volumeDiscountEpochs: Int!
|
||||
$referralDiscountEpochs: Int!
|
||||
) {
|
||||
query Fees($partyId: ID!) {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(
|
||||
partyId: $partyId
|
||||
pagination: { last: $volumeDiscountEpochs }
|
||||
) {
|
||||
volumeDiscountStats(partyId: $partyId, pagination: { last: 1 }) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
@@ -59,10 +52,7 @@ query Fees(
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(
|
||||
partyId: $partyId
|
||||
pagination: { last: $referralDiscountEpochs }
|
||||
) {
|
||||
referralSetStats(partyId: $partyId, pagination: { last: 1 }) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
|
||||
+3
-10
@@ -10,8 +10,6 @@ export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgr
|
||||
|
||||
export type FeesQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
volumeDiscountEpochs: Types.Scalars['Int'];
|
||||
referralDiscountEpochs: Types.Scalars['Int'];
|
||||
}>;
|
||||
|
||||
|
||||
@@ -65,14 +63,11 @@ export type DiscountProgramsQueryHookResult = ReturnType<typeof useDiscountProgr
|
||||
export type DiscountProgramsLazyQueryHookResult = ReturnType<typeof useDiscountProgramsLazyQuery>;
|
||||
export type DiscountProgramsQueryResult = Apollo.QueryResult<DiscountProgramsQuery, DiscountProgramsQueryVariables>;
|
||||
export const FeesDocument = gql`
|
||||
query Fees($partyId: ID!, $volumeDiscountEpochs: Int!, $referralDiscountEpochs: Int!) {
|
||||
query Fees($partyId: ID!) {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(
|
||||
partyId: $partyId
|
||||
pagination: {last: $volumeDiscountEpochs}
|
||||
) {
|
||||
volumeDiscountStats(partyId: $partyId, pagination: {last: 1}) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
@@ -104,7 +99,7 @@ export const FeesDocument = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) {
|
||||
referralSetStats(partyId: $partyId, pagination: {last: 1}) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
@@ -129,8 +124,6 @@ export const FeesDocument = gql`
|
||||
* const { data, loading, error } = useFeesQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* volumeDiscountEpochs: // value for 'volumeDiscountEpochs'
|
||||
* referralDiscountEpochs: // value for 'referralDiscountEpochs'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -36,25 +36,25 @@ export const FeesContainer = () => {
|
||||
const { data: markets, loading: marketsLoading } = useMarketList();
|
||||
|
||||
const { data: programData, loading: programLoading } =
|
||||
useDiscountProgramsQuery();
|
||||
useDiscountProgramsQuery({ errorPolicy: 'ignore' });
|
||||
|
||||
const volumeDiscountWindowLength =
|
||||
programData?.currentVolumeDiscountProgram?.windowLength || 1;
|
||||
const referralDiscountWindowLength =
|
||||
programData?.currentReferralProgram?.windowLength || 1;
|
||||
|
||||
const { data: feesData, loading: feesLoading } = useFeesQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
volumeDiscountEpochs: volumeDiscountWindowLength,
|
||||
referralDiscountEpochs: referralDiscountWindowLength,
|
||||
},
|
||||
skip: !pubKey || !programData,
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
|
||||
|
||||
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
|
||||
useVolumeStats(
|
||||
feesData?.volumeDiscountStats,
|
||||
previousEpoch,
|
||||
feesData?.volumeDiscountStats.edges?.[0]?.node,
|
||||
programData?.currentVolumeDiscountProgram
|
||||
);
|
||||
|
||||
@@ -67,12 +67,12 @@ export const FeesContainer = () => {
|
||||
code,
|
||||
isReferrer,
|
||||
} = useReferralStats(
|
||||
feesData?.referralSetStats,
|
||||
feesData?.referralSetReferees,
|
||||
previousEpoch,
|
||||
feesData?.referralSetStats.edges?.[0]?.node,
|
||||
feesData?.referralSetReferees.edges?.[0]?.node,
|
||||
programData?.currentReferralProgram,
|
||||
feesData?.epoch,
|
||||
feesData?.referrer,
|
||||
feesData?.referee
|
||||
feesData?.referrer.edges?.[0]?.node,
|
||||
feesData?.referee.edges?.[0]?.node
|
||||
);
|
||||
|
||||
const loading = paramsLoading || feesLoading || programLoading;
|
||||
@@ -310,16 +310,25 @@ export const CurrentVolume = ({
|
||||
const t = useT();
|
||||
const nextTier = tiers[tierIndex + 1];
|
||||
const requiredForNextTier = nextTier
|
||||
? Number(nextTier.minimumRunningNotionalTakerVolume) - windowLengthVolume
|
||||
: 0;
|
||||
? new BigNumber(nextTier.minimumRunningNotionalTakerVolume).minus(
|
||||
windowLengthVolume
|
||||
)
|
||||
: new BigNumber(0);
|
||||
const currentVolume = new BigNumber(windowLengthVolume);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 pt-4">
|
||||
<CardStat
|
||||
value={formatNumberRounded(new BigNumber(windowLengthVolume))}
|
||||
text={t('pastEpochs', 'Past {{count}} epochs', { count: windowLength })}
|
||||
value={
|
||||
currentVolume.isZero()
|
||||
? `<${formatNumberRounded(requiredForNextTier)}`
|
||||
: formatNumberRounded(currentVolume)
|
||||
}
|
||||
text={t('pastEpochs', 'Past {{count}} epochs', {
|
||||
count: windowLength,
|
||||
})}
|
||||
/>
|
||||
{requiredForNextTier > 0 && (
|
||||
{requiredForNextTier.isGreaterThan(0) && (
|
||||
<CardStat
|
||||
value={formatNumber(requiredForNextTier)}
|
||||
text={t('Required for next tier')}
|
||||
@@ -465,25 +474,19 @@ const VolumeTiers = ({
|
||||
</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>
|
||||
@@ -526,37 +529,33 @@ const ReferralTiers = ({
|
||||
</tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers)
|
||||
.reverse()
|
||||
.map((t, i) => {
|
||||
const isUserTier = tiers.length - 1 - tierIndex === i;
|
||||
{Array.from(tiers).map((t, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
|
||||
const requiredVolume = Number(
|
||||
t.minimumRunningNotionalTakerVolume
|
||||
const requiredVolume = Number(t.minimumRunningNotionalTakerVolume);
|
||||
let unlocksIn = null;
|
||||
|
||||
if (
|
||||
referralVolumeInWindow >= requiredVolume &&
|
||||
epochsInSet < t.minimumEpochs
|
||||
) {
|
||||
unlocksIn = (
|
||||
<span className="text-muted">
|
||||
Unlocks in {t.minimumEpochs - epochsInSet} epochs
|
||||
</span>
|
||||
);
|
||||
let unlocksIn = null;
|
||||
}
|
||||
|
||||
if (
|
||||
referralVolumeInWindow >= requiredVolume &&
|
||||
epochsInSet < t.minimumEpochs
|
||||
) {
|
||||
unlocksIn = (
|
||||
<span className="text-muted">
|
||||
Unlocks in {t.minimumEpochs - epochsInSet} epochs
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
|
||||
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
|
||||
<Td>{t.minimumEpochs}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
|
||||
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
|
||||
<Td>{t.minimumEpochs}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@@ -2,46 +2,15 @@ import { renderHook } from '@testing-library/react';
|
||||
import { useReferralStats } from './use-referral-stats';
|
||||
|
||||
describe('useReferralStats', () => {
|
||||
const setStats = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 9,
|
||||
discountFactor: '0.2',
|
||||
referralSetRunningNotionalTakerVolume: '100',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: '200',
|
||||
},
|
||||
},
|
||||
],
|
||||
const stat = {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 9,
|
||||
discountFactor: '0.01',
|
||||
referralSetRunningNotionalTakerVolume: '100',
|
||||
};
|
||||
|
||||
const sets = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
atEpoch: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const epoch = {
|
||||
id: '10',
|
||||
const set = {
|
||||
atEpoch: 4,
|
||||
};
|
||||
|
||||
const program = {
|
||||
@@ -78,102 +47,36 @@ describe('useReferralStats', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns formatted data and tiers', () => {
|
||||
it('returns default values if set is not from previous epoch', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(setStats, sets, program, epoch)
|
||||
useReferralStats(10, stat, set, program)
|
||||
);
|
||||
|
||||
// should use stats from latest epoch
|
||||
const stats = setStats.edges[1].node;
|
||||
const set = sets.edges[1].node;
|
||||
|
||||
expect(result.current).toEqual({
|
||||
referralDiscount: Number(stats.discountFactor),
|
||||
referralVolumeInWindow: Number(
|
||||
stats.referralSetRunningNotionalTakerVolume
|
||||
),
|
||||
referralTierIndex: 1,
|
||||
referralDiscount: 0,
|
||||
referralVolumeInWindow: 0,
|
||||
referralTierIndex: -1,
|
||||
referralTiers: program.benefitTiers,
|
||||
epochsInSet: Number(epoch.id) - set.atEpoch,
|
||||
epochsInSet: 0,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ joinedAt: 2, index: -1 },
|
||||
{ joinedAt: 3, index: -1 },
|
||||
{ joinedAt: 4, index: 0 },
|
||||
{ joinedAt: 5, index: 0 },
|
||||
{ joinedAt: 6, index: 1 },
|
||||
{ joinedAt: 7, index: 1 },
|
||||
{ joinedAt: 8, index: 2 },
|
||||
{ joinedAt: 9, index: 2 },
|
||||
])('joined at epoch: $joinedAt should be index: $index', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: '100000',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const setsA = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: Number(epoch.id) - obj.joinedAt,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(statsA, setsA, program, epoch)
|
||||
useReferralStats(9, stat, set, program)
|
||||
);
|
||||
|
||||
expect(result.current.referralTierIndex).toEqual(obj.index);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ volume: '50', index: -1 },
|
||||
{ volume: '100', index: 0 },
|
||||
{ volume: '150', index: 0 },
|
||||
{ volume: '200', index: 1 },
|
||||
{ volume: '250', index: 1 },
|
||||
{ volume: '300', index: 2 },
|
||||
{ volume: '999', index: 2 },
|
||||
])('volume: $volume should be index: $index', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: obj.volume,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const setsA = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(statsA, setsA, program, epoch)
|
||||
);
|
||||
|
||||
expect(result.current.referralTierIndex).toEqual(obj.index);
|
||||
expect(result.current).toEqual({
|
||||
referralDiscount: Number(stat.discountFactor),
|
||||
referralVolumeInWindow: Number(
|
||||
stat.referralSetRunningNotionalTakerVolume
|
||||
),
|
||||
referralTierIndex: 0,
|
||||
referralTiers: program.benefitTiers,
|
||||
epochsInSet: stat.atEpoch - set.atEpoch,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import compact from 'lodash/compact';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { getReferralBenefitTier } from './utils';
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
import { first } from 'lodash';
|
||||
|
||||
export const useReferralStats = (
|
||||
setStats?: FeesQuery['referralSetStats'],
|
||||
setReferees?: FeesQuery['referralSetReferees'],
|
||||
previousEpoch?: number,
|
||||
referralStats?: NonNullable<
|
||||
FeesQuery['referralSetStats']['edges']['0']
|
||||
>['node'],
|
||||
setReferees?: NonNullable<
|
||||
FeesQuery['referralSetReferees']['edges']['0']
|
||||
>['node'],
|
||||
program?: DiscountProgramsQuery['currentReferralProgram'],
|
||||
epoch?: FeesQuery['epoch'],
|
||||
setIfReferrer?: FeesQuery['referrer'],
|
||||
setIfReferee?: FeesQuery['referee']
|
||||
setIfReferrer?: NonNullable<FeesQuery['referrer']['edges']['0']>['node'],
|
||||
setIfReferee?: NonNullable<FeesQuery['referee']['edges']['0']>['node']
|
||||
) => {
|
||||
const referralTiers = program?.benefitTiers || [];
|
||||
|
||||
if (!setStats || !setReferees || !program || !epoch) {
|
||||
if (
|
||||
!previousEpoch ||
|
||||
referralStats?.atEpoch !== previousEpoch ||
|
||||
!program ||
|
||||
!setReferees
|
||||
) {
|
||||
return {
|
||||
referralDiscount: 0,
|
||||
referralVolumeInWindow: 0,
|
||||
@@ -26,41 +30,22 @@ export const useReferralStats = (
|
||||
};
|
||||
}
|
||||
|
||||
const setIfReferrerData = first(
|
||||
compact(setIfReferrer?.edges).map((e) => e.node)
|
||||
);
|
||||
const setIfRefereeData = first(
|
||||
compact(setIfReferee?.edges).map((e) => e.node)
|
||||
);
|
||||
|
||||
const referralSetsStats = compact(setStats.edges).map((e) => e.node);
|
||||
const referralSets = compact(setReferees.edges).map((e) => e.node);
|
||||
|
||||
const referralSet = maxBy(referralSets, (s) => s.atEpoch);
|
||||
const referralStats = maxBy(referralSetsStats, (s) => s.atEpoch);
|
||||
|
||||
const epochsInSet = referralSet ? Number(epoch.id) - referralSet.atEpoch : 0;
|
||||
|
||||
const referralDiscount = Number(referralStats?.discountFactor || 0);
|
||||
const referralVolumeInWindow = Number(
|
||||
referralStats?.referralSetRunningNotionalTakerVolume || 0
|
||||
);
|
||||
|
||||
const referralTierIndex = referralStats
|
||||
? getReferralBenefitTier(
|
||||
epochsInSet,
|
||||
Number(referralStats.referralSetRunningNotionalTakerVolume),
|
||||
referralTiers
|
||||
)
|
||||
: -1;
|
||||
const referralTierIndex = referralTiers.findIndex(
|
||||
(tier) => tier.referralDiscountFactor === referralStats?.discountFactor
|
||||
);
|
||||
|
||||
return {
|
||||
referralDiscount,
|
||||
referralVolumeInWindow,
|
||||
referralTierIndex,
|
||||
referralTiers,
|
||||
epochsInSet,
|
||||
code: (setIfReferrerData || setIfRefereeData)?.id,
|
||||
isReferrer: Boolean(setIfReferrerData),
|
||||
epochsInSet: referralStats.atEpoch - setReferees.atEpoch,
|
||||
code: (setIfReferrer || setIfReferee)?.id,
|
||||
isReferrer: Boolean(setIfReferrer),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,27 +2,11 @@ import { renderHook } from '@testing-library/react';
|
||||
import { useVolumeStats } from './use-volume-stats';
|
||||
|
||||
describe('useReferralStats', () => {
|
||||
const statsList = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 9,
|
||||
discountFactor: '0.1',
|
||||
runningVolume: '100',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
runningVolume: '200',
|
||||
},
|
||||
},
|
||||
],
|
||||
const stats = {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.05',
|
||||
runningVolume: '200',
|
||||
};
|
||||
|
||||
const program = {
|
||||
@@ -44,7 +28,7 @@ describe('useReferralStats', () => {
|
||||
};
|
||||
|
||||
it('returns correct default values', () => {
|
||||
const { result } = renderHook(() => useVolumeStats());
|
||||
const { result } = renderHook(() => useVolumeStats(10));
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: 0,
|
||||
volumeInWindow: 0,
|
||||
@@ -53,11 +37,18 @@ describe('useReferralStats', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(statsList, program));
|
||||
it('returns default values if no stat is not from previous epoch', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(11, stats, program));
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: 0,
|
||||
volumeInWindow: 0,
|
||||
volumeTierIndex: -1,
|
||||
volumeTiers: program.benefitTiers,
|
||||
});
|
||||
});
|
||||
|
||||
// should use stats from latest epoch
|
||||
const stats = statsList.edges[1].node;
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(10, stats, program));
|
||||
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: Number(stats.discountFactor),
|
||||
@@ -66,30 +57,4 @@ describe('useReferralStats', () => {
|
||||
volumeTiers: program.benefitTiers,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ volume: '100', index: 0 },
|
||||
{ volume: '150', index: 0 },
|
||||
{ volume: '200', index: 1 },
|
||||
{ volume: '250', index: 1 },
|
||||
{ volume: '300', index: 2 },
|
||||
{ volume: '350', index: 2 },
|
||||
])('returns index: $index for the running volume: $volume', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
runningVolume: obj.volume,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useVolumeStats(statsA, program));
|
||||
expect(result.current.volumeTierIndex).toBe(obj.index);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import compact from 'lodash/compact';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { getVolumeTier } from './utils';
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
|
||||
export const useVolumeStats = (
|
||||
stats?: FeesQuery['volumeDiscountStats'],
|
||||
previousEpoch: number,
|
||||
lastEpochStats?: NonNullable<
|
||||
FeesQuery['volumeDiscountStats']['edges']['0']
|
||||
>['node'],
|
||||
program?: DiscountProgramsQuery['currentVolumeDiscountProgram']
|
||||
) => {
|
||||
const volumeTiers = program?.benefitTiers || [];
|
||||
|
||||
if (!stats || !program) {
|
||||
if (!lastEpochStats || lastEpochStats.atEpoch !== previousEpoch || !program) {
|
||||
return {
|
||||
volumeDiscount: 0,
|
||||
volumeTierIndex: -1,
|
||||
@@ -18,11 +18,11 @@ export const useVolumeStats = (
|
||||
};
|
||||
}
|
||||
|
||||
const volumeStats = compact(stats.edges).map((e) => e.node);
|
||||
const lastEpochStats = maxBy(volumeStats, (s) => s.atEpoch);
|
||||
const volumeDiscount = Number(lastEpochStats?.discountFactor || 0);
|
||||
const volumeInWindow = Number(lastEpochStats?.runningVolume || 0);
|
||||
const volumeTierIndex = getVolumeTier(volumeInWindow, volumeTiers);
|
||||
const volumeTierIndex = volumeTiers.findIndex(
|
||||
(tier) => tier.volumeDiscountFactor === lastEpochStats?.discountFactor
|
||||
);
|
||||
|
||||
return {
|
||||
volumeDiscount,
|
||||
|
||||
@@ -20,73 +20,6 @@ export const formatPercentage = (num: number) => {
|
||||
return formatter.format(parseFloat(pct.toFixed(5)));
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the index of the benefit tier for volume discounts. A user
|
||||
* only needs to fulfill a minimum volume requirement for the tier
|
||||
*/
|
||||
export const getVolumeTier = (
|
||||
volume: number,
|
||||
tiers: Array<{
|
||||
minimumRunningNotionalTakerVolume: string;
|
||||
}>
|
||||
) => {
|
||||
return tiers.findIndex((tier, i) => {
|
||||
const nextTier = tiers[i + 1];
|
||||
const validVolume =
|
||||
volume >= Number(tier.minimumRunningNotionalTakerVolume);
|
||||
|
||||
if (nextTier) {
|
||||
return (
|
||||
validVolume &&
|
||||
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
|
||||
);
|
||||
}
|
||||
|
||||
return validVolume;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the index of the benefit tiers for referrals. A user must
|
||||
* fulfill both the minimum epochs in the referral set, and the set
|
||||
* must reach the combined total volume
|
||||
*/
|
||||
export const getReferralBenefitTier = (
|
||||
epochsInSet: number,
|
||||
volume: number,
|
||||
tiers: Array<{
|
||||
minimumRunningNotionalTakerVolume: string;
|
||||
minimumEpochs: number;
|
||||
}>
|
||||
) => {
|
||||
const indexByEpoch = tiers.findIndex((tier, i) => {
|
||||
const nextTier = tiers[i + 1];
|
||||
const validEpochs = epochsInSet >= tier.minimumEpochs;
|
||||
|
||||
if (nextTier) {
|
||||
return validEpochs && epochsInSet < nextTier.minimumEpochs;
|
||||
}
|
||||
|
||||
return validEpochs;
|
||||
});
|
||||
const indexByVolume = tiers.findIndex((tier, i) => {
|
||||
const nextTier = tiers[i + 1];
|
||||
const validVolume =
|
||||
volume >= Number(tier.minimumRunningNotionalTakerVolume);
|
||||
|
||||
if (nextTier) {
|
||||
return (
|
||||
validVolume &&
|
||||
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
|
||||
);
|
||||
}
|
||||
|
||||
return validVolume;
|
||||
});
|
||||
|
||||
return Math.min(indexByEpoch, indexByVolume);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a set of fees and a set of discounts return
|
||||
* the adjusted fee factor
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import uniq from 'lodash/uniq';
|
||||
import type { Account } from '@vegaprotocol/accounts';
|
||||
import { useAccounts } from '@vegaprotocol/accounts';
|
||||
import {
|
||||
@@ -31,6 +32,12 @@ import { ViewType, useSidebar } from '../sidebar';
|
||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
import { RewardsHistoryContainer } from './rewards-history';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
||||
|
||||
const ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA = [
|
||||
'bf1e88d19db4b3ca0d1d5bdb73718a01686b18cf731ca26adedf3c8b83802bba', // USDT mainnet
|
||||
'8ba0b10971f0c4747746cd01ff05a53ae75ca91eba1d4d050b527910c983e27e', // USDT testnet
|
||||
];
|
||||
|
||||
export const RewardsContainer = () => {
|
||||
const t = useT();
|
||||
@@ -40,34 +47,67 @@ export const RewardsContainer = () => {
|
||||
NetworkParams.rewards_activityStreak_benefitTiers,
|
||||
NetworkParams.rewards_vesting_baseRate,
|
||||
]);
|
||||
|
||||
const { data: accounts, loading: accountsLoading } = useAccounts(pubKey);
|
||||
|
||||
const { data: assetMap } = useAssetsMapProvider();
|
||||
|
||||
const { data: epochData } = useRewardsEpochQuery();
|
||||
|
||||
// No need to specify the fromEpoch as it will by default give you the last
|
||||
// Note activityStreak in query will fail
|
||||
const { data: rewardsData, loading: rewardsLoading } = useRewardsPageQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
},
|
||||
// Inclusion of activity streak in query currently fails
|
||||
errorPolicy: 'ignore',
|
||||
});
|
||||
|
||||
if (!epochData?.epoch) return null;
|
||||
if (!epochData?.epoch || !assetMap) return null;
|
||||
|
||||
const loading = paramsLoading || accountsLoading || rewardsLoading;
|
||||
|
||||
const rewardAccounts = accounts
|
||||
? accounts.filter((a) =>
|
||||
[
|
||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
AccountType.ACCOUNT_TYPE_VESTING_REWARDS,
|
||||
].includes(a.type)
|
||||
)
|
||||
? accounts
|
||||
.filter((a) =>
|
||||
[
|
||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
AccountType.ACCOUNT_TYPE_VESTING_REWARDS,
|
||||
].includes(a.type)
|
||||
)
|
||||
.filter((a) => new BigNumber(a.balance).isGreaterThan(0))
|
||||
: [];
|
||||
|
||||
const rewardAssetsMap = groupBy(
|
||||
rewardAccounts.filter((a) => a.asset.id !== params.reward_asset),
|
||||
'asset.id'
|
||||
);
|
||||
const rewardAccountsAssetMap = groupBy(rewardAccounts, 'asset.id');
|
||||
|
||||
const lockedBalances = rewardsData?.party?.vestingBalancesSummary
|
||||
.lockedBalances
|
||||
? rewardsData.party.vestingBalancesSummary.lockedBalances.filter((b) =>
|
||||
new BigNumber(b.balance).isGreaterThan(0)
|
||||
)
|
||||
: [];
|
||||
const lockedAssetMap = groupBy(lockedBalances, 'asset.id');
|
||||
|
||||
const vestingBalances = rewardsData?.party?.vestingBalancesSummary
|
||||
.vestingBalances
|
||||
? rewardsData.party.vestingBalancesSummary.vestingBalances.filter((b) =>
|
||||
new BigNumber(b.balance).isGreaterThan(0)
|
||||
)
|
||||
: [];
|
||||
const vestingAssetMap = groupBy(vestingBalances, 'asset.id');
|
||||
|
||||
// each asset reward pot is made up of:
|
||||
// available to withdraw - ACCOUNT_TYPE_VESTED_REWARDS
|
||||
// vesting - vestingBalancesSummary.vestingBalances
|
||||
// locked - vestingBalancesSummary.lockedBalances
|
||||
//
|
||||
// there can be entires for the same asset in each list so we need a uniq list of assets
|
||||
const assets = uniq([
|
||||
...Object.keys(rewardAccountsAssetMap),
|
||||
...Object.keys(lockedAssetMap),
|
||||
...Object.keys(vestingAssetMap),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="grid auto-rows-min grid-cols-6 gap-3">
|
||||
@@ -117,28 +157,72 @@ export const RewardsContainer = () => {
|
||||
</Card>
|
||||
|
||||
{/* Show all other reward pots, most of the time users will not have other rewards */}
|
||||
{Object.keys(rewardAssetsMap).map((assetId) => {
|
||||
const asset = rewardAssetsMap[assetId][0].asset;
|
||||
return (
|
||||
<Card
|
||||
key={assetId}
|
||||
title={t('{{assetSymbol}} Reward pot', {
|
||||
assetSymbol: asset.symbol,
|
||||
})}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={assetId}
|
||||
vestingBalancesSummary={
|
||||
rewardsData?.party?.vestingBalancesSummary
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
{assets
|
||||
.filter((assetId) => assetId !== params.reward_asset)
|
||||
.map((assetId) => {
|
||||
const asset = assetMap ? assetMap[assetId] : null;
|
||||
|
||||
if (!asset) return null;
|
||||
|
||||
// Following code is for mitigating an issue due to a core bug where locked and vesting
|
||||
// balances were incorrectly increased for infrastructure rewards for USDT on mainnet
|
||||
//
|
||||
// We don't want to incorrectly show the wring locked/vesting values, but we DO want to
|
||||
// show the user that they have rewards available to withdraw
|
||||
if (ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA.includes(asset.id)) {
|
||||
const accountsForAsset = rewardAccountsAssetMap[asset.id];
|
||||
const vestedAccount = accountsForAsset?.find(
|
||||
(a) => a.type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||
);
|
||||
|
||||
// No vested rewards available to withdraw, so skip over USDT
|
||||
if (!vestedAccount || Number(vestedAccount.balance) <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={assetId}
|
||||
title={t('{{assetSymbol}} Reward pot', {
|
||||
assetSymbol: asset.symbol,
|
||||
})}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={assetId}
|
||||
// Ensure that these values are shown as 0
|
||||
vestingBalancesSummary={{
|
||||
lockedBalances: [],
|
||||
vestingBalances: [],
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={assetId}
|
||||
title={t('{{assetSymbol}} Reward pot', {
|
||||
assetSymbol: asset.symbol,
|
||||
})}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={assetId}
|
||||
vestingBalancesSummary={
|
||||
rewardsData?.party?.vestingBalancesSummary
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
<Card
|
||||
title={t('Rewards history')}
|
||||
className="lg:col-span-full"
|
||||
@@ -147,6 +231,7 @@ export const RewardsContainer = () => {
|
||||
<RewardsHistoryContainer
|
||||
epoch={Number(epochData?.epoch.id)}
|
||||
pubKey={pubKey}
|
||||
assets={assetMap}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -313,14 +398,14 @@ export const RewardPot = ({
|
||||
export const Vesting = ({
|
||||
pubKey,
|
||||
baseRate,
|
||||
multiplier = '1',
|
||||
multiplier,
|
||||
}: {
|
||||
pubKey: string | null;
|
||||
baseRate: string;
|
||||
multiplier?: string;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const rate = new BigNumber(baseRate).times(multiplier);
|
||||
const rate = new BigNumber(baseRate).times(multiplier || 1);
|
||||
const rateFormatted = formatPercentage(Number(rate));
|
||||
const baseRateFormatted = formatPercentage(Number(baseRate));
|
||||
|
||||
@@ -335,7 +420,7 @@ export const Vesting = ({
|
||||
{pubKey && (
|
||||
<tr>
|
||||
<CardTableTH>{t('Vesting multiplier')}</CardTableTH>
|
||||
<CardTableTD>{multiplier}x</CardTableTD>
|
||||
<CardTableTD>{multiplier ? `${multiplier}x` : '-'}</CardTableTD>
|
||||
</tr>
|
||||
)}
|
||||
</CardTable>
|
||||
@@ -345,16 +430,16 @@ export const Vesting = ({
|
||||
|
||||
export const Multipliers = ({
|
||||
pubKey,
|
||||
streakMultiplier = '1',
|
||||
hoarderMultiplier = '1',
|
||||
streakMultiplier,
|
||||
hoarderMultiplier,
|
||||
}: {
|
||||
pubKey: string | null;
|
||||
streakMultiplier?: string;
|
||||
hoarderMultiplier?: string;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const combinedMultiplier = new BigNumber(streakMultiplier).times(
|
||||
hoarderMultiplier
|
||||
const combinedMultiplier = new BigNumber(streakMultiplier || 1).times(
|
||||
hoarderMultiplier || 1
|
||||
);
|
||||
|
||||
if (!pubKey) {
|
||||
@@ -375,11 +460,15 @@ export const Multipliers = ({
|
||||
<CardTable>
|
||||
<tr>
|
||||
<CardTableTH>{t('Streak reward multiplier')}</CardTableTH>
|
||||
<CardTableTD>{streakMultiplier}x</CardTableTD>
|
||||
<CardTableTD>
|
||||
{streakMultiplier ? `${streakMultiplier}x` : '-'}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<CardTableTH>{t('Hoarder reward multiplier')}</CardTableTH>
|
||||
<CardTableTD>{hoarderMultiplier}x</CardTableTD>
|
||||
<CardTableTD>
|
||||
{hoarderMultiplier ? `${hoarderMultiplier}x` : '-'}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
</CardTable>
|
||||
</div>
|
||||
|
||||
@@ -61,6 +61,14 @@ const rewardSummaries = [
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
epoch: 7,
|
||||
assetId: assets.asset2.id,
|
||||
amount: '300',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getCell = (cells: HTMLElement[], colId: string) => {
|
||||
@@ -69,7 +77,7 @@ const getCell = (cells: HTMLElement[], colId: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
describe('RewarsHistoryTable', () => {
|
||||
describe('RewardsHistoryTable', () => {
|
||||
const props = {
|
||||
epochRewardSummaries: {
|
||||
edges: rewardSummaries,
|
||||
@@ -88,7 +96,7 @@ describe('RewarsHistoryTable', () => {
|
||||
loading: false,
|
||||
};
|
||||
|
||||
it('Renders table with accounts summed up by asset', () => {
|
||||
it('renders table with accounts summed up by asset', () => {
|
||||
render(<RewardHistoryTable {...props} />);
|
||||
|
||||
const container = within(
|
||||
@@ -110,17 +118,27 @@ describe('RewarsHistoryTable', () => {
|
||||
assets.asset2.name
|
||||
);
|
||||
|
||||
// First row
|
||||
const marketCreationCell = getCell(cells, 'marketCreation');
|
||||
expect(
|
||||
marketCreationCell.getByTestId('stack-cell-primary')
|
||||
).toHaveTextContent('300');
|
||||
expect(
|
||||
marketCreationCell.getByTestId('stack-cell-secondary')
|
||||
).toHaveTextContent('100.00%');
|
||||
).toHaveTextContent('50.00%');
|
||||
|
||||
const infrastructureFeesCell = getCell(cells, 'infrastructureFees');
|
||||
expect(
|
||||
infrastructureFeesCell.getByTestId('stack-cell-primary')
|
||||
).toHaveTextContent('300');
|
||||
expect(
|
||||
infrastructureFeesCell.getByTestId('stack-cell-secondary')
|
||||
).toHaveTextContent('50.00%');
|
||||
|
||||
let totalCell = getCell(cells, 'total');
|
||||
expect(totalCell.getByText('300.00')).toBeInTheDocument();
|
||||
expect(totalCell.getByText('600.00')).toBeInTheDocument();
|
||||
|
||||
// Second row
|
||||
row = within(rows[1]);
|
||||
cells = row.getAllByRole('gridcell');
|
||||
|
||||
|
||||
@@ -2,10 +2,7 @@ import debounce from 'lodash/debounce';
|
||||
import { useMemo, useState } from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ColDef, ValueFormatterFunc } from 'ag-grid-community';
|
||||
import {
|
||||
useAssetsMapProvider,
|
||||
type AssetFieldsFragment,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { type AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import {
|
||||
addDecimalsFormatNumberQuantum,
|
||||
formatNumberPercentage,
|
||||
@@ -26,17 +23,17 @@ import { useT } from '../../lib/use-t';
|
||||
export const RewardsHistoryContainer = ({
|
||||
epoch,
|
||||
pubKey,
|
||||
assets,
|
||||
}: {
|
||||
pubKey: string | null;
|
||||
epoch: number;
|
||||
assets: Record<string, AssetFieldsFragment>;
|
||||
}) => {
|
||||
const [epochVariables, setEpochVariables] = useState(() => ({
|
||||
from: epoch - 1,
|
||||
to: epoch,
|
||||
}));
|
||||
|
||||
const { data: assets } = useAssetsMapProvider();
|
||||
|
||||
// No need to specify the fromEpoch as it will by default give you the last
|
||||
const { refetch, data, loading } = useRewardsHistoryQuery({
|
||||
variables: {
|
||||
@@ -154,10 +151,12 @@ export const RewardHistoryTable = ({
|
||||
const rewardValueFormatter: ValueFormatterFunc<RewardRow> = ({
|
||||
data,
|
||||
value,
|
||||
...rest
|
||||
}) => {
|
||||
if (!value || !data) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return addDecimalsFormatNumberQuantum(
|
||||
value,
|
||||
data.asset.decimals,
|
||||
@@ -197,6 +196,11 @@ export const RewardHistoryTable = ({
|
||||
},
|
||||
sort: 'desc',
|
||||
},
|
||||
{
|
||||
field: 'infrastructureFees',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'staking',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { type AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import { getRewards } from './use-reward-row-data';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const asset1 = {
|
||||
id: 'asset1',
|
||||
name: 'USD (KRW)',
|
||||
symbol: 'USD-KRW',
|
||||
decimals: 6,
|
||||
quantum: '1000000',
|
||||
status: Schema.AssetStatus.STATUS_ENABLED,
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
} as AssetFieldsFragment;
|
||||
|
||||
const asset2 = {
|
||||
id: 'asset2',
|
||||
name: 'tDAI TEST',
|
||||
symbol: 'tDAI',
|
||||
decimals: 5,
|
||||
quantum: '1',
|
||||
status: Schema.AssetStatus.STATUS_ENABLED,
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
} as AssetFieldsFragment;
|
||||
|
||||
const asset3 = {
|
||||
id: 'asset3',
|
||||
name: 'Tether USD',
|
||||
symbol: 'USDT',
|
||||
decimals: 6,
|
||||
quantum: '1000000',
|
||||
status: Schema.AssetStatus.STATUS_ENABLED,
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
} as AssetFieldsFragment;
|
||||
|
||||
const asset4 = {
|
||||
id: 'asset4',
|
||||
name: 'USDT-T',
|
||||
symbol: 'USDT-T',
|
||||
decimals: 18,
|
||||
quantum: '1',
|
||||
status: Schema.AssetStatus.STATUS_ENABLED,
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
} as AssetFieldsFragment;
|
||||
|
||||
const assets: Record<string, AssetFieldsFragment> = {
|
||||
asset1,
|
||||
asset2,
|
||||
asset3,
|
||||
asset4,
|
||||
};
|
||||
|
||||
const testData = {
|
||||
rewards: [
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
assetId: 'asset1',
|
||||
amount: '31897424',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
assetId: 'asset2',
|
||||
amount: '57',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
assetId: 'asset3',
|
||||
amount: '5501',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
|
||||
assetId: 'asset3',
|
||||
amount: '5501',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
|
||||
assetId: 'asset4',
|
||||
amount: '5501',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
assetId: 'asset4',
|
||||
amount: '456',
|
||||
},
|
||||
{
|
||||
rewardType: Schema.AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING,
|
||||
assetId: 'asset4',
|
||||
amount: '4565',
|
||||
},
|
||||
],
|
||||
assets,
|
||||
};
|
||||
|
||||
describe('getRewards', () => {
|
||||
it('should return the correct rewards when infra fees are included', () => {
|
||||
const rewards = getRewards(testData.rewards, testData.assets);
|
||||
expect(rewards).toEqual([
|
||||
{
|
||||
asset: asset1,
|
||||
infrastructureFees: 31897424,
|
||||
staking: 0,
|
||||
priceTaking: 0,
|
||||
priceMaking: 0,
|
||||
liquidityProvision: 0,
|
||||
marketCreation: 0,
|
||||
averagePosition: 0,
|
||||
relativeReturns: 0,
|
||||
returnsVolatility: 0,
|
||||
validatorRanking: 0,
|
||||
total: 31897424,
|
||||
},
|
||||
{
|
||||
asset: asset2,
|
||||
infrastructureFees: 57,
|
||||
staking: 0,
|
||||
priceTaking: 0,
|
||||
priceMaking: 0,
|
||||
liquidityProvision: 0,
|
||||
marketCreation: 0,
|
||||
averagePosition: 0,
|
||||
relativeReturns: 0,
|
||||
returnsVolatility: 0,
|
||||
validatorRanking: 0,
|
||||
total: 57,
|
||||
},
|
||||
{
|
||||
asset: asset3,
|
||||
infrastructureFees: 5501,
|
||||
staking: 0,
|
||||
priceTaking: 0,
|
||||
priceMaking: 0,
|
||||
liquidityProvision: 0,
|
||||
marketCreation: 0,
|
||||
averagePosition: 5501,
|
||||
relativeReturns: 0,
|
||||
returnsVolatility: 0,
|
||||
validatorRanking: 0,
|
||||
total: 11002,
|
||||
},
|
||||
{
|
||||
asset: asset4,
|
||||
infrastructureFees: 0,
|
||||
staking: 0,
|
||||
priceTaking: 0,
|
||||
priceMaking: 5501,
|
||||
liquidityProvision: 456,
|
||||
marketCreation: 0,
|
||||
averagePosition: 0,
|
||||
relativeReturns: 0,
|
||||
returnsVolatility: 0,
|
||||
validatorRanking: 4565,
|
||||
total: 10522,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -16,9 +16,10 @@ const REWARD_ACCOUNT_TYPES = [
|
||||
AccountType.ACCOUNT_TYPE_REWARD_RELATIVE_RETURN,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING,
|
||||
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
];
|
||||
|
||||
const getRewards = (
|
||||
export const getRewards = (
|
||||
rewards: Array<{
|
||||
rewardType: AccountType;
|
||||
assetId: string;
|
||||
@@ -56,6 +57,9 @@ const getRewards = (
|
||||
|
||||
return {
|
||||
asset,
|
||||
infrastructureFees: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE
|
||||
),
|
||||
staking: totals.get(AccountType.ACCOUNT_TYPE_GLOBAL_REWARD),
|
||||
priceTaking: totals.get(AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES),
|
||||
priceMaking: totals.get(
|
||||
@@ -101,7 +105,8 @@ export const useRewardsRowData = ({
|
||||
assetId: r.asset.id,
|
||||
amount: r.amount,
|
||||
}));
|
||||
return getRewards(rewards, assets);
|
||||
const result = getRewards(rewards, assets);
|
||||
return result;
|
||||
}
|
||||
|
||||
const rewards = removePaginationWrapper(epochRewardSummaries?.edges);
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
|
||||
VEGA_VERSION=v0.73.5
|
||||
VEGA_VERSION=v0.73.6
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:develop
|
||||
VEGA_VERSION=v0.73.5
|
||||
VEGA_VERSION=v0.73.6
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:main
|
||||
VEGA_VERSION=v0.73.5
|
||||
VEGA_VERSION=v0.73.6
|
||||
|
||||
@@ -12,6 +12,7 @@ from contextlib import contextmanager
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from playwright.sync_api import Browser, Page
|
||||
from config import console_image_name, vega_version
|
||||
from datetime import datetime, timedelta
|
||||
from fixtures.market import (
|
||||
setup_simple_market,
|
||||
setup_opening_auction_market,
|
||||
@@ -78,6 +79,7 @@ def init_vega(request=None):
|
||||
store_transactions=True,
|
||||
transactions_per_block=1000,
|
||||
seconds_per_block=seconds_per_block,
|
||||
genesis_time= datetime.now() - timedelta(days=1),
|
||||
) as vega:
|
||||
try:
|
||||
container = docker_client.containers.run(
|
||||
|
||||
Generated
+1
-1
@@ -1161,7 +1161,7 @@ profile = ["pytest-profiling", "snakeviz"]
|
||||
type = "git"
|
||||
url = "https://github.com/vegaprotocol/vega-market-sim.git"
|
||||
reference = "HEAD"
|
||||
resolved_reference = "e93f7dfa8463c59cfd0e299362b845511cebeef6"
|
||||
resolved_reference = "fbcb974b2055bbc80169cdfd69987f087f9969fb"
|
||||
|
||||
[[package]]
|
||||
name = "websocket-client"
|
||||
|
||||
@@ -9,7 +9,7 @@ packages = [{include = "trading market-sim e2e"}]
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9,<3.11"
|
||||
psutil = "^5.9.5"
|
||||
vega-sim = {git = "https://github.com/vegaprotocol/vega-market-sim.git"}
|
||||
vega-sim = {git = "https://github.com/vegaprotocol/vega-market-sim.git/", branch = "fix/genesis_panic"}
|
||||
pytest-playwright = "^0.4.2"
|
||||
docker = "^6.1.3"
|
||||
pytest-xdist = "^3.3.1"
|
||||
|
||||
@@ -58,7 +58,6 @@ class TestSettledMarket:
|
||||
def test_settled_rows(self, page: Page, create_settled_market):
|
||||
page.goto(f"/#/markets/all")
|
||||
page.get_by_test_id("Closed markets").click()
|
||||
|
||||
row_selector = page.locator(
|
||||
'[data-testid="tab-closed-markets"] .ag-center-cols-container .ag-row'
|
||||
).first
|
||||
@@ -72,7 +71,7 @@ class TestSettledMarket:
|
||||
# 6001-MARK-009
|
||||
# 6001-MARK-008
|
||||
# 6001-MARK-010
|
||||
pattern = r"(\d+)\s+months\s+ago"
|
||||
pattern = r"(\d+)\s+(months|hours|days)\s+ago"
|
||||
date_text = row_selector.locator('[col-id="settlementDate"]').inner_text()
|
||||
assert re.match(pattern, date_text), f"Expected text to match pattern but got {date_text}"
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.service import VegaService
|
||||
from actions.vega import submit_order
|
||||
from actions.utils import change_keys
|
||||
from wallet_config import MM_WALLET, MM_WALLET2
|
||||
import logging
|
||||
|
||||
@@ -196,3 +197,17 @@ def test_price_monitoring(simple_market, vega: VegaService, page: Page):
|
||||
expect(
|
||||
page.get_by_test_id(liquidity_supplied).get_by_test_id(item_value)
|
||||
).to_have_text("50.00 (>100%)")
|
||||
|
||||
COL_ID_FEE = ".ag-center-cols-container [col-id='fee'] .ag-cell-value"
|
||||
|
||||
@pytest.mark.usefixtures("vega", "page", "continuous_market", "risk_accepted", "auth")
|
||||
def test_auction_uncross_fees(continuous_market, vega: VegaService, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id("Fills").click()
|
||||
expect(page.locator(COL_ID_FEE)).to_have_text("0.00 tDAI")
|
||||
page.locator(COL_ID_FEE).hover()
|
||||
expect(page.get_by_test_id("fee-breakdown-tooltip")).to_have_text("If the market was suspendedIf the market is in monitoring auction, half of the infrastructure and liquidity fees will be paid.Infrastructure fee0.00 tDAILiquidity fee0.00 tDAIMaker fee0.00 tDAITotal fees0.00 tDAI")
|
||||
change_keys(page,vega, "market_maker")
|
||||
expect(page.locator(COL_ID_FEE)).to_have_text("0.00 tDAI")
|
||||
page.locator(COL_ID_FEE).hover()
|
||||
expect(page.get_by_test_id("fee-breakdown-tooltip")).to_have_text("If the market was suspendedIf the market is in monitoring auction, half of the infrastructure and liquidity fees will be paid.Infrastructure fee0.00 tDAILiquidity fee0.00 tDAIMaker fee0.00 tDAITotal fees0.00 tDAI")
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from fixtures.market import setup_continuous_market
|
||||
|
||||
from conftest import init_vega
|
||||
|
||||
market_names = ["ETHBTC.QM21", "BTCUSD.MF21", "SOLUSD", "AAPL.MF21"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega():
|
||||
with init_vega() as vega:
|
||||
yield vega
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def create_markets(vega):
|
||||
for market_name in market_names:
|
||||
setup_continuous_market(vega, custom_market_name=market_name)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_table_headers(page: Page, create_markets):
|
||||
page.goto(f"/#/markets/all")
|
||||
headers = [
|
||||
"Market",
|
||||
"Description",
|
||||
"Settlement asset",
|
||||
"Trading mode",
|
||||
"Status",
|
||||
"Mark price",
|
||||
"24h volume",
|
||||
"Open Interest",
|
||||
"Spread",
|
||||
"",
|
||||
]
|
||||
page.wait_for_selector('[data-testid="tab-open-markets"]', state="visible")
|
||||
page_headers = (
|
||||
page.get_by_test_id("tab-open-markets").locator(".ag-header-cell-text").all()
|
||||
)
|
||||
for i, header in enumerate(headers):
|
||||
expect(page_headers[i]).to_have_text(header)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_markets_tab(page: Page, create_markets):
|
||||
page.goto(f"/#/markets/all")
|
||||
expect(page.get_by_test_id("Open markets")).to_have_attribute(
|
||||
"data-state", "active"
|
||||
)
|
||||
expect(page.get_by_test_id("Proposed markets")).to_have_attribute(
|
||||
"data-state", "inactive"
|
||||
)
|
||||
expect(page.get_by_test_id("Closed markets")).to_have_attribute(
|
||||
"data-state", "inactive"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_markets_content(page: Page, create_markets):
|
||||
page.goto(f"/#/markets/all")
|
||||
row_selector = page.locator(
|
||||
'[data-testid="tab-open-markets"] .ag-center-cols-container .ag-row'
|
||||
).first
|
||||
instrument_code_locator = '[col-id="tradableInstrument.instrument.code"] [data-testid="stack-cell-primary"]'
|
||||
# 6001-MARK-035
|
||||
expect(row_selector.locator(instrument_code_locator)).to_have_text("ETHBTC.QM21")
|
||||
|
||||
# 6001-MARK-073
|
||||
expect(row_selector.locator('[title="Future"]')).to_have_text("Futr")
|
||||
|
||||
# 6001-MARK-036
|
||||
expect(
|
||||
row_selector.locator('[col-id="tradableInstrument.instrument.name"]')
|
||||
).to_have_text("ETHBTC.QM21")
|
||||
|
||||
# 6001-MARK-037
|
||||
expect(row_selector.locator('[col-id="tradingMode"]')).to_have_text("Continuous")
|
||||
|
||||
# 6001-MARK-038
|
||||
expect(row_selector.locator('[col-id="state"]')).to_have_text("Active")
|
||||
|
||||
# 6001-MARK-039
|
||||
expect(row_selector.locator('[col-id="data.markPrice"]')).to_have_text("107.50")
|
||||
|
||||
# 6001-MARK-040
|
||||
expect(row_selector.locator('[col-id="data.candles"]')).to_have_text("0.00")
|
||||
|
||||
# 6001-MARK-042
|
||||
expect(
|
||||
row_selector.locator(
|
||||
'[col-id="tradableInstrument.instrument.product.settlementAsset.symbol"]'
|
||||
)
|
||||
).to_have_text("tDAI")
|
||||
|
||||
expect(row_selector.locator('[col-id="data.bestBidPrice"]')).to_have_text("2")
|
||||
|
||||
# 6001-MARK-043
|
||||
row_selector.locator(
|
||||
'[col-id="tradableInstrument.instrument.product.settlementAsset.symbol"] button'
|
||||
).click()
|
||||
expect(page.get_by_test_id("dialog-title")).to_have_text("Asset details - tDAI")
|
||||
# 6001-MARK-019
|
||||
page.get_by_test_id("close-asset-details-dialog").click()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_market_actions(page: Page, create_markets):
|
||||
# 6001-MARK-044
|
||||
# 6001-MARK-045
|
||||
# 6001-MARK-046
|
||||
# 6001-MARK-047
|
||||
page.goto(f"/#/markets/all")
|
||||
page.locator(
|
||||
'.ag-pinned-right-cols-container [col-id="market-actions"]'
|
||||
).first.locator("button").click()
|
||||
|
||||
actions = [
|
||||
"Copy Market ID",
|
||||
"View on Explorer",
|
||||
"View settlement asset details",
|
||||
]
|
||||
action_elements = (
|
||||
page.get_by_test_id("market-actions-content").get_by_role("menuitem").all()
|
||||
)
|
||||
|
||||
for i, action in enumerate(actions):
|
||||
expect(action_elements[i]).to_have_text(action)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_sort_markets(page: Page, create_markets):
|
||||
# 6001-MARK-064
|
||||
|
||||
page.goto(f"/#/markets/all")
|
||||
sorted_market_names = [
|
||||
"AAPL.MF21",
|
||||
"BTCUSD.MF21",
|
||||
"ETHBTC.QM21",
|
||||
"SOLUSD",
|
||||
]
|
||||
page.locator('.ag-header-row [col-id="tradableInstrument.instrument.code"]').click()
|
||||
for i, market_name in enumerate(sorted_market_names):
|
||||
expect(
|
||||
page.locator(
|
||||
f'[row-index="{i}"] [col-id="tradableInstrument.instrument.name"]'
|
||||
)
|
||||
).to_have_text(market_name)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("risk_accepted")
|
||||
def test_drag_and_drop_column(page: Page, create_markets):
|
||||
# 6001-MARK-065
|
||||
page.goto(f"/#/markets/all")
|
||||
col_instrument_code = '.ag-header-row [col-id="tradableInstrument.instrument.code"]'
|
||||
|
||||
page.locator(col_instrument_code).drag_to(
|
||||
page.locator('.ag-header-row [col-id="data.bestBidPrice"]')
|
||||
)
|
||||
expect(page.locator(col_instrument_code)).to_have_attribute("aria-colindex", "9")
|
||||
@@ -1,15 +1,19 @@
|
||||
import pytest
|
||||
import re
|
||||
import vega_sim.api.governance as governance
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.service import VegaService, PeggedOrder
|
||||
import vega_sim.api.governance as governance
|
||||
from actions.vega import submit_order
|
||||
from actions.utils import next_epoch
|
||||
from wallet_config import MM_WALLET, MM_WALLET2, GOVERNANCE_WALLET
|
||||
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("vega", "page", "proposed_market", "risk_accepted")
|
||||
def test_market_lifecycle(proposed_market, vega: VegaService, page: Page):
|
||||
# 7002-SORD-001
|
||||
# 7002-SORD-002
|
||||
trading_mode = page.get_by_test_id("market-trading-mode").get_by_test_id(
|
||||
"item-value"
|
||||
)
|
||||
@@ -18,12 +22,32 @@ def test_market_lifecycle(proposed_market, vega: VegaService, page: Page):
|
||||
# setup market in proposed step, without liquidity provided
|
||||
market_id = proposed_market
|
||||
page.goto(f"/#/markets/{market_id}")
|
||||
|
||||
# 6002-MDET-001
|
||||
expect(page.get_by_test_id("header-title")).to_have_text("BTC:DAI_2023Futr")
|
||||
# 6002-MDET-002
|
||||
expect(page.get_by_test_id("market-expiry")).to_have_text("ExpiryNot time-based")
|
||||
page.get_by_test_id("market-expiry").hover()
|
||||
expect(page.get_by_test_id("expiry-tooltip").first).to_have_text("This market expires when triggered by its oracle, not on a set date.View oracle specification")
|
||||
expect(page.get_by_test_id("expiry-tooltip").first.get_by_test_id("link")).to_have_attribute("href", re.compile('.*'))
|
||||
# 6002-MDET-003
|
||||
expect(page.get_by_test_id("market-price")).to_have_text("Mark Price0.00")
|
||||
# 6002-MDET-004
|
||||
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
|
||||
# 6002-MDET-005
|
||||
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
|
||||
# 6002-MDET-008
|
||||
expect(page.get_by_test_id("market-settlement-asset")).to_have_text("Settlement assettDAI")
|
||||
expect(page.get_by_test_id("liquidity-supplied")).to_have_text("Liquidity supplied 0.00 (0.00%)")
|
||||
page.get_by_test_id("liquidity-supplied").hover()
|
||||
expect(page.get_by_test_id("liquidity-supplied-tooltip").first).to_have_text("Supplied stake0.00Target stake0.00View liquidity provision tableLearn about providing liquidity")
|
||||
expect(page.get_by_test_id("liquidity-supplied-tooltip").first.get_by_test_id("link").first).to_have_text("View liquidity provision table")
|
||||
# check that market is in proposed state
|
||||
# 6002-MDET-006
|
||||
# 6002-MDET-007
|
||||
# 7002-SORD-061
|
||||
expect(trading_mode).to_have_text("No trading")
|
||||
trading_mode.hover()
|
||||
expect(page.get_by_test_id("trading-mode-tooltip").first).to_have_text("No trading enabled for this market.")
|
||||
expect(market_state).to_have_text("Proposed")
|
||||
|
||||
# approve market
|
||||
@@ -35,9 +59,9 @@ def test_market_lifecycle(proposed_market, vega: VegaService, page: Page):
|
||||
|
||||
# "wait" for market to be approved and enacted
|
||||
vega.forward("60s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_fn(10)
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
next_epoch(vega=vega)
|
||||
# check that market is in pending state
|
||||
expect(trading_mode).to_have_text("Opening auction")
|
||||
expect(market_state).to_have_text("Pending")
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import pytest
|
||||
import re
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.service import VegaService
|
||||
from vega_sim.service import MarketStateUpdateType
|
||||
from datetime import datetime, timedelta
|
||||
from conftest import init_vega
|
||||
from actions.utils import change_keys
|
||||
from actions.vega import submit_multiple_orders
|
||||
from fixtures.market import setup_perps_market
|
||||
from wallet_config import MM_WALLET, MM_WALLET2, TERMINATE_WALLET
|
||||
|
||||
row_selector = '[data-testid="tab-funding-payments"] .ag-center-cols-container .ag-row'
|
||||
col_amount = '[col-id="amount"]'
|
||||
|
||||
class TestPerpetuals:
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def vega(self, request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def perps_market(self, vega: VegaService):
|
||||
perps_market = setup_perps_market(vega)
|
||||
submit_multiple_orders(
|
||||
vega, MM_WALLET.name, perps_market, "SIDE_SELL", [[1, 110], [1, 105]]
|
||||
)
|
||||
submit_multiple_orders(
|
||||
vega, MM_WALLET2.name, perps_market, "SIDE_BUY", [[1, 90], [1, 95]]
|
||||
)
|
||||
vega.submit_settlement_data(
|
||||
settlement_key=TERMINATE_WALLET.name,
|
||||
settlement_price=110,
|
||||
market_id=perps_market,
|
||||
)
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
submit_multiple_orders(
|
||||
vega, MM_WALLET.name, perps_market, "SIDE_SELL", [[1, 110], [1, 105]]
|
||||
)
|
||||
submit_multiple_orders(
|
||||
vega, MM_WALLET2.name, perps_market, "SIDE_BUY", [[1, 112], [1, 115]]
|
||||
)
|
||||
vega.submit_settlement_data(
|
||||
settlement_key=TERMINATE_WALLET.name,
|
||||
settlement_price=110,
|
||||
market_id=perps_market,
|
||||
)
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
return perps_market
|
||||
|
||||
@pytest.mark.usefixtures("page","risk_accepted", "auth")
|
||||
def test_funding_payment_profit(self, perps_market, page: Page):
|
||||
page.goto(f"/#/markets/{perps_market}")
|
||||
page.get_by_test_id("Funding payments").click()
|
||||
row = page.locator(row_selector)
|
||||
expect(row.locator(col_amount)).to_have_text("9.00 tDAI")
|
||||
|
||||
@pytest.mark.usefixtures("page","risk_accepted", "auth")
|
||||
def test_funding_payment_loss(self, perps_market, page: Page, vega):
|
||||
page.goto(f"/#/markets/{perps_market}")
|
||||
change_keys(page, vega, "market_maker")
|
||||
page.get_by_test_id("Funding payments").click()
|
||||
row = page.locator(row_selector)
|
||||
expect(row.locator(col_amount)).to_have_text("-27.00 tDAI")
|
||||
|
||||
@pytest.mark.usefixtures("page","risk_accepted", "auth")
|
||||
def test_funding_header(self, perps_market, page: Page):
|
||||
page.goto(f"/#/markets/{perps_market}")
|
||||
expect(page.get_by_test_id("market-funding")).to_contain_text("Funding Rate / Countdown-8.1818%")
|
||||
expect(page.get_by_test_id("index-price")).to_have_text("Index Price110.00")
|
||||
|
||||
@pytest.mark.skip("Skipped due to issue #5421")
|
||||
@pytest.mark.usefixtures("page","risk_accepted", "auth")
|
||||
def test_funding_payment_history(perps_market, page: Page, vega):
|
||||
page.goto(f"/#/markets/{perps_market}")
|
||||
change_keys(page, vega, "market_maker")
|
||||
page.get_by_test_id("Funding history").click()
|
||||
element = page.get_by_test_id("tab-funding-history")
|
||||
# Get the bounding box of the element
|
||||
bounding_box = element.bounding_box()
|
||||
if bounding_box:
|
||||
bottom_right_x = bounding_box["x"] + bounding_box["width"]
|
||||
bottom_right_y = bounding_box["y"] + bounding_box["height"]
|
||||
|
||||
# Hover over the bottom-right corner of the element
|
||||
element.hover(position={"x": bottom_right_x, "y": bottom_right_y})
|
||||
else:
|
||||
print("Bounding box not found for the element")
|
||||
|
||||
@pytest.mark.usefixtures("page","risk_accepted", "auth")
|
||||
def test_perps_market_termination_proposed(page: Page, vega: VegaService):
|
||||
perpetual_market = setup_perps_market(vega)
|
||||
page.goto(f"/#/markets/{perpetual_market}")
|
||||
vega.update_market_state(
|
||||
proposal_key=MM_WALLET.name,
|
||||
market_id=perpetual_market,
|
||||
market_state=MarketStateUpdateType.Terminate,
|
||||
price=100,
|
||||
vote_closing_time = datetime.now() + timedelta(seconds=15),
|
||||
vote_enactment_time = datetime.now() + timedelta(seconds=60),
|
||||
approve_proposal = True,
|
||||
forward_time_to_enactment = False,
|
||||
)
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
banner_text = page.get_by_test_id(f"termination-warning-banner-{perpetual_market}").text_content()
|
||||
pattern = re.compile(
|
||||
r"Trading on Market BTC:DAI_Perpetual may stop on \d{2} [A-Za-z]+\. There is open proposal to close this market\.Proposed final price is 100\.00 BTC\.View proposal"
|
||||
)
|
||||
assert pattern.search(banner_text), f"Text did not match pattern. Text was: {banner_text}"
|
||||
|
||||
@pytest.mark.usefixtures("page","risk_accepted", "auth" )
|
||||
def test_perps_market_terminated(page: Page, vega: VegaService):
|
||||
perpetual_market = setup_perps_market(vega)
|
||||
vega.update_market_state(
|
||||
proposal_key=MM_WALLET.name,
|
||||
market_id=perpetual_market,
|
||||
market_state=MarketStateUpdateType.Terminate,
|
||||
price=100,
|
||||
approve_proposal = True,
|
||||
forward_time_to_enactment = True,
|
||||
)
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
page.goto(f"/#/markets/{perpetual_market}")
|
||||
expect(page.get_by_test_id("market-price")).to_have_text("Mark Price100.00")
|
||||
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)-")
|
||||
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
|
||||
expect(page.get_by_test_id("market-trading-mode")).to_have_text("Trading modeNo trading")
|
||||
expect(page.get_by_test_id("market-state")).to_have_text("StatusClosed")
|
||||
expect(page.get_by_test_id("liquidity-supplied")).to_have_text("Liquidity supplied 0.00 (0.00%)")
|
||||
expect(page.get_by_test_id("market-funding")).to_have_text("Funding Rate / Countdown-Unknown")
|
||||
expect(page.get_by_test_id("index-price")).to_have_text("Index Price-")
|
||||
expect(page.get_by_test_id("deal-ticket-error-message-summary")).to_have_text("This market is closed and not accepting orders")
|
||||
@@ -0,0 +1,116 @@
|
||||
import pytest
|
||||
import re
|
||||
import json
|
||||
from playwright.sync_api import Page, expect, Route
|
||||
from vega_sim.service import VegaService
|
||||
from conftest import init_vega
|
||||
from fixtures.market import setup_continuous_market
|
||||
|
||||
order_size = "order-size"
|
||||
order_price = "order-price"
|
||||
place_order = "place-order"
|
||||
order_side_sell = "order-side-SIDE_SELL"
|
||||
market_order = "order-type-Market"
|
||||
tif = "order-tif"
|
||||
expire = "expire"
|
||||
api_request_match = r"http://localhost:\d+/api/v2/requests"
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega(request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def continuous_market(vega):
|
||||
return setup_continuous_market(vega)
|
||||
|
||||
def handle_route_connection_lost(route: Route, request):
|
||||
if request.method == "POST" and re.match(api_request_match, request.url):
|
||||
route.fulfill(
|
||||
status=200,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body='{"jsonrpc": "2.0", "id": "1"}'
|
||||
)
|
||||
else:
|
||||
route.continue_()
|
||||
|
||||
def handle_route_connection_rejected(route: Route, request):
|
||||
if request.method == "POST" and re.match(api_request_match, request.url):
|
||||
custom_response = {
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": 3001,
|
||||
"data": "the user rejected the wallet connection",
|
||||
"message": "User error"
|
||||
},
|
||||
"id": "0"
|
||||
}
|
||||
route.fulfill(
|
||||
status=400,
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=json.dumps(custom_response)
|
||||
)
|
||||
else:
|
||||
route.continue_()
|
||||
|
||||
def assert_connection_approve(route: Route, request, page:Page):
|
||||
if request.method == "POST" and re.match(api_request_match, request.url):
|
||||
expect(page.get_by_test_id("toast-content")).to_have_text("Please go to your Vega wallet application and approve or reject the transaction.")
|
||||
else:
|
||||
route.continue_()
|
||||
|
||||
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
|
||||
def test_wallet_connection_error(continuous_market, page: Page):
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.route("**/*", handle_route_connection_lost)
|
||||
page.get_by_test_id("connect-vega-wallet").click()
|
||||
page.get_by_test_id("connector-jsonRpc").click()
|
||||
expect(page.get_by_test_id("wallet-dialog-title")).to_have_text("Something went wrong")
|
||||
|
||||
@pytest.mark.usefixtures("page", "risk_accepted")
|
||||
def test_wallet_connection_rejected(continuous_market, page: Page):
|
||||
# 0002-WCON-002
|
||||
# 0002-WCON-005
|
||||
# 0002-WCON-007
|
||||
# 0002-WCON-015
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.route("**/*", handle_route_connection_rejected)
|
||||
page.get_by_test_id("connect-vega-wallet").click()
|
||||
page.get_by_test_id("connector-jsonRpc").click()
|
||||
expect(page.get_by_test_id("dialog-content").nth(1)).to_have_text("User errorthe user rejected the wallet connectionTry againAbout the Vega wallet | Supported browsers ")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
|
||||
def test_wallet_connection_error_transaction(continuous_market, vega: VegaService, page: Page):
|
||||
# 0003-WTXN-009
|
||||
# 0003-WTXN-011
|
||||
# 0002-WCON-016
|
||||
# 0003-WTXN-008
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(order_size).fill("10")
|
||||
page.get_by_test_id(order_price).fill("120")
|
||||
page.route("**/*", handle_route_connection_lost)
|
||||
page.get_by_test_id(place_order).click()
|
||||
expect(page.get_by_test_id("toast-content")).to_have_text("Wallet disconnectedThe connection to your Vega Wallet has been lost.Connect vega wallet")
|
||||
|
||||
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
|
||||
def test_wallet_transaction_rejected(continuous_market, vega: VegaService, page: Page):
|
||||
# 0003-WTXN-007
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(order_size).fill("10")
|
||||
page.get_by_test_id(order_price).fill("120")
|
||||
page.route("**/*", handle_route_connection_rejected)
|
||||
page.get_by_test_id(place_order).click()
|
||||
expect(page.get_by_test_id("toast-content")).to_have_text("Error occurredthe user rejected the wallet connection")
|
||||
|
||||
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
|
||||
def test_wallet_connection_approve(continuous_market, vega: VegaService, page: Page):
|
||||
# 0002-WCON-005
|
||||
# 0002-WCON-007
|
||||
# 0002-WCON-009
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
page.get_by_test_id(order_size).fill("10")
|
||||
page.get_by_test_id(order_price).fill("120")
|
||||
page.route("**/*", assert_connection_approve)
|
||||
page.get_by_test_id(place_order).click()
|
||||
@@ -0,0 +1,60 @@
|
||||
import * as router from 'react-router';
|
||||
import { useNavigateToLastMarket } from './use-navigate-to-last-market';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useTopTradedMarkets } from './use-top-traded-markets';
|
||||
import { Links } from '../links';
|
||||
|
||||
const mockLastMarketId = 'LAST';
|
||||
|
||||
jest.mock('../../stores', () => {
|
||||
const original = jest.requireActual('../../stores');
|
||||
return {
|
||||
...original,
|
||||
useGlobalStore: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('./use-top-traded-markets', () => {
|
||||
return {
|
||||
useTopTradedMarkets: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useNavigateToLastMarket', () => {
|
||||
const navigate = jest.fn();
|
||||
beforeAll(() => {
|
||||
jest.spyOn(router, 'useNavigate').mockImplementation(() => navigate);
|
||||
});
|
||||
|
||||
it('navigates to the last market when it is active', () => {
|
||||
(useGlobalStore as unknown as jest.Mock).mockReturnValue(mockLastMarketId);
|
||||
(useTopTradedMarkets as jest.Mock).mockReturnValue({
|
||||
data: [{ id: mockLastMarketId }],
|
||||
});
|
||||
renderHook(() => useNavigateToLastMarket());
|
||||
expect(navigate).toHaveBeenCalledWith(Links.MARKET(mockLastMarketId), {
|
||||
replace: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to the top traded market if the last one is not active', () => {
|
||||
(useGlobalStore as unknown as jest.Mock).mockReturnValue(mockLastMarketId);
|
||||
(useTopTradedMarkets as jest.Mock).mockReturnValue({
|
||||
data: [{ id: 'TOP' }],
|
||||
});
|
||||
renderHook(() => useNavigateToLastMarket());
|
||||
expect(navigate).toHaveBeenCalledWith(Links.MARKET('TOP'), {
|
||||
replace: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to the list of markets when all of the markets are not active', () => {
|
||||
(useGlobalStore as unknown as jest.Mock).mockReturnValue(mockLastMarketId);
|
||||
(useTopTradedMarkets as jest.Mock).mockReturnValue({
|
||||
data: [],
|
||||
});
|
||||
renderHook(() => useNavigateToLastMarket());
|
||||
expect(navigate).toHaveBeenCalledWith(Links.MARKETS());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useGlobalStore } from '../../stores';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTopTradedMarkets } from './use-top-traded-markets';
|
||||
import { useEffect } from 'react';
|
||||
import { Links } from '../links';
|
||||
|
||||
export const useNavigateToLastMarket = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// this returns a list of active markets ordered by traded factor
|
||||
// hence there's no need to pull markets again or find out in separate
|
||||
// query of the state of last market
|
||||
const { data } = useTopTradedMarkets();
|
||||
const lastMarketId = useGlobalStore((store) => store.marketId);
|
||||
const isLastMarketActive = data?.some((m) => m.id === lastMarketId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
// if last market id is set and it is active, navigate to that market
|
||||
if (lastMarketId && isLastMarketActive) {
|
||||
navigate(Links.MARKET(lastMarketId), {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise if there's a top traded market, navigate to that market
|
||||
const marketDataId = data[0]?.id;
|
||||
if (marketDataId) {
|
||||
navigate(Links.MARKET(marketDataId), {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise navigate to the list of all markets
|
||||
navigate(Links.MARKETS());
|
||||
}, [lastMarketId, data, navigate, isLastMarketActive]);
|
||||
};
|
||||
@@ -13,10 +13,10 @@ import { type IterableElement } from 'type-fest';
|
||||
import {
|
||||
AccountEventsDocument,
|
||||
AccountsDocument,
|
||||
AccountFieldsFragment,
|
||||
AccountsQuery,
|
||||
AccountEventsSubscription,
|
||||
AccountsQueryVariables,
|
||||
type AccountFieldsFragment,
|
||||
type AccountsQuery,
|
||||
type AccountEventsSubscription,
|
||||
type AccountsQueryVariables,
|
||||
} from './__generated__/Accounts';
|
||||
import { type Asset } from '@vegaprotocol/assets';
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ALLOWED_ACCOUNTS = [
|
||||
|
||||
export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
const t = useT();
|
||||
const { pubKey, pubKeys } = useVegaWallet();
|
||||
const { pubKey, pubKeys, isReadOnly } = useVegaWallet();
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.transfer_fee_factor,
|
||||
NetworkParams.transfer_minTransferQuantumMultiple,
|
||||
@@ -70,6 +70,7 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
<TransferForm
|
||||
pubKey={pubKey}
|
||||
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
|
||||
isReadOnly={isReadOnly}
|
||||
assetId={assetId}
|
||||
feeFactor={params.transfer_fee_factor}
|
||||
minQuantumMultiple={params.transfer_minTransferQuantumMultiple}
|
||||
|
||||
@@ -73,6 +73,28 @@ describe('TransferForm', () => {
|
||||
minQuantumMultiple: '1',
|
||||
};
|
||||
|
||||
const propsNoAssets = {
|
||||
pubKey,
|
||||
pubKeys: [
|
||||
pubKey,
|
||||
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
|
||||
],
|
||||
feeFactor: '0.001',
|
||||
submitTransfer: jest.fn(),
|
||||
accounts: [],
|
||||
minQuantumMultiple: '1',
|
||||
};
|
||||
|
||||
it('renders no assets', async () => {
|
||||
renderComponent(propsNoAssets);
|
||||
expect(screen.getByTestId('no-assets-available')).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders no accounts', async () => {
|
||||
renderComponent(propsNoAssets);
|
||||
expect(screen.getByTestId('no-accounts-available')).toBeVisible();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
targetText: 'Include transfer fee',
|
||||
|
||||
@@ -45,6 +45,7 @@ interface Asset {
|
||||
export interface TransferFormProps {
|
||||
pubKey: string | null;
|
||||
pubKeys: string[] | null;
|
||||
isReadOnly?: boolean;
|
||||
accounts: Array<{
|
||||
type: AccountType;
|
||||
balance: string;
|
||||
@@ -59,6 +60,7 @@ export interface TransferFormProps {
|
||||
export const TransferForm = ({
|
||||
pubKey,
|
||||
pubKeys,
|
||||
isReadOnly,
|
||||
assetId: initialAssetId,
|
||||
feeFactor,
|
||||
submitTransfer,
|
||||
@@ -201,27 +203,36 @@ export const TransferForm = ({
|
||||
<Controller
|
||||
control={control}
|
||||
name="asset"
|
||||
render={({ field }) => (
|
||||
<TradingRichSelect
|
||||
data-testid="select-asset"
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
setValue('fromAccount', '');
|
||||
}}
|
||||
placeholder={t('Please select an asset')}
|
||||
value={field.value}
|
||||
>
|
||||
{assets.map((a) => (
|
||||
<AssetOption
|
||||
key={a.key}
|
||||
asset={a}
|
||||
balance={<Balance balance={a.balance} symbol={a.symbol} />}
|
||||
/>
|
||||
))}
|
||||
</TradingRichSelect>
|
||||
)}
|
||||
render={({ field }) =>
|
||||
assets.length > 0 ? (
|
||||
<TradingRichSelect
|
||||
data-testid="select-asset"
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
setValue('fromAccount', '');
|
||||
}}
|
||||
placeholder={t('Please select an asset')}
|
||||
value={field.value}
|
||||
>
|
||||
{assets.map((a) => (
|
||||
<AssetOption
|
||||
key={a.key}
|
||||
asset={a}
|
||||
balance={<Balance balance={a.balance} symbol={a.symbol} />}
|
||||
/>
|
||||
))}
|
||||
</TradingRichSelect>
|
||||
) : (
|
||||
<span
|
||||
data-testid="no-assets-available"
|
||||
className="text-xs text-vega-clight-100 dark:text-vega-cdark-100"
|
||||
>
|
||||
{t('No assets available')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{errors.asset?.message && (
|
||||
<TradingInputError forInput="asset">
|
||||
@@ -249,48 +260,57 @@ export const TransferForm = ({
|
||||
},
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TradingSelect
|
||||
id="fromAccount"
|
||||
defaultValue=""
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
render={({ field }) =>
|
||||
accounts.length > 0 ? (
|
||||
<TradingSelect
|
||||
id="fromAccount"
|
||||
defaultValue=""
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
|
||||
const [type] = parseFromAccount(e.target.value);
|
||||
const [type] = parseFromAccount(e.target.value);
|
||||
|
||||
// Enforce that if transferring from a vested rewards account it must go to
|
||||
// the current connected general account
|
||||
if (
|
||||
type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS &&
|
||||
pubKey
|
||||
) {
|
||||
setValue('toVegaKey', pubKey);
|
||||
setToVegaKeyMode('select');
|
||||
setIncludeFee(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{accounts
|
||||
.filter((a) => {
|
||||
if (!selectedAssetId) return true;
|
||||
return selectedAssetId === a.asset.id;
|
||||
})
|
||||
.map((a) => {
|
||||
const id = `${a.type}-${a.asset.id}`;
|
||||
return (
|
||||
<option value={id} key={id}>
|
||||
{AccountTypeMapping[a.type]} (
|
||||
{addDecimal(a.balance, a.asset.decimals)} {a.asset.symbol}
|
||||
)
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
)}
|
||||
// Enforce that if transferring from a vested rewards account it must go to
|
||||
// the current connected general account
|
||||
if (
|
||||
type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS &&
|
||||
pubKey
|
||||
) {
|
||||
setValue('toVegaKey', pubKey);
|
||||
setToVegaKeyMode('select');
|
||||
setIncludeFee(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{accounts
|
||||
.filter((a) => {
|
||||
if (!selectedAssetId) return true;
|
||||
return selectedAssetId === a.asset.id;
|
||||
})
|
||||
.map((a) => {
|
||||
const id = `${a.type}-${a.asset.id}`;
|
||||
return (
|
||||
<option value={id} key={id}>
|
||||
{AccountTypeMapping[a.type]} (
|
||||
{addDecimal(a.balance, a.asset.decimals)}{' '}
|
||||
{a.asset.symbol})
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
) : (
|
||||
<span
|
||||
data-testid="no-accounts-available"
|
||||
className="text-xs text-vega-clight-100 dark:text-vega-cdark-100"
|
||||
>
|
||||
{t('No accounts available')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{errors.fromAccount?.message && (
|
||||
<TradingInputError forInput="fromAccount">
|
||||
@@ -454,7 +474,7 @@ export const TransferForm = ({
|
||||
decimals={asset?.decimals}
|
||||
/>
|
||||
)}
|
||||
<TradingButton type="submit" fill={true}>
|
||||
<TradingButton type="submit" fill={true} disabled={isReadOnly}>
|
||||
{t('Confirm transfer')}
|
||||
</TradingButton>
|
||||
</form>
|
||||
|
||||
@@ -18,7 +18,7 @@ type Rows = {
|
||||
key: AssetDetail;
|
||||
label: string;
|
||||
tooltip: string;
|
||||
value: (asset: Asset) => ReactNode | undefined;
|
||||
value: (asset: Asset, orignalAsset?: Asset) => ReactNode | undefined;
|
||||
valueTooltip?: (asset: Asset) => string | null | undefined;
|
||||
}[];
|
||||
|
||||
@@ -52,6 +52,21 @@ const num = (asset: Asset, n: string | undefined | null) => {
|
||||
return addDecimalsFormatNumber(n, asset.decimals);
|
||||
};
|
||||
|
||||
const Diff = ({
|
||||
oldValue,
|
||||
newValue,
|
||||
}: {
|
||||
oldValue: ReactNode;
|
||||
newValue: ReactNode;
|
||||
}) => (
|
||||
<span className="flex gap-1">
|
||||
<span className="line-through bg-vega-red-300 dark:bg-vega-red-600">
|
||||
{oldValue}
|
||||
</span>
|
||||
<span className="bg-vega-green-300 dark:bg-vega-green-600">{newValue}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
export const useRows = () => {
|
||||
const t = useT();
|
||||
const AssetTypeMapping = useAssetTypeMapping();
|
||||
@@ -103,7 +118,14 @@ export const useRows = () => {
|
||||
key: AssetDetail.QUANTUM,
|
||||
label: t('Quantum'),
|
||||
tooltip: t('The minimum economically meaningful amount of the asset'),
|
||||
value: (asset) => num(asset, asset.quantum),
|
||||
value: (asset, originalAsset) => {
|
||||
const value = num(asset, asset.quantum);
|
||||
if (originalAsset && originalAsset.quantum !== asset.quantum) {
|
||||
const original = num(originalAsset, originalAsset.quantum);
|
||||
return <Diff oldValue={original} newValue={value} />;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AssetDetail.STATUS,
|
||||
@@ -143,8 +165,24 @@ export const useRows = () => {
|
||||
tooltip: t('WITHDRAW_THRESHOLD_TOOLTIP_TEXT', {
|
||||
defaultValue: WITHDRAW_THRESHOLD_TOOLTIP_TEXT,
|
||||
}),
|
||||
value: (asset) =>
|
||||
num(asset, (asset.source as Schema.ERC20).withdrawThreshold),
|
||||
value: (asset, originalAsset) => {
|
||||
const value = num(
|
||||
asset,
|
||||
(asset.source as Schema.ERC20).withdrawThreshold
|
||||
);
|
||||
if (
|
||||
originalAsset &&
|
||||
(originalAsset.source as Schema.ERC20).withdrawThreshold !==
|
||||
(asset.source as Schema.ERC20).withdrawThreshold
|
||||
) {
|
||||
const original = num(
|
||||
asset,
|
||||
(originalAsset.source as Schema.ERC20).withdrawThreshold
|
||||
);
|
||||
return <Diff oldValue={original} newValue={value} />;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AssetDetail.LIFETIME_LIMIT,
|
||||
@@ -152,8 +190,26 @@ export const useRows = () => {
|
||||
tooltip: t(
|
||||
'The lifetime deposit limit per address. Note: this is a temporary measure that can be changed or removed through governance'
|
||||
),
|
||||
value: (asset) =>
|
||||
num(asset, (asset.source as Schema.ERC20).lifetimeLimit),
|
||||
value: (asset, originalAsset) => {
|
||||
const value = num(
|
||||
asset,
|
||||
(asset.source as Schema.ERC20).lifetimeLimit
|
||||
);
|
||||
|
||||
if (
|
||||
originalAsset &&
|
||||
(originalAsset.source as Schema.ERC20).lifetimeLimit !==
|
||||
(asset.source as Schema.ERC20).lifetimeLimit
|
||||
) {
|
||||
const original = num(
|
||||
asset,
|
||||
(originalAsset.source as Schema.ERC20).lifetimeLimit
|
||||
);
|
||||
return <Diff oldValue={original} newValue={value} />;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AssetDetail.MAX_FAUCET_AMOUNT_MINT,
|
||||
@@ -261,10 +317,13 @@ export const testId = (detail: AssetDetail, field: 'label' | 'value') =>
|
||||
|
||||
export type AssetDetailsTableProps = {
|
||||
asset: Asset;
|
||||
originalAsset?: Asset;
|
||||
|
||||
omitRows?: AssetDetail[];
|
||||
} & Omit<KeyValueTableRowProps, 'children'>;
|
||||
export const AssetDetailsTable = ({
|
||||
asset,
|
||||
originalAsset,
|
||||
omitRows = [],
|
||||
...props
|
||||
}: AssetDetailsTableProps) => {
|
||||
@@ -275,7 +334,7 @@ export const AssetDetailsTable = ({
|
||||
|
||||
const details = useRows().map((r) => ({
|
||||
...r,
|
||||
value: r.value(asset),
|
||||
value: r.value(asset, originalAsset),
|
||||
valueTooltip: r.valueTooltip?.(asset),
|
||||
}));
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { ApolloError } from '@apollo/client';
|
||||
import type { GraphQLErrors } from '@apollo/client/errors';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { Subscription, Observable } from 'zen-observable-ts';
|
||||
import { type Subscription, type Observable } from 'zen-observable-ts';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
type Item = {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -19,16 +19,27 @@ export const LayoutCell = ({
|
||||
}: LayoutCellProps) => {
|
||||
const t = useT();
|
||||
const classes = [
|
||||
'lg:text-right flex justify-between lg:block',
|
||||
'lg:text-right flex lg:block justify-stretch gap-2',
|
||||
'my-2 lg:my-0',
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={classnames(classes)}>
|
||||
{label && <span className="lg:hidden">{label}</span>}
|
||||
{label && (
|
||||
<>
|
||||
<span className="lg:hidden text-xs text-vega-clight-200 dark:text-vega-cdark-200 whitespace-nowrap">
|
||||
{label}
|
||||
</span>
|
||||
<span
|
||||
/* separator */
|
||||
aria-hidden
|
||||
className="border-b border-dashed border-b-vega-clight-400 dark:border-b-vega-cdark-400 w-full h-[9px]"
|
||||
></span>
|
||||
</>
|
||||
)}
|
||||
<span
|
||||
data-testid={dataTestId}
|
||||
className={classnames('font-mono', {
|
||||
className={classnames('font-mono text-xs lg:text-sm', {
|
||||
'text-danger': !isLoading && hasError,
|
||||
'text-muted': isLoading,
|
||||
})}
|
||||
|
||||
@@ -89,30 +89,30 @@ export const NodeSwitcher = ({ closeDialog }: { closeDialog: () => void }) => {
|
||||
<span className="text-right">{t('Block')}</span>
|
||||
<span className="text-right">{t('Subscription')}</span>
|
||||
</LayoutRow>
|
||||
<div>
|
||||
{nodes.map((node, index) => {
|
||||
return (
|
||||
<LayoutRow key={node} dataTestId="node-row">
|
||||
<ApolloWrapper url={node}>
|
||||
<RowData
|
||||
id={index.toString()}
|
||||
url={node}
|
||||
highestBlock={highestBlock}
|
||||
onBlockHeight={handleHighestBlock}
|
||||
/>
|
||||
</ApolloWrapper>
|
||||
</LayoutRow>
|
||||
);
|
||||
})}
|
||||
<CustomRowWrapper
|
||||
inputText={customUrlText}
|
||||
setInputText={setCustomUrlText}
|
||||
nodes={nodes}
|
||||
highestBlock={highestBlock}
|
||||
onBlockHeight={handleHighestBlock}
|
||||
nodeRadio={nodeRadio}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{nodes.map((node, index) => {
|
||||
return (
|
||||
<LayoutRow key={node} dataTestId="node-row">
|
||||
<ApolloWrapper url={node}>
|
||||
<RowData
|
||||
id={index.toString()}
|
||||
url={node}
|
||||
highestBlock={highestBlock}
|
||||
onBlockHeight={handleHighestBlock}
|
||||
/>
|
||||
</ApolloWrapper>
|
||||
</LayoutRow>
|
||||
);
|
||||
})}
|
||||
<CustomRowWrapper
|
||||
inputText={customUrlText}
|
||||
setInputText={setCustomUrlText}
|
||||
nodes={nodes}
|
||||
highestBlock={highestBlock}
|
||||
onBlockHeight={handleHighestBlock}
|
||||
nodeRadio={nodeRadio}
|
||||
/>
|
||||
</div>
|
||||
</TradingRadioGroup>
|
||||
<div className="mt-4">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,20 +28,12 @@ import {
|
||||
import { forwardRef } from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { type Trade } from './fills-data-provider';
|
||||
import {
|
||||
type FillFieldsFragment,
|
||||
type TradeFeeFieldsFragment,
|
||||
} from './__generated__/Fills';
|
||||
import { FillActionsDropdown } from './fill-actions-dropdown';
|
||||
import { getAsset } from '@vegaprotocol/markets';
|
||||
import { useT } from './use-t';
|
||||
import { MAKER, TAKER, getFeesBreakdown, getRoleAndFees } from './fills-utils';
|
||||
|
||||
const TAKER = 'Taker';
|
||||
const MAKER = 'Maker';
|
||||
|
||||
export type Role = typeof TAKER | typeof MAKER | '-';
|
||||
|
||||
export type Props = (AgGridReactProps | AgReactUiProps) & {
|
||||
type Props = (AgGridReactProps | AgReactUiProps) & {
|
||||
partyId: string;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
};
|
||||
@@ -262,63 +254,13 @@ const formatFeeDiscount = (partyId: string) => {
|
||||
}: VegaValueFormatterParams<Trade, 'market'>) => {
|
||||
if (!market || !data) return '-';
|
||||
const asset = getAsset(market);
|
||||
const { fees } = getRoleAndFees({ data, partyId });
|
||||
if (!fees) return '-';
|
||||
|
||||
const total = getTotalFeesDiscounts(fees);
|
||||
return addDecimalsFormatNumber(total, asset.decimals);
|
||||
const { fees: roleFees, role } = getRoleAndFees({ data, partyId });
|
||||
if (!roleFees) return '-';
|
||||
const { totalFeeDiscount } = getFeesBreakdown(role, roleFees);
|
||||
return addDecimalsFormatNumber(totalFeeDiscount, asset.decimals);
|
||||
};
|
||||
};
|
||||
|
||||
export const isEmptyFeeObj = (feeObj: Schema.TradeFee) => {
|
||||
if (!feeObj) return true;
|
||||
return (
|
||||
feeObj.liquidityFee === '0' &&
|
||||
feeObj.makerFee === '0' &&
|
||||
feeObj.infrastructureFee === '0'
|
||||
);
|
||||
};
|
||||
|
||||
export const getRoleAndFees = ({
|
||||
data,
|
||||
partyId,
|
||||
}: {
|
||||
data: Pick<
|
||||
FillFieldsFragment,
|
||||
'buyerFee' | 'sellerFee' | 'buyer' | 'seller' | 'aggressor'
|
||||
>;
|
||||
partyId?: string;
|
||||
}) => {
|
||||
let role: Role;
|
||||
let fees;
|
||||
if (data?.buyer.id === partyId) {
|
||||
if (data.aggressor === Schema.Side.SIDE_BUY) {
|
||||
role = TAKER;
|
||||
fees = data?.buyerFee;
|
||||
} else if (data.aggressor === Schema.Side.SIDE_SELL) {
|
||||
role = MAKER;
|
||||
fees = data?.sellerFee;
|
||||
} else {
|
||||
role = '-';
|
||||
fees = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee;
|
||||
}
|
||||
} else if (data?.seller.id === partyId) {
|
||||
if (data.aggressor === Schema.Side.SIDE_SELL) {
|
||||
role = TAKER;
|
||||
fees = data?.sellerFee;
|
||||
} else if (data.aggressor === Schema.Side.SIDE_BUY) {
|
||||
role = MAKER;
|
||||
fees = data?.buyerFee;
|
||||
} else {
|
||||
role = '-';
|
||||
fees = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee;
|
||||
}
|
||||
} else {
|
||||
return { role: '-', fees: undefined };
|
||||
}
|
||||
return { role, fees };
|
||||
};
|
||||
|
||||
const FeesBreakdownTooltip = ({
|
||||
data,
|
||||
value: market,
|
||||
@@ -331,16 +273,23 @@ const FeesBreakdownTooltip = ({
|
||||
|
||||
const asset = getAsset(market);
|
||||
|
||||
const { role, fees } = getRoleAndFees({ data, partyId }) ?? {};
|
||||
const { role, fees, marketState } = getRoleAndFees({ data, partyId }) ?? {};
|
||||
if (!fees) return null;
|
||||
const { infrastructureFee, liquidityFee, makerFee, totalFee } =
|
||||
getFeesBreakdown(role, fees);
|
||||
getFeesBreakdown(role, fees, marketState);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="fee-breakdown-tooltip"
|
||||
className="bg-vega-light-100 dark:bg-vega-dark-100 border-vega-light-200 dark:border-vega-dark-200 break-word z-20 max-w-sm rounded border px-4 py-2 text-sm text-black dark:text-white"
|
||||
className="bg-vega-light-100 dark:bg-vega-dark-100 border-vega-light-200 dark:border-vega-dark-200 break-word z-20 max-w-sm rounded border px-4 py-2 text-xs text-black dark:text-white"
|
||||
>
|
||||
{marketState && (
|
||||
<p className="mb-1 italic">
|
||||
{t('If the market was {{state}}', {
|
||||
state: Schema.MarketStateMapping[marketState].toLowerCase(),
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{role === MAKER && (
|
||||
<>
|
||||
<p className="mb-1">{t('The maker will receive the maker fee.')}</p>
|
||||
@@ -354,7 +303,7 @@ const FeesBreakdownTooltip = ({
|
||||
{role === TAKER && (
|
||||
<p className="mb-1">{t('Fees to be paid by the taker.')}</p>
|
||||
)}
|
||||
{role === '-' && (
|
||||
{(role === '-' || marketState === Schema.MarketState.STATE_SUSPENDED) && (
|
||||
<p className="mb-1">
|
||||
{t(
|
||||
'If the market is in monitoring auction, half of the infrastructure and liquidity fees will be paid.'
|
||||
@@ -395,8 +344,8 @@ const FeesDiscountBreakdownTooltipItem = ({
|
||||
}) =>
|
||||
value && value !== '0' ? (
|
||||
<>
|
||||
<dt className="col-span-1">{label}</dt>
|
||||
<dd className="col-span-1 text-right">
|
||||
<dt className="col-span-2">{label}</dt>
|
||||
<dd className="col-span-2 text-right">
|
||||
{addDecimalsFormatNumber(value, asset.decimals)} {asset.symbol}
|
||||
</dd>
|
||||
</>
|
||||
@@ -412,15 +361,19 @@ export const FeesDiscountBreakdownTooltip = ({
|
||||
}
|
||||
const asset = getAsset(data.market);
|
||||
|
||||
const { fees } = getRoleAndFees({ data, partyId }) ?? {};
|
||||
if (!fees) return null;
|
||||
|
||||
const {
|
||||
fees: roleFees,
|
||||
marketState,
|
||||
role,
|
||||
} = getRoleAndFees({ data, partyId }) ?? {};
|
||||
if (!roleFees) return null;
|
||||
const fees = getFeesBreakdown(role, roleFees, marketState);
|
||||
return (
|
||||
<div
|
||||
data-testid="fee-discount-breakdown-tooltip"
|
||||
className="bg-vega-light-100 dark:bg-vega-dark-100 border-vega-light-200 dark:border-vega-dark-200 break-word z-20 max-w-sm rounded border px-4 py-2 text-sm text-black dark:text-white"
|
||||
>
|
||||
<dl className="grid grid-cols-2 gap-x-1">
|
||||
<dl className="grid grid-cols-6 gap-x-1 text-xs">
|
||||
{(fees.infrastructureFeeReferralDiscount || '0') !== '0' ||
|
||||
(fees.infrastructureFeeVolumeDiscount || '0') !== '0' ? (
|
||||
<dt className="col-span-2">{t('Infrastructure Fee')}</dt>
|
||||
@@ -464,42 +417,14 @@ export const FeesDiscountBreakdownTooltip = ({
|
||||
label={t('Volume Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
|
||||
<dt className="col-span-2">{t('Total Fee Discount')}</dt>
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.totalFeeDiscount}
|
||||
label={''}
|
||||
asset={asset}
|
||||
/>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getTotalFeesDiscounts = (fees: TradeFeeFieldsFragment) => {
|
||||
return (
|
||||
BigInt(fees.infrastructureFeeReferralDiscount || '0') +
|
||||
BigInt(fees.infrastructureFeeVolumeDiscount || '0') +
|
||||
BigInt(fees.liquidityFeeReferralDiscount || '0') +
|
||||
BigInt(fees.liquidityFeeVolumeDiscount || '0') +
|
||||
BigInt(fees.makerFeeReferralDiscount || '0') +
|
||||
BigInt(fees.makerFeeVolumeDiscount || '0')
|
||||
).toString();
|
||||
};
|
||||
|
||||
export const getFeesBreakdown = (
|
||||
role: Role,
|
||||
feesObj: TradeFeeFieldsFragment
|
||||
) => {
|
||||
const makerFee =
|
||||
role === MAKER
|
||||
? new BigNumber(feesObj.makerFee).times(-1).toString()
|
||||
: feesObj.makerFee;
|
||||
|
||||
const infrastructureFee = feesObj.infrastructureFee;
|
||||
const liquidityFee = feesObj.liquidityFee;
|
||||
|
||||
const totalFee = new BigNumber(infrastructureFee)
|
||||
.plus(makerFee)
|
||||
.plus(liquidityFee)
|
||||
.toString();
|
||||
return {
|
||||
infrastructureFee,
|
||||
liquidityFee,
|
||||
makerFee,
|
||||
totalFee,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import { getFeesBreakdown } from './fills-utils';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
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',
|
||||
totalFeeDiscount: '0',
|
||||
};
|
||||
expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a maker if market is active', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '0',
|
||||
liquidityFee: '0',
|
||||
makerFee: '-1000',
|
||||
totalFee: '-1000',
|
||||
totalFeeDiscount: '0',
|
||||
};
|
||||
expect(
|
||||
getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_ACTIVE)
|
||||
).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a maker if the market is suspended', () => {
|
||||
const fees = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '0',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '1000',
|
||||
liquidityFee: '1500',
|
||||
makerFee: '0',
|
||||
totalFee: '2500',
|
||||
totalFeeDiscount: '0',
|
||||
};
|
||||
expect(
|
||||
getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_SUSPENDED)
|
||||
).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a taker if the market is suspended', () => {
|
||||
const fees = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '0',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '1000',
|
||||
liquidityFee: '1500',
|
||||
makerFee: '0',
|
||||
totalFee: '2500',
|
||||
totalFeeDiscount: '0',
|
||||
};
|
||||
expect(
|
||||
getFeesBreakdown('Taker', fees, Schema.MarketState.STATE_SUSPENDED)
|
||||
).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a taker if market is active', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '1000',
|
||||
totalFee: '6000',
|
||||
totalFeeDiscount: '0',
|
||||
};
|
||||
expect(
|
||||
getFeesBreakdown('Taker', fees, Schema.MarketState.STATE_ACTIVE)
|
||||
).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a maker', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '0',
|
||||
liquidityFee: '0',
|
||||
makerFee: '-1000',
|
||||
totalFee: '-1000',
|
||||
totalFeeDiscount: '0',
|
||||
};
|
||||
expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct total fees discount value for a taker (if the market is active - default)', () => {
|
||||
const fees = {
|
||||
infrastructureFeeReferralDiscount: '1',
|
||||
infrastructureFeeVolumeDiscount: '2',
|
||||
liquidityFeeReferralDiscount: '3',
|
||||
liquidityFeeVolumeDiscount: '4',
|
||||
makerFeeReferralDiscount: '5',
|
||||
makerFeeVolumeDiscount: '6',
|
||||
infrastructureFee: '1000',
|
||||
liquidityFee: '2000',
|
||||
makerFee: '3000',
|
||||
};
|
||||
const { totalFeeDiscount } = getFeesBreakdown('Taker', fees);
|
||||
expect(totalFeeDiscount).toEqual((1 + 2 + 3 + 4 + 5 + 6).toString());
|
||||
});
|
||||
|
||||
it('should return correct total fees discount value for a maker (if the market is active - default)', () => {
|
||||
const fees = {
|
||||
infrastructureFeeReferralDiscount: '1',
|
||||
infrastructureFeeVolumeDiscount: '2',
|
||||
liquidityFeeReferralDiscount: '3',
|
||||
liquidityFeeVolumeDiscount: '4',
|
||||
makerFeeReferralDiscount: '5',
|
||||
makerFeeVolumeDiscount: '6',
|
||||
infrastructureFee: '1000',
|
||||
liquidityFee: '2000',
|
||||
makerFee: '3000',
|
||||
};
|
||||
const { totalFeeDiscount } = getFeesBreakdown('Maker', fees);
|
||||
// makerFeeReferralDiscount and makerFeeVolumeDiscount are added, infra and liq. fees are zeroed
|
||||
expect(totalFeeDiscount).toEqual((5 + 6).toString());
|
||||
});
|
||||
|
||||
it('should return correct total fees discount value for a maker (if the market is suspended)', () => {
|
||||
const fees = {
|
||||
infrastructureFeeReferralDiscount: '1',
|
||||
infrastructureFeeVolumeDiscount: '2',
|
||||
liquidityFeeReferralDiscount: '3',
|
||||
liquidityFeeVolumeDiscount: '4',
|
||||
makerFeeReferralDiscount: '5',
|
||||
makerFeeVolumeDiscount: '6',
|
||||
infrastructureFee: '1000',
|
||||
liquidityFee: '2000',
|
||||
makerFee: '3000',
|
||||
};
|
||||
const { totalFeeDiscount } = getFeesBreakdown(
|
||||
'Maker',
|
||||
fees,
|
||||
Schema.MarketState.STATE_SUSPENDED
|
||||
);
|
||||
// makerFeeReferralDiscount and makerFeeVolumeDiscount are zeroed, infra and liq. fees are halved
|
||||
expect(totalFeeDiscount).toEqual(((1 + 2 + 3 + 4) / 2).toString());
|
||||
});
|
||||
|
||||
it('should return correct total fees discount value for a taker (if the market is suspended)', () => {
|
||||
const fees = {
|
||||
infrastructureFeeReferralDiscount: '1',
|
||||
infrastructureFeeVolumeDiscount: '2',
|
||||
liquidityFeeReferralDiscount: '3',
|
||||
liquidityFeeVolumeDiscount: '4',
|
||||
makerFeeReferralDiscount: '5',
|
||||
makerFeeVolumeDiscount: '6',
|
||||
infrastructureFee: '1000',
|
||||
liquidityFee: '2000',
|
||||
makerFee: '3000',
|
||||
};
|
||||
const { totalFeeDiscount } = getFeesBreakdown(
|
||||
'Taker',
|
||||
fees,
|
||||
Schema.MarketState.STATE_SUSPENDED
|
||||
);
|
||||
// makerFeeReferralDiscount and makerFeeVolumeDiscount are zeroed, infra and liq. fees are halved
|
||||
expect(totalFeeDiscount).toEqual(((1 + 2 + 3 + 4) / 2).toString());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type {
|
||||
FillFieldsFragment,
|
||||
TradeFeeFieldsFragment,
|
||||
} from './__generated__/Fills';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
export const TAKER = 'Taker';
|
||||
export const MAKER = 'Maker';
|
||||
|
||||
export type Role = typeof TAKER | typeof MAKER | '-';
|
||||
|
||||
export const getRoleAndFees = ({
|
||||
data,
|
||||
partyId,
|
||||
}: {
|
||||
data: Pick<
|
||||
FillFieldsFragment,
|
||||
'buyerFee' | 'sellerFee' | 'buyer' | 'seller' | 'aggressor'
|
||||
>;
|
||||
partyId?: string;
|
||||
}): {
|
||||
role: Role;
|
||||
fees?: TradeFeeFieldsFragment;
|
||||
marketState?: Schema.MarketState;
|
||||
} => {
|
||||
let role: Role;
|
||||
let fees;
|
||||
|
||||
if (data?.buyer.id === partyId) {
|
||||
if (data.aggressor === Schema.Side.SIDE_BUY) {
|
||||
role = TAKER;
|
||||
fees = data?.buyerFee;
|
||||
} else if (data.aggressor === Schema.Side.SIDE_SELL) {
|
||||
role = MAKER;
|
||||
fees = data?.sellerFee;
|
||||
} else {
|
||||
role = '-';
|
||||
fees = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee;
|
||||
}
|
||||
} else if (data?.seller.id === partyId) {
|
||||
if (data.aggressor === Schema.Side.SIDE_SELL) {
|
||||
role = TAKER;
|
||||
fees = data?.sellerFee;
|
||||
} else if (data.aggressor === Schema.Side.SIDE_BUY) {
|
||||
role = MAKER;
|
||||
fees = data?.buyerFee;
|
||||
} else {
|
||||
role = '-';
|
||||
fees = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee;
|
||||
}
|
||||
} else {
|
||||
return { role: '-', fees: undefined };
|
||||
}
|
||||
|
||||
// We make the assumption that the market state is active if the maker fee is zero on both sides
|
||||
// This needs to be updated when we have a way to get the correct market state when that fill happened from the API
|
||||
// because the maker fee factor can be set to 0 via governance
|
||||
const marketState =
|
||||
data?.buyerFee.makerFee === data.sellerFee.makerFee &&
|
||||
new BigNumber(data?.buyerFee.makerFee).isZero()
|
||||
? Schema.MarketState.STATE_SUSPENDED
|
||||
: Schema.MarketState.STATE_ACTIVE;
|
||||
return { role, fees, marketState };
|
||||
};
|
||||
|
||||
export const getFeesBreakdown = (
|
||||
role: Role,
|
||||
fees: TradeFeeFieldsFragment,
|
||||
marketState: Schema.MarketState = Schema.MarketState.STATE_ACTIVE
|
||||
) => {
|
||||
// If market is in auction we assume maker fee is zero
|
||||
const isMarketActive = marketState === Schema.MarketState.STATE_ACTIVE;
|
||||
|
||||
// If role is taker, then these are the fees to be paid
|
||||
let { makerFee, infrastructureFee, liquidityFee } = fees;
|
||||
// If role is taker, then these are the fees discounts to be applied
|
||||
let {
|
||||
makerFeeVolumeDiscount,
|
||||
makerFeeReferralDiscount,
|
||||
infrastructureFeeVolumeDiscount,
|
||||
infrastructureFeeReferralDiscount,
|
||||
liquidityFeeVolumeDiscount,
|
||||
liquidityFeeReferralDiscount,
|
||||
} = fees;
|
||||
|
||||
if (isMarketActive) {
|
||||
if (role === MAKER) {
|
||||
makerFee = new BigNumber(fees.makerFee).times(-1).toString();
|
||||
infrastructureFee = '0';
|
||||
liquidityFee = '0';
|
||||
|
||||
// discounts are also zero or we can leave them undefined
|
||||
infrastructureFeeReferralDiscount =
|
||||
infrastructureFeeReferralDiscount && '0';
|
||||
infrastructureFeeVolumeDiscount = infrastructureFeeVolumeDiscount && '0';
|
||||
liquidityFeeReferralDiscount = liquidityFeeReferralDiscount && '0';
|
||||
liquidityFeeVolumeDiscount = liquidityFeeVolumeDiscount && '0';
|
||||
|
||||
// we leave maker discount fees as they are defined
|
||||
}
|
||||
} else {
|
||||
// If market is suspended (in monitoring auction), then half of the fees are paid
|
||||
infrastructureFee = new BigNumber(infrastructureFee)
|
||||
.dividedBy(2)
|
||||
.toString();
|
||||
liquidityFee = new BigNumber(liquidityFee).dividedBy(2).toString();
|
||||
// maker fee is already zero
|
||||
makerFee = '0';
|
||||
|
||||
// discounts are also halved
|
||||
infrastructureFeeReferralDiscount =
|
||||
infrastructureFeeReferralDiscount &&
|
||||
new BigNumber(infrastructureFeeReferralDiscount).dividedBy(2).toString();
|
||||
infrastructureFeeVolumeDiscount =
|
||||
infrastructureFeeVolumeDiscount &&
|
||||
new BigNumber(infrastructureFeeVolumeDiscount).dividedBy(2).toString();
|
||||
liquidityFeeReferralDiscount =
|
||||
liquidityFeeReferralDiscount &&
|
||||
new BigNumber(liquidityFeeReferralDiscount).dividedBy(2).toString();
|
||||
liquidityFeeVolumeDiscount =
|
||||
liquidityFeeVolumeDiscount &&
|
||||
new BigNumber(liquidityFeeVolumeDiscount).dividedBy(2).toString();
|
||||
// maker discount fees should already be zero
|
||||
makerFeeReferralDiscount = makerFeeReferralDiscount && '0';
|
||||
makerFeeVolumeDiscount = makerFeeVolumeDiscount && '0';
|
||||
}
|
||||
|
||||
const totalFee = new BigNumber(infrastructureFee)
|
||||
.plus(makerFee)
|
||||
.plus(liquidityFee)
|
||||
.toString();
|
||||
|
||||
const totalFeeDiscount = new BigNumber(makerFeeVolumeDiscount || '0')
|
||||
.plus(makerFeeReferralDiscount || '0')
|
||||
.plus(infrastructureFeeReferralDiscount || '0')
|
||||
.plus(infrastructureFeeVolumeDiscount || '0')
|
||||
.plus(liquidityFeeReferralDiscount || '0')
|
||||
.plus(liquidityFeeVolumeDiscount || '0')
|
||||
.toString();
|
||||
|
||||
return {
|
||||
infrastructureFee,
|
||||
infrastructureFeeReferralDiscount,
|
||||
infrastructureFeeVolumeDiscount,
|
||||
liquidityFee,
|
||||
liquidityFeeReferralDiscount,
|
||||
liquidityFeeVolumeDiscount,
|
||||
makerFee,
|
||||
makerFeeReferralDiscount,
|
||||
makerFeeVolumeDiscount,
|
||||
totalFee,
|
||||
totalFeeDiscount,
|
||||
};
|
||||
};
|
||||
|
||||
export const isEmptyFeeObj = (feeObj: Schema.TradeFee) => {
|
||||
if (!feeObj) return true;
|
||||
return (
|
||||
feeObj.liquidityFee === '0' &&
|
||||
feeObj.makerFee === '0' &&
|
||||
feeObj.infrastructureFee === '0'
|
||||
);
|
||||
};
|
||||
@@ -119,6 +119,8 @@ describe('getLiquidityProvision', () => {
|
||||
createdAt: '2022-12-16T09:28:29.071781Z',
|
||||
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
|
||||
fee: '0.001',
|
||||
partyId:
|
||||
'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
accountsConnection: {
|
||||
|
||||
@@ -159,7 +159,14 @@ export const getLiquidityProvision = (
|
||||
const liquidityProvider = liquidityProviders.find(
|
||||
(f) => liquidityProvision.party.id === f.partyId
|
||||
);
|
||||
if (!liquidityProvider) return liquidityProvision;
|
||||
|
||||
if (!liquidityProvider) {
|
||||
return {
|
||||
...liquidityProvision,
|
||||
partyId: liquidityProvision.party.id,
|
||||
};
|
||||
}
|
||||
|
||||
const accounts = compact(
|
||||
liquidityProvision.party.accountsConnection?.edges
|
||||
).map((e) => e.node);
|
||||
|
||||
@@ -107,9 +107,6 @@ export const LiquidityTable = ({
|
||||
|
||||
const feesAccruedTooltip = ({ value, data }: ITooltipParams) => {
|
||||
if (!value) return '-';
|
||||
const newValue = new BigNumber(value)
|
||||
.times(Number(stakeToCcyVolume) || 1)
|
||||
.toString();
|
||||
let lessThanFull = false,
|
||||
lessThanMinimum = false;
|
||||
if (data.sla) {
|
||||
@@ -154,7 +151,7 @@ export const LiquidityTable = ({
|
||||
}
|
||||
);
|
||||
}
|
||||
return addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0);
|
||||
return addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0);
|
||||
};
|
||||
|
||||
const stakeToCcyVolumeQuantumFormatter = ({
|
||||
@@ -416,14 +413,9 @@ export const LiquidityTable = ({
|
||||
},
|
||||
'text-red-500': ({ data }: { data: LiquidityProvisionData }) => {
|
||||
if (!data.sla) return false;
|
||||
return (
|
||||
new BigNumber(
|
||||
data.sla.currentEpochFractionOfTimeOnBook
|
||||
).isLessThan(data.commitmentMinTimeFraction) &&
|
||||
new BigNumber(
|
||||
data.sla.currentEpochFractionOfTimeOnBook
|
||||
).isGreaterThan(0)
|
||||
);
|
||||
return new BigNumber(
|
||||
data.sla.currentEpochFractionOfTimeOnBook
|
||||
).isLessThan(data.commitmentMinTimeFraction);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,7 +7,11 @@ import {
|
||||
} from '@vegaprotocol/data-provider';
|
||||
import { type Market } from '@vegaprotocol/markets';
|
||||
import { marketsMapProvider } from '@vegaprotocol/markets';
|
||||
import { Cursor, type PageInfo, type Edge } from '@vegaprotocol/data-provider';
|
||||
import {
|
||||
type Cursor,
|
||||
type PageInfo,
|
||||
type Edge,
|
||||
} from '@vegaprotocol/data-provider';
|
||||
import { OrderStatus } from '@vegaprotocol/types';
|
||||
import {
|
||||
OrdersDocument,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { memo, forwardRef, useMemo, type ForwardedRef } from 'react';
|
||||
import {
|
||||
MAXGOINT64,
|
||||
HALFMAXGOINT64,
|
||||
addDecimalsFormatNumber,
|
||||
getDateTimeFormat,
|
||||
isNumeric,
|
||||
@@ -151,7 +151,7 @@ export const OrderListTable = memo<
|
||||
: '';
|
||||
|
||||
if (
|
||||
data.size === MAXGOINT64 &&
|
||||
data.size >= HALFMAXGOINT64 &&
|
||||
data.timeInForce ===
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC &&
|
||||
data.reduceOnly
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PositionsManager } from './positions-manager';
|
||||
import { positionsMarketsProvider } from './positions-data-providers';
|
||||
import { singleRow } from './positions.mock';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { MAXGOINT64 } from '@vegaprotocol/utils';
|
||||
import { HALFMAXGOINT64 } from '@vegaprotocol/utils';
|
||||
|
||||
const mockCreate = jest.fn();
|
||||
|
||||
@@ -31,9 +31,7 @@ jest.mock('@vegaprotocol/data-provider', () => ({
|
||||
}));
|
||||
|
||||
describe('PositionsManager', () => {
|
||||
// TODO: Close position temporarily disabled in https://github.com/vegaprotocol/frontend-monorepo/pull/5350
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('should close position with max uint64', async () => {
|
||||
it('should close position with half of max uint64', async () => {
|
||||
render(<PositionsManager partyIds={['partyId']} isReadOnly={false} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
@@ -43,6 +41,6 @@ describe('PositionsManager', () => {
|
||||
|
||||
expect(
|
||||
mockCreate.mock.lastCall[0].batchMarketInstructions.submissions[0].size
|
||||
).toEqual(MAXGOINT64);
|
||||
).toEqual(HALFMAXGOINT64);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,13 +7,11 @@ import {
|
||||
} from './positions-data-providers';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useT } from '../use-t';
|
||||
|
||||
// TODO: Close position temporarily disabled in https://github.com/vegaprotocol/frontend-monorepo/pull/5350
|
||||
//
|
||||
// import { useCallback } from 'react';
|
||||
// import * as Schema from '@vegaprotocol/types';
|
||||
// import { useVegaTransactionStore } from '@vegaprotocol/web3';
|
||||
// import { MAXGOINT64 } from '@vegaprotocol/utils';
|
||||
import { useCallback } from 'react';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { useVegaTransactionStore } from '@vegaprotocol/web3';
|
||||
import { HALFMAXGOINT64 } from '@vegaprotocol/utils';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
|
||||
interface PositionsManagerProps {
|
||||
partyIds: string[];
|
||||
@@ -32,37 +30,35 @@ export const PositionsManager = ({
|
||||
}: PositionsManagerProps) => {
|
||||
const t = useT();
|
||||
const { pubKeys, pubKey } = useVegaWallet();
|
||||
const create = useVegaTransactionStore((store) => store.create);
|
||||
const disableClosePositionsButton = FLAGS.DISABLE_CLOSE_POSITION;
|
||||
|
||||
// TODO: Close position temporarily disabled in https://github.com/vegaprotocol/frontend-monorepo/pull/5350
|
||||
//
|
||||
// const create = useVegaTransactionStore((store) => store.create);
|
||||
//
|
||||
// const onClose = useCallback(
|
||||
// ({ marketId, openVolume }: { marketId: string; openVolume: string }) =>
|
||||
// create({
|
||||
// batchMarketInstructions: {
|
||||
// cancellations: [
|
||||
// {
|
||||
// marketId,
|
||||
// orderId: '', // omit order id to cancel all active orders
|
||||
// },
|
||||
// ],
|
||||
// submissions: [
|
||||
// {
|
||||
// marketId: marketId,
|
||||
// type: Schema.OrderType.TYPE_MARKET as const,
|
||||
// timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC as const,
|
||||
// side: openVolume.startsWith('-')
|
||||
// ? Schema.Side.SIDE_BUY
|
||||
// : Schema.Side.SIDE_SELL,
|
||||
// size: MAXGOINT64, // improvement for avoiding leftovers filled in the meantime when close request has been sent
|
||||
// reduceOnly: true,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// }),
|
||||
// [create]
|
||||
// );
|
||||
const onClose = useCallback(
|
||||
({ marketId, openVolume }: { marketId: string; openVolume: string }) =>
|
||||
create({
|
||||
batchMarketInstructions: {
|
||||
cancellations: [
|
||||
{
|
||||
marketId,
|
||||
orderId: '', // omit order id to cancel all active orders
|
||||
},
|
||||
],
|
||||
submissions: [
|
||||
{
|
||||
marketId: marketId,
|
||||
type: Schema.OrderType.TYPE_MARKET as const,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC as const,
|
||||
side: openVolume.startsWith('-')
|
||||
? Schema.Side.SIDE_BUY
|
||||
: Schema.Side.SIDE_SELL,
|
||||
size: HALFMAXGOINT64, // improvement for avoiding leftovers filled in the meantime when close request has been sent
|
||||
reduceOnly: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
[create]
|
||||
);
|
||||
|
||||
const { data: marketIds } = useDataProvider({
|
||||
dataProvider: positionsMarketsProvider,
|
||||
@@ -81,8 +77,7 @@ export const PositionsManager = ({
|
||||
pubKeys={pubKeys}
|
||||
rowData={data}
|
||||
onMarketClick={onMarketClick}
|
||||
// TODO: temporarily disable close position
|
||||
// onClose={onClose}
|
||||
onClose={disableClosePositionsButton ? undefined : onClose}
|
||||
isReadOnly={isReadOnly}
|
||||
multipleKeys={partyIds.length > 1}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No positions')}
|
||||
|
||||
@@ -30,7 +30,7 @@ export const MarketProposalNotification = ({
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="border-default min-w-min whitespace-nowrap border-l pb-1 pl-1 pr-1">
|
||||
<div className="border-default min-w-min border-l pb-1 pl-1 pr-1">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
message={message}
|
||||
|
||||
@@ -37,8 +37,6 @@ export function Dialog({
|
||||
'w-full h-full'
|
||||
);
|
||||
const wrapperClasses = classNames(
|
||||
// Positions the modal in the center of screen
|
||||
'z-20 relative rounded top-[10vh]',
|
||||
// Dimensions
|
||||
'max-w-[90vw] p-4 md:p-8',
|
||||
// Need to apply background and text colors again as content is rendered in a portal
|
||||
@@ -72,27 +70,34 @@ export function Dialog({
|
||||
onInteractOutside={onInteractOutside}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
<div className={wrapperClasses}>
|
||||
{onChange && (
|
||||
<DialogPrimitives.Close
|
||||
className="absolute p-2 top-0 right-0 md:top-2 md:right-2"
|
||||
data-testid="dialog-close"
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CROSS} size={24} />
|
||||
</DialogPrimitives.Close>
|
||||
<div
|
||||
className={classNames(
|
||||
// Positions the modal in the center of screen
|
||||
'z-20 relative rounded top-[5vw] pb-[5vw] lg:top-[10vh] lg:pb-[10vh]'
|
||||
)}
|
||||
<div className="flex gap-4 max-w-full">
|
||||
{icon && <div className="fill-current">{icon}</div>}
|
||||
<div data-testid="dialog-content" className="flex-1 max-w-full">
|
||||
{title && (
|
||||
<h1
|
||||
className="text-xl uppercase mb-4 pr-2"
|
||||
data-testid="dialog-title"
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
)}
|
||||
<div>{children}</div>
|
||||
>
|
||||
<div className={wrapperClasses}>
|
||||
{onChange && (
|
||||
<DialogPrimitives.Close
|
||||
className="absolute p-2 top-0 right-0 md:top-2 md:right-2"
|
||||
data-testid="dialog-close"
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CROSS} size={24} />
|
||||
</DialogPrimitives.Close>
|
||||
)}
|
||||
<div className="flex gap-4 max-w-full">
|
||||
{icon && <div className="fill-current">{icon}</div>}
|
||||
<div data-testid="dialog-content" className="flex-1 max-w-full">
|
||||
{title && (
|
||||
<h1
|
||||
className="text-xl uppercase mb-4 pr-2"
|
||||
data-testid="dialog-title"
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
)}
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,7 +93,7 @@ export const Notification = ({
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col items-start overflow-hidden gap-0 mt-1',
|
||||
'flex flex-col items-start overflow-hidden gap-0',
|
||||
'text-vega-clight-50 dark:text-vega-cdark-50',
|
||||
'font-alpha',
|
||||
{ 'text-sm': size === 'small', 'text-base': size === 'medium' }
|
||||
|
||||
@@ -57,12 +57,20 @@ interface RadioProps {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TradingRadio = ({ id, value, label, disabled }: RadioProps) => {
|
||||
export const TradingRadio = ({
|
||||
id,
|
||||
value,
|
||||
label,
|
||||
disabled,
|
||||
className,
|
||||
}: RadioProps) => {
|
||||
const wrapperClasses = classNames(
|
||||
'flex items-center gap-1.5 text-xs',
|
||||
labelClasses
|
||||
labelClasses,
|
||||
className
|
||||
);
|
||||
const itemClasses = classNames(
|
||||
'flex justify-center items-center',
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
export const MAXGOINT64 = '9223372036854775807';
|
||||
// The fix for the close position functionality needs MaxInt64/2 for the size
|
||||
// Issue: https://github.com/vegaprotocol/vega/issues/10177
|
||||
// Core PR: https://github.com/vegaprotocol/vega/pull/10178
|
||||
export const HALFMAXGOINT64 = '4611686018427387903';
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
toBigNum,
|
||||
truncateByChars,
|
||||
useFormatTrigger,
|
||||
MAXGOINT64,
|
||||
HALFMAXGOINT64,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
||||
import { useEthWithdrawApprovalsStore } from './use-ethereum-withdraw-approvals-store';
|
||||
@@ -1014,7 +1014,7 @@ export const getVegaTransactionContentIntent = (tx: VegaStoredTxState) => {
|
||||
tx.order &&
|
||||
tx.order.status === Schema.OrderStatus.STATUS_STOPPED &&
|
||||
tx.order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC &&
|
||||
tx.order.size === MAXGOINT64 &&
|
||||
tx.order.size >= HALFMAXGOINT64 &&
|
||||
// isClosePositionTransaction(tx) &&
|
||||
Intent.Success;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from './__generated__/Erc20Approval';
|
||||
import {
|
||||
PendingWithdrawalFragmentDoc,
|
||||
PendingWithdrawalFragment,
|
||||
type PendingWithdrawalFragment,
|
||||
} from './__generated__/Withdrawal';
|
||||
|
||||
export const useCompleteWithdraw = () => {
|
||||
|
||||
Reference in New Issue
Block a user