Compare commits

...
Author SHA1 Message Date
Joe 4bc7b82ad1 chore: fix failing explorer market test 2023-10-26 10:45:05 +01:00
Joe 27edc446d3 chore: fix failing assertions 2023-10-26 10:37:22 +01:00
Matthew Russell 52c96794f7 feat(trading): fees page (#5055) 2023-10-25 14:59:30 -07:00
Ben 58ce016d4d chore(trading): suspend market test to console test (#5128) 2023-10-25 16:21:11 +01:00
Ben ac16eb06f5 chore(trading): delete fills cypress tests (#5122) 2023-10-25 14:02:43 +01:00
Ben 3319871617 chore(trading): delete dupe transfer tests (#5121) 2023-10-25 12:29:21 +00:00
Bartłomiej Głownia 37e850304a feat(trading): upgrade pennant@1.14.0 (#5120) 2023-10-25 12:44:23 +01:00
daro-maj 655131c744 test(trading): fix capsule test (#5116) 2023-10-25 12:39:18 +01:00
m.ray f1cb1b9408 fix(trading): quote base unit for size (#5124) 2023-10-25 12:36:57 +01:00
m.ray 891ac527a2 feat(trading): add tooltips for iceberg and post or reduce only (#5119) 2023-10-25 12:18:52 +01:00
Art 546087fb9f chore(trading): referrals with sidebar (#5117) 2023-10-25 11:14:00 +02:00
BenandMatthew Russell f7a82d33ba chore(trading): withdraw tests to jest (#5109)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-10-25 08:45:58 +01:00
Matthew Russell d85f413e41 chore(trading,positions): update estimatePosition query to use market decimals for liquidation price (#5005) 2023-10-24 17:30:17 -07:00
Edd c7d0025b4f feat(explorer): add determinsitic id to transfer tx (#5112) 2023-10-24 16:48:17 +01:00
Art 6c9272cd53 chore(trading): i18n-friendly referrals (#5113) 2023-10-24 17:43:33 +02:00
m.rayandasiaznik 82fb29d541 feat(trading): add liquidity provision new SLA parameters to liquidity tables (#5032)
Co-authored-by: asiaznik <artur@vegaprotocol.io>
2023-10-24 10:32:07 +01:00
70 changed files with 2305 additions and 1010 deletions
@@ -39,7 +39,6 @@ context('Market page', { tags: '@regression' }, function () {
cy.contains('Test market 1').click();
cy.getByTestId(marketHeaders).should('have.text', 'Test market 1');
cy.validate_element_from_table('Name', 'Test market 1');
cy.validate_element_from_table('Market ID', this.createdMarketId);
cy.validate_element_from_table('Trading Mode', 'Opening auction');
cy.validate_element_from_table('Market Decimal Places', '5');
cy.validate_element_from_table('Position Decimal Places', '5');
@@ -12,6 +12,7 @@ import {
SPECIAL_CASE_NETWORK,
SPECIAL_CASE_NETWORK_ID,
} from '../../links/party-link/party-link';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
type Transfer = components['schemas']['commandsv1Transfer'];
@@ -63,10 +64,16 @@ export const TxDetailsTransfer = ({
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableRow modifier="bordered">
<TableRow modifier="bordered" data-testid="type">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{getTypeLabelForTransfer(transfer)}</TableCell>
</TableRow>
<TableRow modifier="bordered" data-testid="id">
<TableCell {...sharedHeaderProps}>{t('Transfer ID')}</TableCell>
<TableCell>
{txSignatureToDeterministicId(txData.signature.value)}
</TableCell>
</TableRow>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -74,7 +81,7 @@ export const TxDetailsTransfer = ({
hideTypeRow={true}
/>
{from ? (
<TableRow modifier="bordered">
<TableRow modifier="bordered" data-testid="from">
<TableCell>{t('From')}</TableCell>
<TableCell>
<PartyLink id={from} />
@@ -82,7 +89,7 @@ export const TxDetailsTransfer = ({
</TableRow>
) : null}
{transfer.to ? (
<TableRow modifier="bordered">
<TableRow modifier="bordered" data-testid="to">
<TableCell>{t('To')}</TableCell>
<TableCell>
<PartyLink id={transfer.to} />
@@ -90,7 +97,7 @@ export const TxDetailsTransfer = ({
</TableRow>
) : null}
{transfer.asset && transfer.amount ? (
<TableRow modifier="bordered">
<TableRow modifier="bordered" data-testid="amount">
<TableCell>{t('Amount')}</TableCell>
<TableCell>
<SizeInAsset assetId={transfer.asset} size={transfer.amount} />
@@ -1,6 +1,15 @@
import { getTypeLabelForTransfer } from './details/tx-transfer';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../routes/blocks/tendermint-blocks-response';
import type { components } from '../../../types/explorer';
import {
TxDetailsTransfer,
getTypeLabelForTransfer,
} from './details/tx-transfer';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
import { render } from '@testing-library/react';
type Transfer = components['schemas']['commandsv1Transfer'];
describe('TX: Transfer: getLabelForTransfer', () => {
@@ -56,3 +65,70 @@ describe('TX: Transfer: getLabelForTransfer', () => {
expect(getTypeLabelForTransfer(mock)).toEqual('Transfer');
});
});
describe('TxDetailsTransfer', () => {
const mockBlockData = {
result: {
block: {
header: {
height: '123',
},
},
},
};
const mockTxData: Partial<BlockExplorerTransactionResult> = {
hash: 'test',
submitter:
'e1943eea46fed576cf2be42972f3c5515ad3d0ac7ac013f56677c12a53a1b3ed',
command: {
nonce: '5188810881378065222',
blockHeight: '14951513',
transfer: {
fromAccountType: 'ACCOUNT_TYPE_GENERAL',
to: '78432a2808f20b18a46ccc6a917bdc4d63c2b9e7007f777bdcab5a9f462c5ba6',
toAccountType: 'ACCOUNT_TYPE_GENERAL',
asset:
'dd20590509d30d20bdbbe64dc1090c1140c7690121a9b9940bc66f62dfa2e599',
amount: '4800000000',
reference: '',
oneOff: {
deliverOn: '0',
},
},
},
signature: {
value:
'610c2e196a7d4fed4413b9e82af267b1ff3e30e943df3a3d28096fd60604d430d752fbaf6dd4f84d496be78885bb6118f40560bff7832c06bd7a3d67b718b700',
},
};
it('renders basic transfer details', () => {
const { getByTestId } = render(
<MockedProvider>
<MemoryRouter>
<TxDetailsTransfer
txData={mockTxData as BlockExplorerTransactionResult}
pubKey={mockTxData.command.submitter}
blockData={mockBlockData as TendermintBlocksResponse}
/>
</MemoryRouter>
</MockedProvider>
);
const id = getByTestId('id');
expect(id.children[0].textContent).toEqual('Transfer ID');
expect(id.children[1].textContent).toEqual(
'51f3bab5eb2637651012507a64d497790a734248792c16e5cf36df8984074fbd'
);
const type = getByTestId('type');
expect(type.children[1].textContent).toEqual('Transfer');
const from = getByTestId('from');
expect(from.children[1].textContent).toEqual(mockTxData.submitter);
const to = getByTestId('to');
expect(to.children[1].textContent).toEqual(mockTxData.command.transfer.to);
});
});
@@ -537,10 +537,10 @@ describe(
cy.VegaWalletSubmitProposal(createGovernanceTransferProposalTxBody());
cy.reload();
getProposalFromTitle('Governance transfer proposal').within(() => {
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
cy.getByTestId(marketProposalType).should('have.text', 'New transfer');
cy.getByTestId(viewProposalButton).click();
});
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
cy.getByTestId(marketProposalType).should('have.text', 'New transfer');
cy.getByTestId(governanceTransferToggle).click();
cy.getByTestId('proposal-transfer-details-table').within(() => {
getProposalInformationFromTable('Source Type')
@@ -590,7 +590,7 @@ describe(
);
cy.getByTestId(viewProposalButton).click();
});
cy.getByTestId(marketProposalType).should('have.text', 'CancelTransfer');
cy.getByTestId(marketProposalType).should('have.text', 'Cancel transfer');
getProposalInformationFromTable('Error details')
.invoke('text')
.and('eq', 'Governance transfer invalid transfer id not found');
@@ -128,9 +128,7 @@ describe('<ProposalReferralProgramDetails />', () => {
it('should not render if there are no relevant fields', () => {
const incompleteProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateReferralProgram',
},
change: {},
},
});
+13 -13
View File
@@ -48,8 +48,8 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
cy.get('@markets').then((markets) => {
cy.wrap(markets[0]).as('market');
});
cy.setOnBoardingViewed();
cy.visit('/#/portfolio');
cy.connectVegaWallet();
});
it('can deposit', function () {
@@ -70,6 +70,8 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
cy.getByTestId('deposit-button').click();
connectEthereumWallet('Unknown');
selectAsset(btcName);
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
cy.getByTestId('approve-default').should(
'contain.text',
`Before you can make a deposit of your chosen asset, ${btcSymbol}, you need to approve its use in your Ethereum wallet`
@@ -120,7 +122,7 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
cy.getByTestId(collateralTab).click();
cy.getByTestId('open-transfer').click();
cy.getByTestId('open-transfer').eq(1).click();
cy.getByTestId('transfer-form').should('be.visible');
cy.getByTestId('transfer-form').find('[name="toAddress"]').select(1);
cy.get('select option')
@@ -147,7 +149,8 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
// 0003-WTXN-011
cy.getByTestId('Withdrawals').click();
cy.getByTestId('withdraw-dialog-button').click();
selectAsset(0);
selectAsset(btcName);
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
cy.get(amountField).focus();
cy.get(amountField).clear().type('1');
cy.getByTestId('submit-withdrawal').click();
@@ -180,24 +183,21 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
cy.get('@markets').then((markets) => {
cy.wrap(markets[0]).as('market');
});
cy.setOnBoardingViewed();
cy.setVegaWallet();
});
it('shows node health', function () {
// 0006-NETW-010
const regex = /^Operational\d+$/;
const market = this.market;
cy.visit(`/#/markets/${market.id}`);
cy.getByTestId('node-health-trigger').realHover();
cy.getByTestId('node-health')
.children()
.first()
.should('contain.text', 'Operational')
.then(($el) => {
const blockHeight = parseInt($el.text());
// block height will increase over the course of the test run so best
// we can do here is check that its showing something sensible
expect(blockHeight).to.be.greaterThan(0);
});
.invoke('text')
.should('match', regex);
cy.getByTestId('node-health')
.children()
.eq(1)
@@ -239,7 +239,6 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
.should('contain.text', order.size);
cy.getByTestId(openOrdersTab).click();
cy.getByTestId('edit', txTimeout).should('contain.text', 'Edit');
cy.getByTestId('tab-open-orders').within(() => {
cy.get('.ag-center-cols-container')
.children()
@@ -280,8 +279,7 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
cy.visit(`/#/markets/${market.id}`);
cy.getByTestId(toastCloseBtn, txTimeout).click();
cy.getByTestId(openOrdersTab).click();
cy.getByTestId('edit', txTimeout).should('be.visible');
cy.getByTestId('edit').first().should('be.visible').click();
cy.getByTestId('edit').first().click();
cy.getByTestId('dialog-title').should('contain.text', 'Edit order');
cy.get('#limitPrice').focus().clear().type(newPrice);
cy.getByTestId('edit-order').find('[type="submit"]').click();
@@ -350,6 +348,7 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
cy.getByTestId('withdraw-dialog-button').click();
connectEthereumWallet('Unknown');
selectAsset(btcName);
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
cy.get(amountField).clear().type('1');
cy.getByTestId('submit-withdrawal').click();
cy.getByTestId(toastContent, txTimeout).should(
@@ -437,6 +436,7 @@ describe('capsule', { tags: '@slow', testIsolation: true }, () => {
cy.getByTestId('deposit-button').click();
connectEthereumWallet('Unknown');
selectAsset(btcName);
cy.get('[data-testid="rich-select-option"]').eq(btcName).click();
cy.contains('Deposits of tBTC not approved').should('not.exist');
cy.contains('Use maximum').should('be.visible');
cy.get(amountField).clear().type('20000000');
@@ -69,7 +69,7 @@ describe(
cy.contains('Something went wrong').should('not.exist');
cy.contains('Application error').should('not.exist');
cy.getByTestId('tab-liquidity').within(() => {
cy.get('[col-id="party.id"]').eq(1).should('not.be.empty');
cy.get('[col-id="partyId"]').eq(1).should('not.be.empty');
});
});
}
@@ -12,8 +12,7 @@ const marketSummaryBlock = 'header-summary';
const itemValue = 'item-value';
const itemHeader = 'item-header';
const colCommitmentAmount = '[col-id="commitmentAmount"]';
const colAverageEntryValuation = '[col-id="averageEntryValuation"]';
const colEquityLikeShare = '[col-id="equityLikeShare"]';
const colEquityLikeShare = '[col-id="feeShare.equityLikeShare"]';
const colFee = '[col-id="fee"]';
const colCommitmentAmount_1 = '[col-id="commitmentAmount_1"]';
const colBalance = '[col-id="balance"]';
@@ -24,11 +23,16 @@ const colUpdatedAt = '[col-id="updatedAt"] button';
const headers = [
'Party',
'Commitment (tDAI)',
'Share',
'Proposed fee',
'Market valuation at entry',
'Obligation',
'Supplied',
'Fee',
'Adjusted stake share',
'Share',
'Live supplied liquidity',
'Live time fraction on book',
'Live liquidity quality score (%)',
'Last time fraction on the book',
'Last fee penalty',
'Last bond penalty',
'Status',
'Created',
'Updated',
@@ -64,11 +68,8 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
// 5002-LIQP-002
cy.get(rowSelector)
.first()
.find('[col-id="party.id"]')
.should(
'have.text',
'69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
);
.find('[col-id="partyId"]')
.should('have.text', '69464e…dc6f');
cy.get(rowSelector)
.first()
@@ -82,12 +83,6 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
cy.get(rowSelector).first().find(colFee).should('have.text', '0.09%');
// 5002-LIQP-013
cy.get(rowSelector)
.first()
.find(colAverageEntryValuation)
.should('have.text', '685,852.93692');
cy.get(rowSelector)
.first()
.find(colCommitmentAmount_1)
@@ -104,7 +99,8 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
cy.get(rowSelector).first().find(colCreatedAt).should('not.be.empty');
cy.get(rowSelector).first().find(colUpdatedAt).should('not.be.empty');
});
it.skip('liquidity status column should be sorted properly', () => {
it('liquidity status column should be sorted properly', () => {
// 5002-LIQP-003
const liquidityColDefault = ['Active', 'Pending'];
const liquidityColAsc = ['Active', 'Pending'];
@@ -220,11 +216,8 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
// 5002-LIQP-011
cy.get(rowSelectorLiquidityActive)
.first()
.find('[col-id="party.id"]')
.should(
'have.text',
'69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
);
.find('[col-id="partyId"]')
.should('have.text', '69464e…dc6f');
cy.get(rowSelectorLiquidityActive)
.first()
@@ -241,12 +234,6 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
.find(colFee)
.should('have.text', '0.09%');
// 5002-LIQP-013
cy.get(rowSelectorLiquidityActive)
.first()
.find(colAverageEntryValuation)
.should('have.text', '685,852.93692');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colCommitmentAmount_1)
@@ -277,11 +264,8 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
cy.getByTestId('Inactive').click();
cy.get(rowSelectorLiquidityInactive)
.first()
.find('[col-id="party.id"]')
.should(
'have.text',
'cc464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
);
.find('[col-id="partyId"]')
.should('have.text', 'cc464e…dc6f');
cy.get(rowSelectorLiquidityInactive)
.first()
@@ -298,11 +282,6 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
.find(colFee)
.should('have.text', '0.40%');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colAverageEntryValuation)
.should('have.text', '685,852.93692');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colCommitmentAmount_1)
@@ -1,70 +0,0 @@
import * as Schema from '@vegaprotocol/types';
import {
TIFlist,
orderPriceField,
orderSizeField,
orderTIFDropDown,
placeOrderBtn,
toggleLimit,
toggleMarket,
} from '../support/deal-ticket';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { accountsQuery } from '@vegaprotocol/mock';
describe('suspended market validation', { tags: '@regression' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage(
Schema.MarketState.STATE_SUSPENDED,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
const accounts = accountsQuery();
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
beforeEach(() => {
cy.setVegaWallet();
});
it('should show warning for market order', function () {
cy.getByTestId(toggleMarket).click();
// 7002-SORD-060
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('deal-ticket-error-message-type').should(
'have.text',
'This market is in auction until it reaches sufficient liquidity. Only limit orders are permitted when market is in auction'
);
});
it('should show info for allowed TIF', function () {
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('0.1');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('deal-ticket-warning-auction').should(
'have.text',
'Any orders placed now will not trade until the auction ends'
);
});
it('should show warning for not allowed TIF', function () {
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderTIFDropDown).select(
TIFlist.filter((item) => item.code === 'FOK')[0].value
);
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('deal-ticket-error-message-tif').should(
'have.text',
'This market is in auction until it reaches sufficient liquidity. Until the auction ends, you can only place GFA, GTT, or GTC limit orders'
);
});
});
@@ -1,110 +0,0 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { fillsQuery } from '@vegaprotocol/mock';
const tabFills = 'tab-fills';
describe('fills', { tags: '@regression' }, () => {
// 7005-FILL-001
// 7005-FILL-002
// 7005-FILL-003
// 7005-FILL-004
// 7005-FILL-005
// 7005-FILL-006
// 7005-FILL-007
// 7005-FILL-008
beforeEach(() => {
// Ensure page loads with correct key
cy.window().then((window) => {
cy.wrap(
window.localStorage.setItem(
'vega_wallet_key',
Cypress.env('VEGA_PUBLIC_KEY')
)
);
});
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockGQL((req) => {
aliasGQLQuery(
req,
'Fills',
fillsQuery({}, Cypress.env('VEGA_PUBLIC_KEY'))
);
});
cy.mockSubscription();
});
it('renders fills on portfolio page', () => {
cy.visit('/#/portfolio');
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
cy.getByTestId('Fills').click();
validateFillsDisplayed();
});
it('renders fills on trading tab', () => {
cy.visit('/#/markets/market-0');
cy.getByTestId('Fills').click();
validateFillsDisplayed();
});
function validateFillsDisplayed() {
cy.getByTestId(tabFills).should('be.visible');
cy.getByTestId(tabFills).contains('Market');
cy.getByTestId(tabFills)
.get(
'[role="gridcell"][col-id="market.tradableInstrument.instrument.code"]'
)
.each(($marketSymbol) => {
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
});
cy.getByTestId(tabFills).contains('Size');
cy.get(`[col-id='size']`).eq(1).should('contain.text', '+');
cy.get(`[col-id='size']`).eq(2).should('contain.text', '-');
cy.getByTestId(tabFills)
.get('[role="gridcell"][col-id="size"]')
.each(($amount) => {
cy.wrap($amount).invoke('text').should('not.be.empty');
});
cy.getByTestId(tabFills).contains('Price');
cy.getByTestId(tabFills)
.get('[role="gridcell"][col-id="price"]')
.each(($prices) => {
cy.wrap($prices).invoke('text').should('not.be.empty');
});
cy.getByTestId(tabFills).contains('Notional');
cy.getByTestId(tabFills)
.get('[role="gridcell"][col-id="price_1"]')
.each(($total) => {
cy.wrap($total).invoke('text').should('not.be.empty');
});
cy.getByTestId(tabFills).contains('Role');
cy.getByTestId(tabFills)
.get('[role="gridcell"][col-id="aggressor"]')
.each(($role) => {
cy.wrap($role)
.invoke('text')
.then((text) => {
const roles = ['Maker', 'Taker', '-'];
expect(roles.indexOf(text.trim())).to.be.greaterThan(-1);
});
});
cy.getByTestId(tabFills).contains('Fee');
cy.getByTestId(tabFills)
.get(
'[role="gridcell"][col-id="market.tradableInstrument.instrument.product"]'
)
.each(($fees) => {
cy.wrap($fees).invoke('text').should('not.be.empty');
});
cy.getByTestId(tabFills).contains('Date');
const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
cy.get('[col-id="createdAt"]').each(($tradeDateTime, index) => {
if (index != 0) {
//ignore header
cy.wrap($tradeDateTime).invoke('text').should('match', dateTimeRegex);
}
});
}
});
@@ -1,119 +0,0 @@
import { selectAsset } from '../support/helpers';
const amountField = 'input[name="amount"]';
const includeTransferFeeRadioBtn = 'include-transfer-fee';
const manageVegaWallet = 'manage-vega-wallet';
const toAddressField = '[name="toAddress"]';
const totalTransferfee = 'total-transfer-fee';
const transferAmount = 'transfer-amount';
const transferForm = 'transfer-form';
const transferFee = 'transfer-fee';
const walletTransfer = 'wallet-transfer';
const ASSET_SEPOLIA_TBTC = 2;
describe.skip(
'transfer fees',
{ tags: '@regression', testIsolation: true },
() => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Assets');
cy.wait('@Accounts');
cy.mockVegaWalletTransaction();
});
it('transfer fees tooltips', () => {
// 1003-TRAN-015
// 1003-TRAN-016
// 1003-TRAN-017
// 1003-TRAN-018
// 1003-TRAN-019
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type(
'7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535'
);
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
/// Check Include Transfer Fee tooltip
cy.get('label[for="include-transfer-fee"] div').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Transfer Fee tooltip
cy.contains('div', 'Transfer fee').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Amount to be transferred tooltip
cy.contains('div', 'Amount to be transferred').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Total amount (with fee) tooltip
cy.contains('div', 'Total amount (with fee)').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
});
it('transfer fees', () => {
// 1003-TRAN-020
// 1003-TRAN-021
// 1003-TRAN-022
// 1003-TRAN-023
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type(
'7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535'
);
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(includeTransferFeeRadioBtn).should('be.disabled');
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '1.00');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.01');
cy.getByTestId(includeTransferFeeRadioBtn).click();
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '0.99');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.00');
});
}
);
@@ -1,131 +0,0 @@
import { connectEthereumWallet } from '../support/ethereum-wallet';
import { selectAsset } from '../support/helpers';
const formFieldError = 'input-error-text';
const toAddressField = 'input[name="to"]';
const amountField = 'input[name="amount"]';
const useMaximumAmount = 'use-maximum';
const submitWithdrawBtn = 'submit-withdrawal';
const ethAddressValue = Cypress.env('ETHEREUM_WALLET_ADDRESS');
const ASSET_SEPOLIA_TBTC = 2;
const ASSET_EURO = 1;
describe('withdraw form validation', { tags: '@smoke' }, () => {
before(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.getByTestId('Withdrawals').click();
cy.getByTestId('Withdraw').click(); // sidebar item
// It also requires connection Ethereum wallet
connectEthereumWallet('MetaMask');
cy.wait('@Accounts');
cy.wait('@Assets');
});
it('empty fields', () => {
cy.getByTestId(submitWithdrawBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
// only 2 despite 3 fields because the ethereum address will be auto populated
cy.getByTestId(formFieldError).should('have.length', 2);
// Test for Ethereum address
cy.get(toAddressField).should('have.value', ethAddressValue);
});
it('min amount', () => {
// 1002-WITH-010
selectAsset(ASSET_SEPOLIA_TBTC);
cy.get(amountField).clear().type('0');
cy.getByTestId(submitWithdrawBtn).click();
cy.get('[data-testid="input-error-text"]').should(
'contain.text',
'Value is below minimum'
);
});
it('max amount', () => {
// 1002-WITH-005
// 1002-WITH-008
selectAsset(ASSET_EURO); // Will be above maximum because the vega wallet doesn't have any collateral
cy.get(amountField).clear().type('1001', { delay: 100 });
cy.getByTestId(submitWithdrawBtn).click();
cy.get('[data-testid="input-error-text"]').should(
'contain.text',
'Insufficient amount in account'
);
});
it('can set amount using use maximum button', () => {
// 1002-WITH-004
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(useMaximumAmount).click();
cy.get(amountField).should('have.value', '1000.00001');
});
});
describe(
'withdraw actions',
{ tags: '@regression', testIsolation: true },
() => {
// this is extremely ugly hack, but setting it properly in contract is too much effort for such simple validation
// 1002-WITH-018
const withdrawalThreshold =
Cypress.env('VEGA_ENV') === 'CUSTOM' ? '0.00' : '100.00';
before(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.wait('@Accounts');
cy.wait('@Assets');
cy.getByTestId('Withdrawals').click();
cy.getByTestId('Withdraw').click();
// It also requires connection Ethereum wallet
connectEthereumWallet('MetaMask');
cy.mockVegaWalletTransaction();
});
it('triggers transaction when submitted', () => {
// 1002-WITH-002
// 1002-WITH-003
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId('BALANCE_AVAILABLE_label').should(
'contain.text',
'Balance available'
);
cy.getByTestId('BALANCE_AVAILABLE_value').should(
'have.text',
'1,000.00001'
);
cy.getByTestId('WITHDRAWAL_THRESHOLD_label').should(
'contain.text',
'Delayed withdrawal threshold'
);
cy.getByTestId('WITHDRAWAL_THRESHOLD_value').should(
'contain.text',
withdrawalThreshold
);
cy.getByTestId('DELAY_TIME_label').should('contain.text', 'Delay time');
cy.getByTestId('DELAY_TIME_value').should('have.text', 'None');
cy.get(amountField).clear().type('10');
cy.getByTestId(submitWithdrawBtn).click();
cy.getByTestId('toast').should('contain.text', 'Awaiting confirmation');
});
}
);
+2 -6
View File
@@ -30,11 +30,11 @@ import {
blockStatisticsQuery,
networkParamQuery,
liquidityProvisionsQuery,
liquidityProviderFeeShareQuery,
successorMarketQuery,
parentMarketIdQuery,
successorMarketIdsQuery,
successorMarketProposalDetailsQuery,
liquidityProvidersQuery,
} from '@vegaprotocol/mock';
import type { PartialDeep } from 'type-fest';
import type { MarketDataQuery, MarketsQuery } from '@vegaprotocol/markets';
@@ -162,11 +162,7 @@ const mockTradingPage = (
aliasGQLQuery(req, 'Trades', tradesQuery());
aliasGQLQuery(req, 'Chart', chartQuery());
aliasGQLQuery(req, 'LiquidityProvisions', liquidityProvisionsQuery());
aliasGQLQuery(
req,
'LiquidityProviderFeeShare',
liquidityProviderFeeShareQuery
);
aliasGQLQuery(req, 'LiquidityProviders', liquidityProvidersQuery());
aliasGQLQuery(req, 'Candles', candlesQuery());
aliasGQLQuery(req, 'Withdrawals', withdrawalsQuery());
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
+6 -6
View File
@@ -3,21 +3,21 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
NX_VEGA_ENV=TESTNET
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.stagnet1.vega.rocks
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+11
View File
@@ -0,0 +1,11 @@
import { t } from '@vegaprotocol/i18n';
import { FeesContainer } from '../../components/fees-container';
export const Fees = () => {
return (
<div className="container p-4 mx-auto">
<h1 className="px-4 pb-4 text-2xl">{t('Fees')}</h1>
<FeesContainer />
</div>
);
};
+1
View File
@@ -0,0 +1 @@
export { Fees } from './fees';
@@ -32,7 +32,7 @@ const WithdrawalsIndicator = () => {
return null;
}
return (
<span className="bg-vega-clight-500 dark:bg-vega-cdark-500 text-default rounded p-1 leading-none">
<span className="p-1 leading-none rounded bg-vega-clight-500 dark:bg-vega-cdark-500 text-default">
{ready.length}
</span>
);
@@ -128,7 +128,7 @@ interface PortfolioGridChildProps {
const PortfolioGridChild = ({ children }: PortfolioGridChildProps) => {
return (
<section className="h-full p-1">
<div className="border border-default h-full rounded-sm">{children}</div>
<div className="h-full border rounded-sm border-default">{children}</div>
</section>
);
};
@@ -81,7 +81,7 @@ export const ApplyCodeForm = () => {
if (!res) {
setError('code', {
type: 'required',
message: 'The transaction could not be sent',
message: t('The transaction could not be sent'),
});
}
if (res) {
@@ -98,7 +98,7 @@ export const ApplyCodeForm = () => {
message:
err instanceof Error
? err.message
: 'Your code has been rejected',
: t('Your code has been rejected'),
});
}
});
@@ -152,7 +152,7 @@ export const ApplyCodeForm = () => {
<span className="text-vega-green-500">
<VegaIcon name={VegaIconNames.TICK} size={20} />
</span>{' '}
<span className="pt-1">Code applied</span>
<span className="pt-1">{t('Code applied')}</span>
</h3>
</div>
);
@@ -162,7 +162,7 @@ export const ApplyCodeForm = () => {
if (!pubKey) {
return {
disabled: false,
children: 'Connect wallet',
children: t('Connect wallet'),
type: 'button' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
onClick: ((event) => {
event.preventDefault();
@@ -174,7 +174,7 @@ export const ApplyCodeForm = () => {
if (isReadOnly) {
return {
disabled: true,
children: 'Apply a code',
children: t('Apply a code'),
type: 'submit' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
};
}
@@ -182,14 +182,14 @@ export const ApplyCodeForm = () => {
if (status === 'requested') {
return {
disabled: true,
children: 'Confirm in wallet...',
children: t('Confirm in wallet...'),
type: 'submit' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
};
}
return {
disabled: false,
children: 'Apply a code',
children: t('Apply a code'),
type: 'submit' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
};
};
@@ -198,10 +198,10 @@ export const ApplyCodeForm = () => {
<>
<div className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg">
<h3 className="mb-4 text-2xl text-center calt">
Apply a referral code
{t('Apply a referral code')}
</h3>
<p className="mb-4 text-center text-base">
Enter a referral code to get trading discounts.
{t('Enter a referral code to get trading discounts.')}
</p>
<form
className={classNames('w-full flex flex-col gap-4', {
@@ -210,11 +210,11 @@ export const ApplyCodeForm = () => {
onSubmit={handleSubmit(onSubmit)}
>
<label>
<span className="sr-only">Your referral code</span>
<span className="sr-only">{t('Your referral code')}</span>
<Input
hasError={Boolean(errors.code)}
{...register('code', {
required: 'You have to provide a code to apply it.',
required: t('You have to provide a code to apply it.'),
validate: validateCode,
})}
placeholder="Enter a code"
@@ -236,7 +236,7 @@ export const ApplyCodeForm = () => {
) : null}
{previewData ? (
<div className="mt-10">
<h2 className="text-2xl mb-5">You are joining</h2>
<h2 className="text-2xl mb-5">{t('You are joining')}</h2>
<Statistics data={previewData} as="referee" />
</div>
) : null}
@@ -24,6 +24,7 @@ import {
DISCLAIMER_REFERRAL_DOCS_LINK,
} from './constants';
import { useReferral } from './hooks/use-referral';
import { t } from '@vegaprotocol/i18n';
export const CreateCodeContainer = () => {
return <CreateCodeForm />;
@@ -38,10 +39,13 @@ export const CreateCodeForm = () => {
return (
<div className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg">
<h3 className="mb-4 text-2xl text-center calt">Create a referral code</h3>
<h3 className="mb-4 text-2xl text-center calt">
{t('Create a referral code')}
</h3>
<p className="mb-4 text-center text-base">
Generate a referral code to share with your friends and start earning
commission.
{t(
'Generate a referral code to share with your friends and start earning commission.'
)}
</p>
<div className="w-full flex flex-col">
@@ -56,12 +60,12 @@ export const CreateCodeForm = () => {
}
}}
>
{pubKey ? 'Create a referral code' : 'Connect wallet'}
{pubKey ? t('Create a referral code') : t('Connect wallet')}
</RainbowButton>
</div>
<Dialog
title="Create a referral code"
title={t('Create a referral code')}
open={dialogOpen}
onChange={() => setDialogOpen(false)}
size="small"
@@ -124,21 +128,21 @@ const CreateCodeDialog = ({
const getButtonProps = () => {
if (status === 'idle' || status === 'error') {
return {
children: 'Generate code',
children: t('Generate code'),
onClick: () => onSubmit(),
};
}
if (status === 'loading') {
return {
children: 'Confirm in wallet...',
children: t('Confirm in wallet...'),
disabled: true,
};
}
if (status === 'success') {
return {
children: 'Close',
children: t('Close'),
intent: Intent.Success,
onClick: () => {
refetch();
@@ -151,12 +155,12 @@ const CreateCodeDialog = ({
if (!pubKey || currentStakeAvailable == null || requiredStake == null) {
return (
<div className="flex flex-col gap-4">
<p>You must be connected to the Vega wallet.</p>
<p>{t('You must be connected to the Vega wallet.')}</p>
<TradingButton
intent={Intent.Primary}
onClick={() => setDialogOpen(false)}
>
Close
{t('Close')}
</TradingButton>
</div>
);
@@ -166,16 +170,18 @@ const CreateCodeDialog = ({
return (
<div className="flex flex-col gap-4">
<p>
You need at least{' '}
{addDecimalsFormatNumber(requiredStake.toString(), 18)} VEGA staked to
generate a referral code and participate in the referral program.
{t('You need at least')}{' '}
{addDecimalsFormatNumber(requiredStake.toString(), 18)}{' '}
{t(
'VEGA staked to generate a referral code and participate in the referral program.'
)}
</p>
<TradingAnchorButton
href={createLink(TokenStaticLinks.ASSOCIATE)}
intent={Intent.Primary}
target="_blank"
>
Stake some $VEGA now
{t('Stake some $VEGA now')}
</TradingAnchorButton>
</div>
);
@@ -185,8 +191,9 @@ const CreateCodeDialog = ({
<div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && (
<p>
Generate a referral code to share with your friends and start earning
commission.
{t(
'Generate a referral code to share with your friends and start earning commission.'
)}
</p>
)}
{status === 'success' && code && (
@@ -201,7 +208,7 @@ const CreateCodeDialog = ({
className="text-sm no-underline"
icon={<VegaIcon name={VegaIconNames.COPY} />}
>
<span>Copy</span>
<span>{t('Copy')}</span>
</TradingButton>
</CopyWithTooltip>
</div>
@@ -214,10 +221,10 @@ const CreateCodeDialog = ({
{err && <InputError>{err}</InputError>}
<div className="flex justify-center pt-5 mt-2 text-sm border-t gap-4 text-default border-default">
<ExternalLink href={ABOUT_REFERRAL_DOCS_LINK}>
About the referral program
{t('About the referral program')}
</ExternalLink>
<ExternalLink href={DISCLAIMER_REFERRAL_DOCS_LINK}>
Disclaimer
{t('Disclaimer')}
</ExternalLink>
</div>
</div>
@@ -3,6 +3,7 @@ import { RainbowButton } from './buttons';
import { AnimatedDudeWithWire } from './graphics/dude';
import { LayoutWithSky } from './layout';
import { Routes } from '../../lib/links';
import { t } from '@vegaprotocol/i18n';
export const ErrorBoundary = () => {
const error = useRouteError();
@@ -39,7 +40,7 @@ export const ErrorBoundary = () => {
variant="border"
className="text-xs"
>
Go back and try again
{t('Go back and try again')}
</RainbowButton>
</p>
</LayoutWithSky>
@@ -60,7 +61,7 @@ export const NotFound = () => {
<h1 className="text-6xl font-alpha calt mb-10">{'Not found'}</h1>
<p className="text-lg mb-10">
{"The page you're looking for doesn't exists."}
{t("The page you're looking for doesn't exists.")}
</p>
<p className="text-lg mb-10">
@@ -69,7 +70,7 @@ export const NotFound = () => {
variant="border"
className="text-xs"
>
Go back and try again
{t('Go back and try again')}
</RainbowButton>
</p>
</div>
@@ -1,3 +1,4 @@
import { t } from '@vegaprotocol/i18n';
import { Table } from './table';
export const HowItWorksTable = () => (
@@ -13,7 +14,9 @@ export const HowItWorksTable = () => (
1
</span>
),
step: 'Referrers generate a code assigned to their key via an on chain transaction',
step: t(
'Referrers generate a code assigned to their key via an on chain transaction'
),
},
{
number: (
@@ -21,7 +24,9 @@ export const HowItWorksTable = () => (
2
</span>
),
step: 'Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction',
step: t(
'Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction'
),
},
{
number: (
@@ -29,7 +34,9 @@ export const HowItWorksTable = () => (
3
</span>
),
step: 'Discounts are applied automatically during trading based on the key(s) used',
step: t(
'Discounts are applied automatically during trading based on the key(s) used'
),
},
{
number: (
@@ -37,7 +44,9 @@ export const HowItWorksTable = () => (
4
</span>
),
step: 'Referrers earn commission based on a percentage of the taker fees their referees pay',
step: t(
'Referrers earn commission based on a percentage of the taker fees their referees pay'
),
},
{
number: (
@@ -45,7 +54,9 @@ export const HowItWorksTable = () => (
5
</span>
),
step: 'The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee',
step: t(
'The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee'
),
},
]}
></Table>
@@ -1,5 +1,6 @@
import classNames from 'classnames';
import { AnimatedDudeWithWire } from './graphics/dude';
import { t } from '@vegaprotocol/i18n';
export const LandingBanner = () => {
return (
@@ -7,22 +8,18 @@ export const LandingBanner = () => {
<div className="">
<div
aria-hidden
className="absolute top-64 right-[220px] md:right-[340px] max-sm:hidden"
className="absolute top-20 right-[120px] md:right-[240px] max-sm:hidden"
>
<AnimatedDudeWithWire />
</div>
<div className="pt-32 sm:w-[50%]">
<div className="pt-20 sm:w-[50%]">
<h1 className="text-6xl font-alpha calt mb-10">
Earn commission & stake rewards
{t('Earn commission & stake rewards')}
</h1>
<p className="text-lg mb-10">
Invite friends and earn commission in the form of Vega rewards from
the trading fees they pay. Stake those rewards to earn multipliers
on future rewards.
</p>
<p className="text-lg">
Any friends that join using the code will receive discounts off
trading fees.
{t(
'Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.'
)}
</p>
</div>
</div>
@@ -2,6 +2,7 @@ import classNames from 'classnames';
import type { HTMLAttributes } from 'react';
import { SKY_BACKGROUND } from './constants';
import { Outlet } from 'react-router-dom';
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
export const Layout = ({
className,
@@ -28,8 +29,10 @@ export const LayoutWithSky = ({
...props
}: HTMLAttributes<HTMLDivElement>) => {
return (
<div className={classNames('h-full overflow-auto', SKY_BACKGROUND)}>
<TinyScroll
className={classNames('max-h-full overflow-auto', SKY_BACKGROUND)}
>
<Layout className={className} {...props} />
</div>
</TinyScroll>
);
};
@@ -26,6 +26,7 @@ import sortBy from 'lodash/sortBy';
import { useLayoutEffect, useRef, useState } from 'react';
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
import BigNumber from 'bignumber.js';
import { t } from '@vegaprotocol/i18n';
export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet();
@@ -116,34 +117,34 @@ export const Statistics = ({
: 0;
const baseCommissionTile = (
<StatTile title="Base commission rate">
<StatTile title={t('Base commission rate')}>
{baseCommissionValue * 100}%
</StatTile>
);
const stakingMultiplierTile = (
<StatTile
title="Staking multiplier"
title={t('Staking multiplier')}
description={`(${addDecimalsFormatNumber(
stakeAvailable?.toString() || 0,
18
)} $VEGA staked)`}
>
{multiplier || 'None'}
{multiplier || t('None')}
</StatTile>
);
const finalCommissionTile = (
<StatTile title="Final commission rate">
<StatTile title={t('Final commission rate')}>
{finalCommissionValue * 100}%
</StatTile>
);
const numberOfTradersValue = data.referees.length;
const numberOfTradersTile = (
<StatTile title="Number of traders">{numberOfTradersValue}</StatTile>
<StatTile title={t('Number of traders')}>{numberOfTradersValue}</StatTile>
);
const codeTile = <CodeTile code={data?.code} />;
const createdAtTile = (
<StatTile title="Created at">
<StatTile title={t('Created at')}>
<span className="text-3xl">
{getDateFormat().format(new Date(data.createdAt))}
</span>
@@ -154,7 +155,10 @@ export const Statistics = ({
.map((r) => new BigNumber(r.totalRefereeGeneratedRewards))
.reduce((all, r) => all.plus(r), new BigNumber(0));
const totalCommissionTile = (
<StatTile title="Total commission (last 30 days)" description="(Quantum)">
<StatTile
title={t('Total commission (last 30 days)')}
description={t('(qUSD)')}
>
{getNumberFormat(0).format(Number(totalCommissionValue))}
</StatTile>
);
@@ -184,29 +188,31 @@ export const Statistics = ({
});
const currentBenefitTierTile = (
<StatTile title="Current tier">
<StatTile title={t('Current tier')}>
{currentBenefitTierValue?.tier || '-'}
</StatTile>
);
const discountFactorTile = (
<StatTile title="Discount">{discountFactorValue * 100}%</StatTile>
<StatTile title={t('Discount')}>{discountFactorValue * 100}%</StatTile>
);
const runningVolumeTile = (
<StatTile title="Combined volume">
<StatTile title={t('Combined volume')}>
{compactNumFormat.format(runningVolumeValue)}
</StatTile>
);
const epochsTile = <StatTile title="Epochs in set">{epochsValue}</StatTile>;
const epochsTile = (
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
);
const nextTierVolumeTile = (
<StatTile title="Volume to next tier">
<StatTile title={t('Volume to next tier')}>
{nextBenefitTierVolumeValue <= 0
? '-'
? '0'
: compactNumFormat.format(nextBenefitTierVolumeValue)}
</StatTile>
);
const nextTierEpochsTile = (
<StatTile title="Epochs to next tier">
{nextBenefitTierEpochsValue <= 0 ? '-' : nextBenefitTierEpochsValue}
<StatTile title={t('Epochs to next tier')}>
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
</StatTile>
);
@@ -249,7 +255,7 @@ export const Statistics = ({
{/* Referees (only for referrer view) */}
{as === 'referrer' && data.referees.length > 0 && (
<div className="mt-20 mb-20">
<h2 className="text-2xl mb-5">Referees</h2>
<h2 className="text-2xl mb-5">{t('Referees')}</h2>
<div
className={classNames(
collapsed && [
@@ -273,12 +279,12 @@ export const Statistics = ({
<Table
ref={tableRef}
columns={[
{ name: 'party', displayName: 'Trader' },
{ name: 'joined', displayName: 'Date Joined' },
{ name: 'volume', displayName: 'Volume (last 30 days)' },
{ name: 'party', displayName: t('Trader') },
{ name: 'joined', displayName: t('Date Joined') },
{ name: 'volume', displayName: t('Volume (last 30 days)') },
{
name: 'commission',
displayName: 'Commission earned (last 30 days)',
displayName: t('Commission earned (last 30 days)'),
},
]}
data={sortBy(
@@ -22,9 +22,9 @@ import { t } from '@vegaprotocol/i18n';
const Nav = () => (
<div className="flex justify-center border-b border-vega-cdark-500">
<TabLink end to={Routes.REFERRALS}>
I want a code
{t('I want a code')}
</TabLink>
<TabLink to={Routes.REFERRALS_APPLY_CODE}>I have a code</TabLink>
<TabLink to={Routes.REFERRALS_APPLY_CODE}>{t('I have a code')}</TabLink>
</div>
);
@@ -77,7 +77,7 @@ export const Referrals = () => {
</div>
) : error ? (
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
<p>Something went wrong</p>
<p>{t('Something went wrong')}</p>
<span className="text-xs">{error.message}</span>
</div>
) : (
@@ -88,7 +88,7 @@ export const Referrals = () => {
<TiersContainer />
<div className="mt-10 mb-5 text-center">
<h2 className="text-2xl">How it works</h2>
<h2 className="text-2xl">{t('How it works')}</h2>
</div>
<div className="md:w-[60%] mx-auto">
<HowItWorksTable />
@@ -98,7 +98,8 @@ export const Referrals = () => {
href={REFERRAL_DOCS_LINK}
target="_blank"
>
Read the terms <VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
{t('Read the terms')}{' '}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</TradingAnchorButton>
</div>
</div>
+17 -13
View File
@@ -7,6 +7,7 @@ import { Tag } from './tag';
import type { ComponentProps, ReactNode } from 'react';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { DApp, TOKEN_PROPOSALS, useLinks } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
const Loading = ({ variant }: { variant: 'large' | 'inline' }) => (
<div
@@ -61,7 +62,7 @@ const StakingTier = ({
<Tag color={color[tier]}>Multiplier {referralRewardMultiplier}x</Tag>
<h3 className="mt-1 mb-1 text-base">{label}</h3>
<p className="text-sm text-vega-clight-100 dark:text-vega-cdark-100">
Stake a minimum of {minimumStakedTokens} $VEGA tokens
{t('Stake a minimum of')} {minimumStakedTokens} {t('$VEGA tokens')}
</p>
</div>
</div>
@@ -81,9 +82,12 @@ export const TiersContainer = () => {
if ((!loading && !details) || error) {
return (
<div className="text-base px-5 py-10 text-center">
We&apos;re sorry but we don&apos;t have an active referral programme
currently running. You can propose a new programme{' '}
<ExternalLink href={governanceLink(TOKEN_PROPOSALS)}>here</ExternalLink>
{t(
"We're sorry but we don't have an active referral programme currently running. You can propose a new programme"
)}{' '}
<ExternalLink href={governanceLink(TOKEN_PROPOSALS)}>
{t('here')}
</ExternalLink>
.
</div>
);
@@ -93,10 +97,10 @@ export const TiersContainer = () => {
<>
{/* Benefit tiers */}
<div className="flex flex-col items-baseline justify-between mt-10 mb-5">
<h2 className="text-2xl">Referral tiers</h2>
<h2 className="text-2xl">{t('Referral tiers')}</h2>
{ends && (
<span className="text-sm text-vega-clight-200 dark:text-vega-cdark-200">
Program ends: {ends}
{t('Program ends:')} {ends}
</span>
)}
</div>
@@ -119,7 +123,7 @@ export const TiersContainer = () => {
{/* Staking tiers */}
<div className="flex flex-row items-baseline justify-between mb-5">
<h2 className="text-2xl">Staking multipliers</h2>
<h2 className="text-2xl">{t('Staking multipliers')}</h2>
</div>
<div className="mb-20 flex flex-col justify-items-stretch lg:flex-row gap-5">
{loading || !stakingTiers || stakingTiers.length === 0 ? (
@@ -170,15 +174,15 @@ const TiersTable = ({
return (
<Table
columns={[
{ name: 'tierElement', displayName: 'Tier' },
{ name: 'tierElement', displayName: t('Tier') },
{
name: 'commission',
displayName: 'Referrer commission',
tooltip: 'A percentage of commission earned by the referrer',
displayName: t('Referrer commission'),
tooltip: t('A percentage of commission earned by the referrer'),
},
{ name: 'discount', displayName: 'Referrer trading discount' },
{ name: 'volume', displayName: 'Min. trading volume' },
{ name: 'epochs', displayName: 'Min. epochs' },
{ name: 'discount', displayName: t('Referrer trading discount') },
{ name: 'volume', displayName: t('Min. trading volume') },
{ name: 'epochs', displayName: t('Min. epochs') },
]}
data={data.map((d) => ({
...d,
@@ -0,0 +1,58 @@
query DiscountPrograms {
currentReferralProgram {
benefitTiers {
minimumEpochs
minimumRunningNotionalTakerVolume
referralDiscountFactor
}
windowLength
}
currentVolumeDiscountProgram {
benefitTiers {
minimumRunningNotionalTakerVolume
volumeDiscountFactor
}
windowLength
}
}
query Fees(
$partyId: ID!
$volumeDiscountEpochs: Int!
$referralDiscountEpochs: Int!
) {
epoch {
id
}
volumeDiscountStats(
partyId: $partyId
pagination: { last: $volumeDiscountEpochs }
) {
edges {
node {
atEpoch
discountFactor
runningVolume
}
}
}
referralSetReferees(referee: $partyId) {
edges {
node {
atEpoch
}
}
}
referralSetStats(
partyId: $partyId
pagination: { last: $referralDiscountEpochs }
) {
edges {
node {
atEpoch
discountFactor
referralSetRunningNotionalTakerVolume
}
}
}
}
@@ -0,0 +1,131 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type DiscountProgramsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgram?: { __typename?: 'CurrentReferralProgram', windowLength: number, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string }> } | null, currentVolumeDiscountProgram?: { __typename?: 'VolumeDiscountProgram', windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } | null };
export type FeesQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
volumeDiscountEpochs: Types.Scalars['Int'];
referralDiscountEpochs: Types.Scalars['Int'];
}>;
export type FeesQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string }, volumeDiscountStats: { __typename?: 'VolumeDiscountStatsConnection', edges: Array<{ __typename?: 'VolumeDiscountStatsEdge', node: { __typename?: 'VolumeDiscountStats', atEpoch: number, discountFactor: string, runningVolume: string } } | null> }, referralSetReferees: { __typename?: 'ReferralSetRefereeConnection', edges: Array<{ __typename?: 'ReferralSetRefereeEdge', node: { __typename?: 'ReferralSetReferee', atEpoch: number } } | null> }, referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, discountFactor: string, referralSetRunningNotionalTakerVolume: string } } | null> } };
export const DiscountProgramsDocument = gql`
query DiscountPrograms {
currentReferralProgram {
benefitTiers {
minimumEpochs
minimumRunningNotionalTakerVolume
referralDiscountFactor
}
windowLength
}
currentVolumeDiscountProgram {
benefitTiers {
minimumRunningNotionalTakerVolume
volumeDiscountFactor
}
windowLength
}
}
`;
/**
* __useDiscountProgramsQuery__
*
* To run a query within a React component, call `useDiscountProgramsQuery` and pass it any options that fit your needs.
* When your component renders, `useDiscountProgramsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useDiscountProgramsQuery({
* variables: {
* },
* });
*/
export function useDiscountProgramsQuery(baseOptions?: Apollo.QueryHookOptions<DiscountProgramsQuery, DiscountProgramsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<DiscountProgramsQuery, DiscountProgramsQueryVariables>(DiscountProgramsDocument, options);
}
export function useDiscountProgramsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<DiscountProgramsQuery, DiscountProgramsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<DiscountProgramsQuery, DiscountProgramsQueryVariables>(DiscountProgramsDocument, options);
}
export type DiscountProgramsQueryHookResult = ReturnType<typeof useDiscountProgramsQuery>;
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!) {
epoch {
id
}
volumeDiscountStats(
partyId: $partyId
pagination: {last: $volumeDiscountEpochs}
) {
edges {
node {
atEpoch
discountFactor
runningVolume
}
}
}
referralSetReferees(referee: $partyId) {
edges {
node {
atEpoch
}
}
}
referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) {
edges {
node {
atEpoch
discountFactor
referralSetRunningNotionalTakerVolume
}
}
}
}
`;
/**
* __useFeesQuery__
*
* To run a query within a React component, call `useFeesQuery` and pass it any options that fit your needs.
* When your component renders, `useFeesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useFeesQuery({
* variables: {
* partyId: // value for 'partyId'
* volumeDiscountEpochs: // value for 'volumeDiscountEpochs'
* referralDiscountEpochs: // value for 'referralDiscountEpochs'
* },
* });
*/
export function useFeesQuery(baseOptions: Apollo.QueryHookOptions<FeesQuery, FeesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FeesQuery, FeesQueryVariables>(FeesDocument, options);
}
export function useFeesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FeesQuery, FeesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FeesQuery, FeesQueryVariables>(FeesDocument, options);
}
export type FeesQueryHookResult = ReturnType<typeof useFeesQuery>;
export type FeesLazyQueryHookResult = ReturnType<typeof useFeesLazyQuery>;
export type FeesQueryResult = Apollo.QueryResult<FeesQuery, FeesQueryVariables>;
@@ -0,0 +1,36 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
export const FeeCard = ({
children,
title,
className,
loading = false,
}: {
children: ReactNode;
title: string;
className?: string;
loading?: boolean;
}) => {
return (
<div
className={classNames(
'p-4 bg-vega-clight-800 dark:bg-vega-cdark-800 col-span-full lg:col-auto',
'rounded-lg',
className
)}
>
<h2 className="mb-3">{title}</h2>
{loading ? <FeeCardLoader /> : children}
</div>
);
};
export const FeeCardLoader = () => {
return (
<div className="flex flex-col gap-2">
<div className="w-full h-5 bg-vega-clight-600 dark:bg-vega-cdark-600" />
<div className="w-3/4 h-6 bg-vega-clight-600 dark:bg-vega-cdark-600" />
</div>
);
};
@@ -0,0 +1,103 @@
import { render, screen } from '@testing-library/react';
import { formatNumber } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { CurrentVolume, TradingFees } from './fees-container';
import { formatPercentage, getAdjustedFee } from './utils';
describe('TradingFees', () => {
it('renders correct fee data', () => {
const makerFee = 0.01;
const infraFee = 0.01;
const minLiqFee = 0.1;
const maxLiqFee = 0.3;
const referralDiscount = 0.01;
const volumeDiscount = 0.01;
const makerBigNum = new BigNumber(makerFee);
const infraBigNum = new BigNumber(infraFee);
const minLiqBigNum = new BigNumber(minLiqFee);
const maxLiqBigNum = new BigNumber(maxLiqFee);
const referralBigNum = new BigNumber(referralDiscount);
const volumeBigNum = new BigNumber(volumeDiscount);
const props = {
params: {
market_fee_factors_makerFee: makerFee.toString(),
market_fee_factors_infrastructureFee: infraFee.toString(),
},
markets: [
{ fees: { factors: { liquidityFee: minLiqFee.toString() } } },
{ fees: { factors: { liquidityFee: '0.2' } } },
{ fees: { factors: { liquidityFee: maxLiqFee.toString() } } },
],
referralDiscount,
volumeDiscount,
};
render(<TradingFees {...props} />);
const minFee = formatPercentage(
makerBigNum.plus(infraFee).plus(minLiqFee).toNumber()
);
const maxFee = formatPercentage(
makerBigNum.plus(infraFee).plus(maxLiqFee).toNumber()
);
expect(
screen.getByText('Total fee before discount').nextElementSibling
).toHaveTextContent(`${minFee}%-${maxFee}%`);
expect(
screen.getByText('Infrastructure').nextElementSibling
).toHaveTextContent(formatPercentage(infraFee) + '%');
expect(screen.getByText('Maker').nextElementSibling).toHaveTextContent(
formatPercentage(makerFee) + '%'
);
const minAdjustedFees = formatPercentage(
getAdjustedFee(
[makerBigNum, infraBigNum, minLiqBigNum],
[referralBigNum, volumeBigNum]
)
);
const maxAdjustedFees = formatPercentage(
getAdjustedFee(
[makerBigNum, infraBigNum, maxLiqBigNum],
[referralBigNum, volumeBigNum]
)
);
expect(screen.getByTestId('adjusted-fees')).toHaveTextContent(
`${minAdjustedFees}%-${maxAdjustedFees}%`
);
});
});
describe('CurerntVolume', () => {
it('renders the required amount for the next tier', () => {
const windowLengthVolume = 1500;
const nextTierVolume = 2000;
const props = {
tiers: [
{ minimumRunningNotionalTakerVolume: '1000' },
{ minimumRunningNotionalTakerVolume: nextTierVolume.toString() },
{ minimumRunningNotionalTakerVolume: '3000' },
],
tierIndex: 0,
windowLengthVolume,
epochs: 5,
};
render(<CurrentVolume {...props} />);
expect(
screen.getByText(formatNumber(windowLengthVolume)).nextElementSibling
).toHaveTextContent(`Past ${props.epochs} epochs`);
expect(
screen.getByText(formatNumber(nextTierVolume - windowLengthVolume))
.nextElementSibling
).toHaveTextContent('Required for next tier');
});
});
@@ -0,0 +1,490 @@
import maxBy from 'lodash/maxBy';
import minBy from 'lodash/minBy';
import { t } from '@vegaprotocol/i18n';
import { useVegaWallet } from '@vegaprotocol/wallet';
import {
useNetworkParams,
NetworkParams,
} from '@vegaprotocol/network-parameters';
import { useMarketList } from '@vegaprotocol/markets';
import { formatNumber } from '@vegaprotocol/utils';
import { useDiscountProgramsQuery, useFeesQuery } from './__generated__/Fees';
import { FeeCard } from './fees-card';
import { MarketFees } from './market-fees';
import { Stat } from './stat';
import { useVolumeStats } from './use-volume-stats';
import { useReferralStats } from './use-referral-stats';
import { formatPercentage, getAdjustedFee } from './utils';
import { Table, Td, Th, THead, Tr } from './table';
import BigNumber from 'bignumber.js';
export const FeesContainer = () => {
const { pubKey } = useVegaWallet();
const { params, loading: paramsLoading } = useNetworkParams([
NetworkParams.market_fee_factors_makerFee,
NetworkParams.market_fee_factors_infrastructureFee,
]);
const { data: markets, loading: marketsLoading } = useMarketList();
const { data: programData, loading: programLoading } =
useDiscountProgramsQuery();
const volumeDiscountEpochs =
programData?.currentVolumeDiscountProgram?.windowLength || 1;
const referralDiscountEpochs =
programData?.currentReferralProgram?.windowLength || 1;
const { data: feesData, loading: feesLoading } = useFeesQuery({
variables: {
partyId: pubKey || '',
volumeDiscountEpochs,
referralDiscountEpochs,
},
skip: !pubKey || !programData,
});
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
useVolumeStats(
feesData?.volumeDiscountStats,
programData?.currentVolumeDiscountProgram
);
const {
referralDiscount,
referralVolumeInWindow,
referralTierIndex,
referralTiers,
epochsInSet,
} = useReferralStats(
feesData?.referralSetStats,
feesData?.referralSetReferees,
programData?.currentReferralProgram,
feesData?.epoch
);
const loading = paramsLoading || feesLoading || programLoading;
const isConnected = Boolean(pubKey);
return (
<div className="grid auto-rows-min grid-cols-4 gap-3">
{isConnected && (
<>
<FeeCard
title={t('My trading fees')}
className="sm:col-span-2"
loading={loading}
>
<TradingFees
params={params}
markets={markets}
referralDiscount={referralDiscount}
volumeDiscount={volumeDiscount}
/>
</FeeCard>
<FeeCard
title={t('Total discount')}
className="sm:col-span-2"
loading={loading}
>
<TotalDiscount
referralDiscount={referralDiscount}
volumeDiscount={volumeDiscount}
/>
</FeeCard>
<FeeCard
title={t('My current volume')}
className="sm:col-span-2"
loading={loading}
>
<CurrentVolume
tiers={volumeTiers}
tierIndex={volumeTierIndex}
windowLengthVolume={volumeInWindow}
epochs={volumeDiscountEpochs}
/>
</FeeCard>
<FeeCard
title={t('Referral benefits')}
className="sm:col-span-2"
loading={loading}
>
<ReferralBenefits
setRunningNotionalTakerVolume={referralVolumeInWindow}
epochsInSet={epochsInSet}
epochs={referralDiscountEpochs}
/>
</FeeCard>
</>
)}
<FeeCard
title={t('Volume discount')}
className="lg:col-span-full xl:col-span-2"
loading={loading}
>
<VolumeTiers
tiers={volumeTiers}
tierIndex={volumeTierIndex}
lastEpochVolume={volumeInWindow}
/>
</FeeCard>
<FeeCard
title={t('Referral discount')}
className="lg:col-span-full xl:col-span-2"
loading={loading}
>
<ReferralTiers
tiers={referralTiers}
tierIndex={referralTierIndex}
epochsInSet={epochsInSet}
referralVolumeInWindow={referralVolumeInWindow}
/>
</FeeCard>
<FeeCard
title={t('Liquidity fees')}
className="lg:col-span-full"
loading={marketsLoading}
>
<MarketFees
markets={markets}
referralDiscount={referralDiscount}
volumeDiscount={volumeDiscount}
/>
</FeeCard>
</div>
);
};
export const TradingFees = ({
params,
markets,
referralDiscount,
volumeDiscount,
}: {
params: {
market_fee_factors_infrastructureFee: string;
market_fee_factors_makerFee: string;
};
markets: Array<{ fees: { factors: { liquidityFee: string } } }> | null;
referralDiscount: number;
volumeDiscount: number;
}) => {
const referralDiscountBigNum = new BigNumber(referralDiscount);
const volumeDiscountBigNum = new BigNumber(volumeDiscount);
// Show min and max liquidity fees from all markets
const minLiq = minBy(markets, (m) => Number(m.fees.factors.liquidityFee));
const maxLiq = maxBy(markets, (m) => Number(m.fees.factors.liquidityFee));
const total = new BigNumber(params.market_fee_factors_makerFee).plus(
new BigNumber(params.market_fee_factors_infrastructureFee)
);
const adjustedTotal = getAdjustedFee(
[total],
[referralDiscountBigNum, volumeDiscountBigNum]
);
let minTotal;
let maxTotal;
let minAdjustedTotal;
let maxAdjustedTotal;
if (minLiq && maxLiq) {
const minLiqFee = new BigNumber(minLiq.fees.factors.liquidityFee);
const maxLiqFee = new BigNumber(maxLiq.fees.factors.liquidityFee);
minTotal = total.plus(minLiqFee);
maxTotal = total.plus(maxLiqFee);
minAdjustedTotal = getAdjustedFee(
[total, minLiqFee],
[referralDiscountBigNum, volumeDiscountBigNum]
);
maxAdjustedTotal = getAdjustedFee(
[total, maxLiqFee],
[referralDiscountBigNum, volumeDiscountBigNum]
);
}
return (
<div>
<div className="pt-6 leading-none">
<p className="block text-3xl leading-none" data-testid="adjusted-fees">
{minAdjustedTotal !== undefined && maxAdjustedTotal !== undefined
? `${formatPercentage(minAdjustedTotal)}%-${formatPercentage(
maxAdjustedTotal
)}%`
: `${formatPercentage(adjustedTotal)}%`}
</p>
<table className="w-full mt-0.5 text-xs text-muted">
<tbody>
<tr>
<th className="font-normal text-left text-default">
{t('Total fee before discount')}
</th>
<td className="text-right text-default">
{minTotal !== undefined && maxTotal !== undefined
? `${formatPercentage(
minTotal.toNumber()
)}%-${formatPercentage(maxTotal.toNumber())}%`
: `${formatPercentage(total.toNumber())}%`}
</td>
</tr>
<tr>
<th className="font-normal text-left">{t('Infrastructure')}</th>
<td className="text-right">
{formatPercentage(
Number(params.market_fee_factors_infrastructureFee)
)}
%
</td>
</tr>
<tr>
<th className="font-normal text-left ">{t('Maker')}</th>
<td className="text-right">
{formatPercentage(Number(params.market_fee_factors_makerFee))}%
</td>
</tr>
{minLiq && maxLiq && (
<tr>
<th className="font-normal text-left ">{t('Liquidity')}</th>
<td className="text-right">
{formatPercentage(Number(minLiq.fees.factors.liquidityFee))}%
{'-'}
{formatPercentage(Number(maxLiq.fees.factors.liquidityFee))}%
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
);
};
export const CurrentVolume = ({
tiers,
tierIndex,
windowLengthVolume,
epochs,
}: {
tiers: Array<{ minimumRunningNotionalTakerVolume: string }>;
tierIndex: number;
windowLengthVolume: number;
epochs: number;
}) => {
const nextTier = tiers[tierIndex + 1];
const requiredForNextTier = nextTier
? Number(nextTier.minimumRunningNotionalTakerVolume) - windowLengthVolume
: 0;
return (
<div>
<Stat
value={formatNumber(windowLengthVolume)}
text={t('Past %s epochs', epochs.toString())}
/>
{requiredForNextTier > 0 && (
<Stat
value={formatNumber(requiredForNextTier)}
text={t('Required for next tier')}
/>
)}
</div>
);
};
const ReferralBenefits = ({
epochsInSet,
setRunningNotionalTakerVolume,
epochs,
}: {
epochsInSet: number;
setRunningNotionalTakerVolume: number;
epochs: number;
}) => {
return (
<div>
<Stat
// all sets volume (not just current party)
value={formatNumber(setRunningNotionalTakerVolume)}
text={t(
'Combined running notional over the %s epochs',
epochs.toString()
)}
/>
<Stat value={epochsInSet} text={t('epochs in referral set')} />
</div>
);
};
const TotalDiscount = ({
referralDiscount,
volumeDiscount,
}: {
referralDiscount: number;
volumeDiscount: number;
}) => {
return (
<div>
<Stat
value={formatPercentage(referralDiscount + volumeDiscount) + '%'}
highlight={true}
/>
<table className="w-full mt-0.5 text-xs text-muted">
<tbody>
<tr>
<th className="font-normal text-left">{t('Volume discount')}</th>
<td className="text-right">{formatPercentage(volumeDiscount)}%</td>
</tr>
<tr>
<th className="font-normal text-left ">{t('Referral discount')}</th>
<td className="text-right">
{formatPercentage(referralDiscount)}%
</td>
</tr>
</tbody>
</table>
</div>
);
};
const VolumeTiers = ({
tiers,
tierIndex,
lastEpochVolume,
}: {
tiers: Array<{
volumeDiscountFactor: string;
minimumRunningNotionalTakerVolume: string;
}>;
tierIndex: number;
lastEpochVolume: number;
}) => {
if (!tiers.length) {
return (
<p className="text-sm text-muted">
{t('No volume discount program active')}
</p>
);
}
return (
<div>
<Table>
<THead>
<tr>
<Th>{t('Tier')}</Th>
<Th>{t('Discount')}</Th>
<Th>{t('Min. trading volume')}</Th>
<Th>{t('My volume (last epoch)')}</Th>
<Th />
</tr>
</THead>
<tbody>
{Array.from(tiers)
.reverse()
.map((tier, i) => {
const isUserTier = tiers.length - 1 - tierIndex === i;
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>
{formatPercentage(Number(tier.volumeDiscountFactor))}%
</Td>
<Td>
{formatNumber(tier.minimumRunningNotionalTakerVolume)}
</Td>
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
<Td>{isUserTier ? <YourTier /> : null}</Td>
</Tr>
);
})}
</tbody>
</Table>
</div>
);
};
const ReferralTiers = ({
tiers,
tierIndex,
epochsInSet,
referralVolumeInWindow,
}: {
tiers: Array<{
referralDiscountFactor: string;
minimumRunningNotionalTakerVolume: string;
minimumEpochs: number;
}>;
tierIndex: number;
epochsInSet: number;
referralVolumeInWindow: number;
}) => {
if (!tiers.length) {
return (
<p className="text-sm text-muted">{t('No referral program active')}</p>
);
}
return (
<div>
<Table>
<THead>
<tr>
<Th>{t('Tier')}</Th>
<Th>{t('Discount')}</Th>
<Th>{t('Min. trading volume')}</Th>
<Th>{t('Required epochs')}</Th>
<Th />
</tr>
</THead>
<tbody>
{Array.from(tiers)
.reverse()
.map((t, i) => {
const isUserTier = tiers.length - 1 - tierIndex === i;
const requiredVolume = Number(
t.minimumRunningNotionalTakerVolume
);
let unlocksIn = null;
if (
referralVolumeInWindow >= requiredVolume &&
epochsInSet < t.minimumEpochs
) {
unlocksIn = (
<span className="text-muted">
Unlocks in {t.minimumEpochs - epochsInSet} epochs
</span>
);
}
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>
);
};
const YourTier = () => {
return (
<span className="px-4 py-1.5 rounded-xl bg-rainbow whitespace-nowrap text-white">
{t('Your tier')}
</span>
);
};
@@ -0,0 +1 @@
export { FeesContainer } from './fees-container';
@@ -0,0 +1,90 @@
import compact from 'lodash/compact';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
import { AgGrid } from '@vegaprotocol/datagrid';
import { t } from '@vegaprotocol/i18n';
import { formatPercentage, getAdjustedFee } from './utils';
import { MarketCodeCell } from '../../client-pages/markets/market-code-cell';
import BigNumber from 'bignumber.js';
const feesTableColumnDefs = [
{ field: 'code', cellRenderer: 'MarketCodeCell' },
{
field: 'feeAfterDiscount',
headerName: t('Total fee after discount'),
valueFormatter: ({ value }: { value: number }) => value + '%',
},
{
field: 'infraFee',
valueFormatter: ({ value }: { value: number }) => value + '%',
},
{
field: 'makerFee',
valueFormatter: ({ value }: { value: number }) => value + '%',
},
{
field: 'liquidityFee',
valueFormatter: ({ value }: { value: number }) => value + '%',
},
{
field: 'totalFee',
headerName: t('Total fee before discount'),
valueFormatter: ({ value }: { value: number }) => value + '%',
},
];
const feesTableDefaultColDef = {
flex: 1,
resizable: true,
sortable: true,
};
const components = {
MarketCodeCell,
};
export const MarketFees = ({
markets,
referralDiscount,
volumeDiscount,
}: {
markets: MarketMaybeWithDataAndCandles[] | null;
referralDiscount: number;
volumeDiscount: number;
}) => {
const rows = compact(markets || []).map((m) => {
const infraFee = new BigNumber(m.fees.factors.infrastructureFee);
const makerFee = new BigNumber(m.fees.factors.makerFee);
const liquidityFee = new BigNumber(m.fees.factors.liquidityFee);
const totalFee = infraFee.plus(makerFee).plus(liquidityFee);
const feeAfterDiscount = getAdjustedFee(
[infraFee, makerFee, liquidityFee],
[new BigNumber(referralDiscount), new BigNumber(volumeDiscount)]
);
return {
code: m.tradableInstrument.instrument.code,
productType: m.tradableInstrument.instrument.product.__typename,
infraFee: formatPercentage(infraFee.toNumber()),
makerFee: formatPercentage(makerFee.toNumber()),
liquidityFee: formatPercentage(liquidityFee.toNumber()),
totalFee: formatPercentage(totalFee.toNumber()),
feeAfterDiscount: formatPercentage(feeAfterDiscount),
parentMarketID: m.parentMarketID,
successorMarketID: m.successorMarketID,
};
});
return (
<div className="border rounded-sm border-default">
<AgGrid
columnDefs={feesTableColumnDefs}
rowData={rows}
defaultColDef={feesTableDefaultColDef}
domLayout="autoHeight"
components={components}
rowHeight={45}
/>
</div>
);
};
@@ -0,0 +1,26 @@
import classNames from 'classnames';
export const Stat = ({
value,
text,
highlight,
}: {
value: string | number;
text?: string;
highlight?: boolean;
}) => {
return (
<p className="pt-3 leading-none first:pt-6">
<span
className={classNames('inline-block text-3xl leading-none', {
'text-transparent bg-rainbow bg-clip-text': highlight,
})}
>
{value}
</span>
{text && (
<small className="block mt-0.5 text-xs text-muted">{text}</small>
)}
</p>
);
};
@@ -0,0 +1,40 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
const cellClass = 'px-4 py-2 text-xs font-normal text-left last:text-right';
export const Th = ({ children }: { children?: ReactNode }) => {
return (
<th className={classNames(cellClass, 'text-secondary leading-none py-3')}>
{children}
</th>
);
};
export const Td = ({ children }: { children?: ReactNode }) => {
return <th className={cellClass}>{children}</th>;
};
export const Tr = ({ children }: { children?: ReactNode }) => {
return (
<tr className="hover:bg-vega-clight-600 dark:hover:bg-vega-cdark-700">
{children}
</tr>
);
};
export const Table = ({ children }: { children: ReactNode }) => {
return (
<table className="w-full border border-separate rounded-sm border-spacing-0 border-vega-clight-600 dark:border-vega-cdark-600">
{children}
</table>
);
};
export const THead = ({ children }: { children: ReactNode }) => {
return (
<thead className="border-b bg-vega-clight-700 dark:bg-vega-cdark-700 border-vega-clight-600 dark:border-vega-cdark-600">
{children}
</thead>
);
};
@@ -0,0 +1,175 @@
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 sets = {
edges: [
{
node: {
atEpoch: 3,
},
},
{
node: {
atEpoch: 4,
},
},
],
};
const epoch = {
id: '10',
};
const program = {
windowLength: 5,
benefitTiers: [
{
minimumEpochs: 4,
minimumRunningNotionalTakerVolume: '100',
referralDiscountFactor: '0.01',
},
{
minimumEpochs: 6,
minimumRunningNotionalTakerVolume: '200',
referralDiscountFactor: '0.05',
},
{
minimumEpochs: 8,
minimumRunningNotionalTakerVolume: '300',
referralDiscountFactor: '0.1',
},
],
};
it('returns correct default values', () => {
const { result } = renderHook(() => useReferralStats());
expect(result.current).toEqual({
referralDiscount: 0,
referralVolumeInWindow: 0,
referralTierIndex: -1,
referralTiers: [],
epochsInSet: 0,
});
});
it('returns formatted data and tiers', () => {
const { result } = renderHook(() =>
useReferralStats(setStats, sets, program, epoch)
);
// should use stats from latest epoch
const stats = setStats.edges[1].node;
const set = sets.edges[1].node;
expect(result.current).toEqual({
referralDiscount: Number(stats.discountFactor),
referralVolumeInWindow: Number(
stats.referralSetRunningNotionalTakerVolume
),
referralTierIndex: 1,
referralTiers: program.benefitTiers,
epochsInSet: Number(epoch.id) - set.atEpoch,
});
});
it.each([
{ joinedAt: 2, index: -1 },
{ joinedAt: 3, index: -1 },
{ joinedAt: 4, index: 0 },
{ joinedAt: 5, index: 0 },
{ joinedAt: 6, index: 1 },
{ joinedAt: 7, index: 1 },
{ joinedAt: 8, index: 2 },
{ joinedAt: 9, index: 2 },
])('joined at epoch: $joinedAt should be index: $index', (obj) => {
const statsA = {
edges: [
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: '100000',
},
},
],
};
const setsA = {
edges: [
{
node: {
atEpoch: Number(epoch.id) - obj.joinedAt,
},
},
],
};
const { result } = renderHook(() =>
useReferralStats(statsA, setsA, program, epoch)
);
expect(result.current.referralTierIndex).toEqual(obj.index);
});
it.each([
{ volume: '50', index: -1 },
{ volume: '100', index: 0 },
{ volume: '150', index: 0 },
{ volume: '200', index: 1 },
{ volume: '250', index: 1 },
{ volume: '300', index: 2 },
{ volume: '999', index: 2 },
])('volume: $volume should be index: $index', (obj) => {
const statsA = {
edges: [
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: obj.volume,
},
},
],
};
const setsA = {
edges: [
{
node: {
atEpoch: 1,
},
},
],
};
const { result } = renderHook(() =>
useReferralStats(statsA, setsA, program, epoch)
);
expect(result.current.referralTierIndex).toEqual(obj.index);
});
});
@@ -0,0 +1,52 @@
import compact from 'lodash/compact';
import maxBy from 'lodash/maxBy';
import { getReferralBenefitTier } from './utils';
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
export const useReferralStats = (
setStats?: FeesQuery['referralSetStats'],
setReferees?: FeesQuery['referralSetReferees'],
program?: DiscountProgramsQuery['currentReferralProgram'],
epoch?: FeesQuery['epoch']
) => {
const referralTiers = program?.benefitTiers || [];
if (!setStats || !setReferees || !program || !epoch) {
return {
referralDiscount: 0,
referralVolumeInWindow: 0,
referralTierIndex: -1,
referralTiers,
epochsInSet: 0,
};
}
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;
return {
referralDiscount,
referralVolumeInWindow,
referralTierIndex,
referralTiers,
epochsInSet,
};
};
@@ -0,0 +1,95 @@
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 program = {
windowLength: 5,
benefitTiers: [
{
minimumRunningNotionalTakerVolume: '100',
volumeDiscountFactor: '0.01',
},
{
minimumRunningNotionalTakerVolume: '200',
volumeDiscountFactor: '0.05',
},
{
minimumRunningNotionalTakerVolume: '300',
volumeDiscountFactor: '0.1',
},
],
};
it('returns correct default values', () => {
const { result } = renderHook(() => useVolumeStats());
expect(result.current).toEqual({
volumeDiscount: 0,
volumeInWindow: 0,
volumeTierIndex: -1,
volumeTiers: [],
});
});
it('returns formatted data and tiers', () => {
const { result } = renderHook(() => useVolumeStats(statsList, program));
// should use stats from latest epoch
const stats = statsList.edges[1].node;
expect(result.current).toEqual({
volumeDiscount: Number(stats.discountFactor),
volumeInWindow: Number(stats.runningVolume),
volumeTierIndex: 1,
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);
});
});
@@ -0,0 +1,33 @@
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'],
program?: DiscountProgramsQuery['currentVolumeDiscountProgram']
) => {
const volumeTiers = program?.benefitTiers || [];
if (!stats || !program) {
return {
volumeDiscount: 0,
volumeTierIndex: -1,
volumeInWindow: 0,
volumeTiers,
};
}
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);
return {
volumeDiscount,
volumeTierIndex,
volumeInWindow,
volumeTiers,
};
};
@@ -0,0 +1,101 @@
import { getUserLocale } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
/**
* Convert a number between 0-1 into a percentage value between 0-100
*
* Not using formatNumberPercentage from vegaprotocol/utils as this
* returns a string and includes extra 0s on the end. We need these
* values in aggrid as numbers for sorting
*/
export const formatPercentage = (num: number) => {
const pct = new BigNumber(num).times(100);
const dps = pct.decimalPlaces();
const formatter = new Intl.NumberFormat(getUserLocale(), {
minimumFractionDigits: dps || 0,
maximumFractionDigits: dps || 0,
});
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
*/
export const getAdjustedFee = (fees: BigNumber[], discounts: BigNumber[]) => {
const totalFee = fees.reduce((sum, f) => sum.plus(f), new BigNumber(0));
const totalDiscount = discounts.reduce(
(sum, d) => sum.plus(d),
new BigNumber(0)
);
return totalFee
.times(BigNumber.max(0, new BigNumber(1).minus(totalDiscount)))
.toNumber();
};
@@ -27,7 +27,7 @@ export const LayoutWithSidebar = ({
<div className={gridClasses}>
<div className="col-span-full">{header}</div>
<main
className={classNames('col-start-1 col-end-1', {
className={classNames('col-start-1 col-end-1 overflow-y-auto', {
'lg:col-end-3': !sidebarOpen,
'hidden lg:block lg:col-end-2': sidebarOpen,
})}
@@ -67,6 +67,7 @@ describe('Navbar', () => {
[`/markets/${marketId}`, 'Trading'],
['/portfolio', 'Portfolio'],
['/referrals', 'Referrals'],
['/fees', 'Fees'],
[expect.stringContaining('governance'), 'Governance'],
];
@@ -100,6 +101,7 @@ describe('Navbar', () => {
[`/markets/${marketId}`, 'Trading'],
['/portfolio', 'Portfolio'],
['/referrals', 'Referrals'],
['/fees', 'Fees'],
[expect.stringContaining('governance'), 'Governance'],
];
const links = menu.getAllByRole('link');
@@ -187,6 +187,11 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
</NavbarLink>
</NavbarItem>
)}
<NavbarItem>
<NavbarLink to={Links.FEES()} onClick={onClick}>
{t('Fees')}
</NavbarLink>
</NavbarItem>
<NavbarItem>
<NavbarLinkExternal to={useLinks(DApp.Governance)()}>
{t('Governance')}
+2
View File
@@ -17,6 +17,7 @@ export const Routes = {
REFERRALS_APPLY_CODE: '/referrals/apply-code',
REFERRALS_CREATE_CODE: '/referrals/create-code',
TEAMS: '/teams',
FEES: '/fees',
} as const;
type ConsoleLinks = {
@@ -40,4 +41,5 @@ export const Links: ConsoleLinks = {
REFERRALS_APPLY_CODE: () => Routes.REFERRALS_APPLY_CODE,
REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE,
TEAMS: () => Routes.TEAMS,
FEES: () => Routes.FEES,
};
+17 -2
View File
@@ -13,6 +13,7 @@ import { Assets } from '../client-pages/assets';
import { Deposit } from '../client-pages/deposit';
import { Withdraw } from '../client-pages/withdraw';
import { Transfer } from '../client-pages/transfer';
import { Fees } from '../client-pages/fees';
import { Routes as AppRoutes } from '../lib/links';
import { LayoutWithSky } from '../client-pages/referrals/layout';
import { Referrals } from '../client-pages/referrals/referrals';
@@ -55,10 +56,14 @@ export const routerConfig: RouteObject[] = compact([
FLAGS.REFERRALS
? {
path: AppRoutes.REFERRALS,
element: <LayoutWithSky />,
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [
{
element: <Referrals />,
element: (
<LayoutWithSky>
<Referrals />
</LayoutWithSky>
),
children: [
{
index: true,
@@ -81,6 +86,16 @@ export const routerConfig: RouteObject[] = compact([
],
}
: undefined,
{
path: 'fees/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [
{
index: true,
element: <Fees />,
},
],
},
{
path: 'markets/*',
element: (
+4 -4
View File
@@ -184,10 +184,10 @@ html [data-theme='dark'] {
/* Light variables */
.ag-theme-balham {
--ag-background-color: theme(colors.white);
--ag-background-color: transparent;
--ag-border-color: theme(colors.vega.clight.600);
--ag-header-background-color: theme(colors.vega.clight.700);
--ag-odd-row-background-color: theme(colors.white);
--ag-odd-row-background-color: transparent;
--ag-header-column-separator-color: theme(colors.vega.clight.500);
--ag-row-border-color: theme(colors.vega.clight.600);
--ag-row-hover-color: theme(colors.vega.clight.800);
@@ -196,10 +196,10 @@ html [data-theme='dark'] {
/* Dark variables */
.ag-theme-balham-dark {
--ag-background-color: theme(colors.vega.cdark.900);
--ag-background-color: transparent;
--ag-border-color: theme(colors.vega.cdark.600);
--ag-header-background-color: theme(colors.vega.cdark.700);
--ag-odd-row-background-color: theme(colors.vega.cdark.900);
--ag-odd-row-background-color: transparent;
--ag-header-column-separator-color: theme(colors.vega.cdark.500);
--ag-row-border-color: theme(colors.vega.cdark.600);
--ag-row-hover-color: theme(colors.vega.cdark.800);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 947 KiB

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 947 KiB

After

Width:  |  Height:  |  Size: 419 KiB

+4 -3
View File
@@ -33,7 +33,7 @@ import BigNumber from 'bignumber.js';
import classNames from 'classnames';
import { AccountsActionsDropdown } from './accounts-actions-dropdown';
const colorClass = (percentageUsed: number, neutral = false) => {
const colorClass = (percentageUsed: number) => {
return classNames('text-right', {
'text-vega-orange': percentageUsed >= 75 && percentageUsed < 90,
'text-vega-red': percentageUsed >= 90,
@@ -210,7 +210,7 @@ export const AccountTable = ({
},
cellClass: ({ data }) => {
const percentageUsed = percentageValue(data?.used, data?.total);
return colorClass(percentageUsed, true);
return colorClass(percentageUsed);
},
valueFormatter: ({
value,
@@ -270,7 +270,8 @@ export const AccountTable = ({
onClickDeposit && onClickDeposit(assetId);
}}
>
<VegaIcon name={VegaIconNames.DEPOSIT} /> {t('Deposit')}
<VegaIcon name={VegaIconNames.DEPOSIT} size={14} />{' '}
{t('Deposit')}
</TradingButton>
</CenteredGridCellWrapper>
);
@@ -259,11 +259,9 @@ export const DealTicketMarginDetails = ({
? liquidationEstimateWorstCaseIncludingBuyOrders
: liquidationEstimateWorstCaseIncludingSellOrders;
// The estimate order query API gives us the liquidation price in formatted by asset decimals.
// We need to calculate it with asset decimals, but display it with market decimals precision until the API changes.
liquidationPriceEstimate = formatValue(
liquidationEstimateWorstCase.toString(),
assetDecimals,
market.decimalPlaces,
undefined,
market.decimalPlaces
);
@@ -276,7 +274,7 @@ export const DealTicketMarginDetails = ({
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
assetDecimals,
market.decimalPlaces,
undefined,
market.decimalPlaces
);
@@ -308,7 +306,7 @@ export const DealTicketMarginDetails = ({
key={'value-dropdown'}
className="flex items-center justify-between w-full gap-2"
>
<div className="flex items-center gap-1 text-left">
<div className="flex items-center text-left gap-1">
<Tooltip description={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}>
<span className="text-muted">{t('Margin required')}</span>
</Tooltip>
@@ -24,6 +24,7 @@ import {
Tooltip,
TradingButton as Button,
Pill,
ExternalLink,
} from '@vegaprotocol/ui-toolkit';
import { useOpenVolume } from '@vegaprotocol/positions';
@@ -77,6 +78,7 @@ import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
import noop from 'lodash/noop';
import { isNonPersistentOrder } from '../../utils/time-in-force-persistance';
import { KeyValue } from './key-value';
import { DocsLinks } from '@vegaprotocol/environment';
export const REDUCE_ONLY_TOOLTIP =
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.';
@@ -273,7 +275,9 @@ export const DealTicket = ({
const assetSymbol = getAsset(market).symbol;
const assetUnit = getQuoteName(market);
const baseQuote = getBaseQuoteUnit(
market.tradableInstrument.instrument.metadata.tags
);
const summaryError = useMemo(() => {
if (!pubKey) {
@@ -412,7 +416,7 @@ export const DealTicket = ({
id="order-size"
className="w-full"
type="number"
appendElement={assetUnit && <Pill size="xs">{assetUnit}</Pill>}
appendElement={baseQuote && <Pill size="xs">{baseQuote}</Pill>}
step={sizeStep}
min={sizeStep}
data-testid="order-size"
@@ -548,15 +552,20 @@ export const DealTicket = ({
render={({ field }) => (
<Tooltip
description={
<span>
{disablePostOnlyCheckbox
? t(
'"Post only" can not be used on "Fill or Kill" or "Immediate or Cancel" orders.'
)
: t(
'"Post only" will ensure the order is not filled immediately but is placed on the order book as a passive order. When the order is processed it is either stopped (if it would not be filled immediately), or placed in the order book as a passive order until the price taker matches with it.'
)}
</span>
<>
<span>
{disablePostOnlyCheckbox
? t(
'"Post only" can not be used on "Fill or Kill" or "Immediate or Cancel" orders.'
)
: t(
'"Post only" will ensure the order is not filled immediately but is placed on the order book as a passive order. When the order is processed it is either stopped (if it would not be filled immediately), or placed in the order book as a passive order until the price taker matches with it.'
)}
</span>{' '}
<ExternalLink href={DocsLinks?.POST_REDUCE_ONLY}>
{t('Find out more')}
</ExternalLink>
</>
}
>
<div>
@@ -580,13 +589,18 @@ export const DealTicket = ({
render={({ field }) => (
<Tooltip
description={
<span>
{disableReduceOnlyCheckbox
? t(
'"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".'
)
: t(REDUCE_ONLY_TOOLTIP)}
</span>
<>
<span>
{disableReduceOnlyCheckbox
? t(
'"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".'
)
: t(REDUCE_ONLY_TOOLTIP)}
</span>{' '}
<ExternalLink href={DocsLinks?.POST_REDUCE_ONLY}>
{t('Find out more')}
</ExternalLink>
</>
}
>
<div>
@@ -618,7 +632,10 @@ export const DealTicket = ({
{t(`Trade only a fraction of the order size at once.
After the peak size of the order has traded, the size is reset. This is repeated until the order is cancelled, expires, or its full volume trades away.
For example, an iceberg order with a size of 1000 and a peak size of 100 will effectively be split into 10 orders with a size of 100 each.
Note that the full volume of the order is not hidden and is still reflected in the order book.`)}
Note that the full volume of the order is not hidden and is still reflected in the order book.`)}{' '}
<ExternalLink href={DocsLinks?.ICEBERG_ORDERS}>
{t('Find out more')}
</ExternalLink>{' '}
</p>
}
>
@@ -668,7 +685,7 @@ export const DealTicket = ({
subLabel={`${formatValue(
normalizedOrder.size,
market.positionDecimalPlaces
)} ${assetUnit} @ ${
)} ${baseQuote} @ ${
type === Schema.OrderType.TYPE_MARKET
? 'market'
: `${formatValue(
+2
View File
@@ -82,6 +82,8 @@ export const DocsLinks = VEGA_DOCS_URL
VALIDATOR_SCORES_REWARDS: `${VEGA_DOCS_URL}/concepts/vega-chain/validator-scores-and-rewards`,
MARKET_LIFECYCLE: `${VEGA_DOCS_URL}/concepts/trading-on-vega/market-lifecycle`,
ETH_DATA_SOURCES: `${VEGA_DOCS_URL}/concepts/trading-on-vega/data-sources#ethereum-data-sources`,
ICEBERG_ORDERS: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#iceberg-order`,
POST_REDUCE_ONLY: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#conditional-order-parameters`,
}
: undefined;
+8
View File
@@ -37,6 +37,14 @@ const defaultFill: PartialDeep<Trade> = {
};
describe('FillsTable', () => {
it('correct columns are rendered', async () => {
// 7005-FILL-001
// 7005-FILL-002
// 7005-FILL-003
// 7005-FILL-004
// 7005-FILL-005
// 7005-FILL-006
// 7005-FILL-007
// 7005-FILL-008
await act(async () => {
render(<FillsTable partyId="party-id" rowData={[generateFill()]} />);
});
+29 -25
View File
@@ -32,39 +32,43 @@ query LiquidityProvisions($marketId: ID!) {
}
}
subscription LiquidityProvisionsUpdate($partyId: ID, $marketId: ID) {
liquidityProvisions(partyId: $partyId, marketId: $marketId) {
id
partyID
createdAt
updatedAt
marketID
commitmentAmount
fee
status
}
}
# Liquidity Provider Share Fee
fragment LiquidityProviderFeeShareFields on LiquidityProviderFeeShare {
party {
id
}
equityLikeShare
averageEntryValuation
averageScore
virtualStake
}
query LiquidityProviderFeeShare($marketId: ID!) {
market(id: $marketId) {
id
data {
market {
id
}
liquidityProviderFeeShare {
...LiquidityProviderFeeShareFields
fragment LiquidityProviderSLAFields on LiquidityProviderSLA {
currentEpochFractionOfTimeOnBook
lastEpochFractionOfTimeOnBook
lastEpochFeePenalty
lastEpochBondPenalty
hysteresisPeriodFeePenalties
requiredLiquidity
notionalVolumeBuys
notionalVolumeSells
}
query LiquidityProviders($marketId: ID!) {
liquidityProviders(marketId: $marketId) {
edges {
node {
...LiquidityProviderFields
}
}
}
}
fragment LiquidityProviderFields on LiquidityProvider {
partyId
marketId
feeShare {
...LiquidityProviderFeeShareFields
}
sla {
...LiquidityProviderSLAFields
}
}
+51 -73
View File
@@ -12,22 +12,18 @@ export type LiquidityProvisionsQueryVariables = Types.Exact<{
export type LiquidityProvisionsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', id: string, createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } } } | null> | null } | null } | null };
export type LiquidityProvisionsUpdateSubscriptionVariables = Types.Exact<{
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
marketId?: Types.InputMaybe<Types.Scalars['ID']>;
}>;
export type LiquidityProviderFeeShareFieldsFragment = { __typename?: 'LiquidityProviderFeeShare', equityLikeShare: string, averageEntryValuation: string, averageScore: string, virtualStake: string };
export type LiquidityProviderSLAFieldsFragment = { __typename?: 'LiquidityProviderSLA', currentEpochFractionOfTimeOnBook: string, lastEpochFractionOfTimeOnBook: string, lastEpochFeePenalty: string, lastEpochBondPenalty: string, hysteresisPeriodFeePenalties?: Array<string> | null, requiredLiquidity: string, notionalVolumeBuys: string, notionalVolumeSells: string };
export type LiquidityProvisionsUpdateSubscription = { __typename?: 'Subscription', liquidityProvisions?: Array<{ __typename?: 'LiquidityProvisionUpdate', id: string, partyID: string, createdAt: any, updatedAt?: any | null, marketID: string, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus }> | null };
export type LiquidityProviderFeeShareFieldsFragment = { __typename?: 'LiquidityProviderFeeShare', equityLikeShare: string, averageEntryValuation: string, party: { __typename?: 'Party', id: string } };
export type LiquidityProviderFeeShareQueryVariables = Types.Exact<{
export type LiquidityProvidersQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type LiquidityProviderFeeShareQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, data?: { __typename?: 'MarketData', market: { __typename?: 'Market', id: string }, liquidityProviderFeeShare?: Array<{ __typename?: 'LiquidityProviderFeeShare', equityLikeShare: string, averageEntryValuation: string, party: { __typename?: 'Party', id: string } }> | null } | null } | null };
export type LiquidityProvidersQuery = { __typename?: 'Query', liquidityProviders?: { __typename?: 'LiquidityProviderConnection', edges: Array<{ __typename?: 'LiquidityProviderEdge', node: { __typename?: 'LiquidityProvider', partyId: string, marketId: string, feeShare?: { __typename?: 'LiquidityProviderFeeShare', equityLikeShare: string, averageEntryValuation: string, averageScore: string, virtualStake: string } | null, sla?: { __typename?: 'LiquidityProviderSLA', currentEpochFractionOfTimeOnBook: string, lastEpochFractionOfTimeOnBook: string, lastEpochFeePenalty: string, lastEpochBondPenalty: string, hysteresisPeriodFeePenalties?: Array<string> | null, requiredLiquidity: string, notionalVolumeBuys: string, notionalVolumeSells: string } | null } }> } | null };
export type LiquidityProviderFieldsFragment = { __typename?: 'LiquidityProvider', partyId: string, marketId: string, feeShare?: { __typename?: 'LiquidityProviderFeeShare', equityLikeShare: string, averageEntryValuation: string, averageScore: string, virtualStake: string } | null, sla?: { __typename?: 'LiquidityProviderSLA', currentEpochFractionOfTimeOnBook: string, lastEpochFractionOfTimeOnBook: string, lastEpochFeePenalty: string, lastEpochBondPenalty: string, hysteresisPeriodFeePenalties?: Array<string> | null, requiredLiquidity: string, notionalVolumeBuys: string, notionalVolumeSells: string } | null };
export const LiquidityProvisionFieldsFragmentDoc = gql`
fragment LiquidityProvisionFields on LiquidityProvision {
@@ -52,13 +48,37 @@ export const LiquidityProvisionFieldsFragmentDoc = gql`
`;
export const LiquidityProviderFeeShareFieldsFragmentDoc = gql`
fragment LiquidityProviderFeeShareFields on LiquidityProviderFeeShare {
party {
id
}
equityLikeShare
averageEntryValuation
averageScore
virtualStake
}
`;
export const LiquidityProviderSLAFieldsFragmentDoc = gql`
fragment LiquidityProviderSLAFields on LiquidityProviderSLA {
currentEpochFractionOfTimeOnBook
lastEpochFractionOfTimeOnBook
lastEpochFeePenalty
lastEpochBondPenalty
hysteresisPeriodFeePenalties
requiredLiquidity
notionalVolumeBuys
notionalVolumeSells
}
`;
export const LiquidityProviderFieldsFragmentDoc = gql`
fragment LiquidityProviderFields on LiquidityProvider {
partyId
marketId
feeShare {
...LiquidityProviderFeeShareFields
}
sla {
...LiquidityProviderSLAFields
}
}
${LiquidityProviderFeeShareFieldsFragmentDoc}
${LiquidityProviderSLAFieldsFragmentDoc}`;
export const LiquidityProvisionsDocument = gql`
query LiquidityProvisions($marketId: ID!) {
market(id: $marketId) {
@@ -100,84 +120,42 @@ export function useLiquidityProvisionsLazyQuery(baseOptions?: Apollo.LazyQueryHo
export type LiquidityProvisionsQueryHookResult = ReturnType<typeof useLiquidityProvisionsQuery>;
export type LiquidityProvisionsLazyQueryHookResult = ReturnType<typeof useLiquidityProvisionsLazyQuery>;
export type LiquidityProvisionsQueryResult = Apollo.QueryResult<LiquidityProvisionsQuery, LiquidityProvisionsQueryVariables>;
export const LiquidityProvisionsUpdateDocument = gql`
subscription LiquidityProvisionsUpdate($partyId: ID, $marketId: ID) {
liquidityProvisions(partyId: $partyId, marketId: $marketId) {
id
partyID
createdAt
updatedAt
marketID
commitmentAmount
fee
status
}
}
`;
/**
* __useLiquidityProvisionsUpdateSubscription__
*
* To run a query within a React component, call `useLiquidityProvisionsUpdateSubscription` and pass it any options that fit your needs.
* When your component renders, `useLiquidityProvisionsUpdateSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useLiquidityProvisionsUpdateSubscription({
* variables: {
* partyId: // value for 'partyId'
* marketId: // value for 'marketId'
* },
* });
*/
export function useLiquidityProvisionsUpdateSubscription(baseOptions?: Apollo.SubscriptionHookOptions<LiquidityProvisionsUpdateSubscription, LiquidityProvisionsUpdateSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<LiquidityProvisionsUpdateSubscription, LiquidityProvisionsUpdateSubscriptionVariables>(LiquidityProvisionsUpdateDocument, options);
}
export type LiquidityProvisionsUpdateSubscriptionHookResult = ReturnType<typeof useLiquidityProvisionsUpdateSubscription>;
export type LiquidityProvisionsUpdateSubscriptionResult = Apollo.SubscriptionResult<LiquidityProvisionsUpdateSubscription>;
export const LiquidityProviderFeeShareDocument = gql`
query LiquidityProviderFeeShare($marketId: ID!) {
market(id: $marketId) {
id
data {
market {
id
}
liquidityProviderFeeShare {
...LiquidityProviderFeeShareFields
export const LiquidityProvidersDocument = gql`
query LiquidityProviders($marketId: ID!) {
liquidityProviders(marketId: $marketId) {
edges {
node {
...LiquidityProviderFields
}
}
}
}
${LiquidityProviderFeeShareFieldsFragmentDoc}`;
${LiquidityProviderFieldsFragmentDoc}`;
/**
* __useLiquidityProviderFeeShareQuery__
* __useLiquidityProvidersQuery__
*
* To run a query within a React component, call `useLiquidityProviderFeeShareQuery` and pass it any options that fit your needs.
* When your component renders, `useLiquidityProviderFeeShareQuery` returns an object from Apollo Client that contains loading, error, and data properties
* To run a query within a React component, call `useLiquidityProvidersQuery` and pass it any options that fit your needs.
* When your component renders, `useLiquidityProvidersQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useLiquidityProviderFeeShareQuery({
* const { data, loading, error } = useLiquidityProvidersQuery({
* variables: {
* marketId: // value for 'marketId'
* },
* });
*/
export function useLiquidityProviderFeeShareQuery(baseOptions: Apollo.QueryHookOptions<LiquidityProviderFeeShareQuery, LiquidityProviderFeeShareQueryVariables>) {
export function useLiquidityProvidersQuery(baseOptions: Apollo.QueryHookOptions<LiquidityProvidersQuery, LiquidityProvidersQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<LiquidityProviderFeeShareQuery, LiquidityProviderFeeShareQueryVariables>(LiquidityProviderFeeShareDocument, options);
return Apollo.useQuery<LiquidityProvidersQuery, LiquidityProvidersQueryVariables>(LiquidityProvidersDocument, options);
}
export function useLiquidityProviderFeeShareLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<LiquidityProviderFeeShareQuery, LiquidityProviderFeeShareQueryVariables>) {
export function useLiquidityProvidersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<LiquidityProvidersQuery, LiquidityProvidersQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<LiquidityProviderFeeShareQuery, LiquidityProviderFeeShareQueryVariables>(LiquidityProviderFeeShareDocument, options);
return Apollo.useLazyQuery<LiquidityProvidersQuery, LiquidityProvidersQueryVariables>(LiquidityProvidersDocument, options);
}
export type LiquidityProviderFeeShareQueryHookResult = ReturnType<typeof useLiquidityProviderFeeShareQuery>;
export type LiquidityProviderFeeShareLazyQueryHookResult = ReturnType<typeof useLiquidityProviderFeeShareLazyQuery>;
export type LiquidityProviderFeeShareQueryResult = Apollo.QueryResult<LiquidityProviderFeeShareQuery, LiquidityProviderFeeShareQueryVariables>;
export type LiquidityProvidersQueryHookResult = ReturnType<typeof useLiquidityProvidersQuery>;
export type LiquidityProvidersLazyQueryHookResult = ReturnType<typeof useLiquidityProvidersLazyQuery>;
export type LiquidityProvidersQueryResult = Apollo.QueryResult<LiquidityProvidersQuery, LiquidityProvidersQueryVariables>;
@@ -1,7 +1,9 @@
import type { LiquidityProviderFeeShare } from '@vegaprotocol/types';
import { AccountType } from '@vegaprotocol/types';
import { getLiquidityProvision } from './liquidity-data-provider';
import type { LiquidityProvisionFieldsFragment } from './__generated__/MarketLiquidity';
import type {
LiquidityProviderFieldsFragment,
LiquidityProvisionFieldsFragment,
} from './__generated__/MarketLiquidity';
const input = {
liquidityProvisions: [
@@ -31,28 +33,32 @@ const input = {
__typename: 'LiquidityProvision',
} as LiquidityProvisionFieldsFragment,
],
liquidityFeeShare: [
liquidityProviders: [
{
party: {
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
__typename: 'Party',
partyId:
'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
feeShare: {
equityLikeShare: '1',
averageEntryValuation: '12064118310408958216220.7224556301338111',
__typename: 'LiquidityProviderFeeShare',
},
equityLikeShare: '1',
averageEntryValuation: '12064118310408958216220.7224556301338111',
__typename: 'LiquidityProviderFeeShare',
} as LiquidityProviderFeeShare,
} as LiquidityProviderFieldsFragment,
],
};
const result = [
{
__typename: 'LiquidityProvision',
averageEntryValuation: '12064118310408958216220.7224556301338111',
__typename: undefined,
balance: '1.8003328918633596575e+22',
commitmentAmount: '18003328918633596575000',
createdAt: '2022-12-16T09:28:29.071781Z',
equityLikeShare: '1',
feeShare: {
equityLikeShare: '1',
__typename: 'LiquidityProviderFeeShare',
averageEntryValuation: '12064118310408958216220.7224556301338111',
},
fee: '0.001',
partyId: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
party: {
__typename: 'Party',
accountsConnection: {
@@ -84,13 +90,13 @@ describe('getLiquidityProvision', () => {
it('should return correct array when correct liquidity provision parameters are provided', () => {
const data = getLiquidityProvision(
input.liquidityProvisions,
input.liquidityFeeShare
input.liquidityProviders
);
expect(data).toStrictEqual(result);
});
it('should return empty array when no liquidity provision parameters are provided', () => {
const data = getLiquidityProvision([], input.liquidityFeeShare);
const data = getLiquidityProvision([], input.liquidityProviders);
expect(data).toStrictEqual([]);
});
@@ -5,86 +5,50 @@ import {
} from '@vegaprotocol/data-provider';
import * as Schema from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import produce from 'immer';
import {
LiquidityProviderFeeShareDocument,
LiquidityProvidersDocument,
LiquidityProvisionsDocument,
LiquidityProvisionsUpdateDocument,
} from './__generated__/MarketLiquidity';
import type {
LiquidityProviderFeeShareFieldsFragment,
LiquidityProviderFeeShareQuery,
LiquidityProviderFeeShareQueryVariables,
LiquidityProviderFieldsFragment,
LiquidityProvidersQuery,
LiquidityProvidersQueryVariables,
LiquidityProvisionFieldsFragment,
LiquidityProvisionsQuery,
LiquidityProvisionsQueryVariables,
LiquidityProvisionsUpdateSubscription,
} from './__generated__/MarketLiquidity';
export const liquidityProvisionsDataProvider = makeDataProvider<
LiquidityProvisionsQuery,
LiquidityProvisionFieldsFragment[],
LiquidityProvisionsUpdateSubscription,
LiquidityProvisionsUpdateSubscription['liquidityProvisions'],
never,
never,
LiquidityProvisionsQueryVariables
>({
query: LiquidityProvisionsDocument,
subscriptionQuery: LiquidityProvisionsUpdateDocument,
update: (
data: LiquidityProvisionFieldsFragment[] | null,
deltas: LiquidityProvisionsUpdateSubscription['liquidityProvisions']
) => {
return produce(data || [], (draft) => {
deltas?.forEach((delta) => {
const index = draft.findIndex((a) => delta.id === a.id);
if (index !== -1) {
draft[index].commitmentAmount = delta.commitmentAmount;
draft[index].fee = delta.fee;
draft[index].updatedAt = delta.updatedAt;
draft[index].status = delta.status;
} else {
draft.unshift({
id: delta.id,
commitmentAmount: delta.commitmentAmount,
fee: delta.fee,
status: delta.status,
updatedAt: delta.updatedAt,
createdAt: delta.createdAt,
party: {
id: delta.partyID,
},
// TODO add accounts connection to the subscription
});
}
});
});
},
getData: (responseData: LiquidityProvisionsQuery | null) => {
return (
responseData?.market?.liquidityProvisionsConnection?.edges?.map(
(e) => e?.node
) ?? []
).filter((e) => !!e) as LiquidityProvisionFieldsFragment[];
},
getDelta: (
subscriptionData: LiquidityProvisionsUpdateSubscription
): LiquidityProvisionsUpdateSubscription['liquidityProvisions'] => {
return subscriptionData.liquidityProvisions;
).filter((n) => !!n) as LiquidityProvisionFieldsFragment[];
},
});
export const liquidityFeeShareDataProvider = makeDataProvider<
LiquidityProviderFeeShareQuery,
LiquidityProviderFeeShareFieldsFragment[],
export const lpDataProvider = makeDataProvider<
LiquidityProvidersQuery,
LiquidityProviderFieldsFragment[],
never,
never,
LiquidityProviderFeeShareQueryVariables
LiquidityProvidersQueryVariables
>({
query: LiquidityProviderFeeShareDocument,
query: LiquidityProvidersDocument,
getData: (data) => {
return data?.market?.data?.liquidityProviderFeeShare || [];
return (
data?.liquidityProviders?.edges.filter(Boolean).map((e) => e.node) ?? []
);
},
});
@@ -101,17 +65,17 @@ export const lpAggregatedDataProvider = makeDerivedDataProvider<
marketId: variables.marketId,
}),
(callback, client, variables) =>
liquidityFeeShareDataProvider(callback, client, {
lpDataProvider(callback, client, {
marketId: variables.marketId,
}),
],
(
[liquidityProvisions, liquidityFeeShare],
[liquidityProvisions, liquidityProvider],
{ filter }
): LiquidityProvisionData[] => {
return getLiquidityProvision(
liquidityProvisions,
liquidityFeeShare,
liquidityProvider,
filter
);
}
@@ -139,9 +103,17 @@ export const matchFilter = (
return true;
};
export interface LiquidityProvisionData
extends Omit<LiquidityProvisionFieldsFragment, '__typename'> {
assetDecimalPlaces?: number;
balance?: string;
averageEntryValuation?: string;
equityLikeShare?: string;
}
export const getLiquidityProvision = (
liquidityProvisions: LiquidityProvisionFieldsFragment[],
liquidityFeeShare: LiquidityProviderFeeShareFieldsFragment[],
liquidityProvider: LiquidityProviderFieldsFragment[],
filter?: Filter
): LiquidityProvisionData[] => {
return liquidityProvisions
@@ -161,10 +133,8 @@ export const getLiquidityProvision = (
return true;
})
.map((lp) => {
const feeShare = liquidityFeeShare.find(
(f) => f.party.id === lp.party.id
);
if (!feeShare) return lp;
const lpObj = liquidityProvider.find((f) => lp.party.id === f.partyId);
if (!lpObj) return lp;
const accounts = compact(lp.party.accountsConnection?.edges).map(
(e) => e.node
);
@@ -180,17 +150,9 @@ export const getLiquidityProvision = (
.toString() || '0';
return {
...lp,
averageEntryValuation: feeShare?.averageEntryValuation,
equityLikeShare: feeShare?.equityLikeShare,
...lpObj,
balance,
__typename: undefined,
};
});
};
export interface LiquidityProvisionData
extends LiquidityProvisionFieldsFragment {
assetDecimalPlaces?: number;
balance?: string;
averageEntryValuation?: string;
equityLikeShare?: string;
}
@@ -10,8 +10,10 @@ const singleRow = {
commitmentAmount: '56298653179',
fee: '0.001',
status: Schema.LiquidityProvisionStatus.STATUS_ACTIVE,
equityLikeShare: '0.5',
averageEntryValuation: '0.5',
feeShare: {
equityLikeShare: '0.5',
averageEntryValuation: '0.5',
},
supplied: '67895',
obligation: '56785',
} as unknown as LiquidityProvisionData;
@@ -41,13 +43,23 @@ describe('LiquidityTable', () => {
h.querySelector('[ref="eText"]')?.textContent?.trim()
);
const expectedHeaders = [
undefined,
undefined,
undefined,
undefined,
undefined,
'Party',
'Commitment ()',
'Share',
'Proposed fee',
'Market valuation at entry',
'Obligation',
'Supplied',
'Fee',
'Adjusted stake share',
'Share',
'Live supplied liquidity',
'Live time fraction on book',
'Live liquidity quality score (%)',
'Last time fraction on the book',
'Last fee penalty',
'Last bond penalty',
'Status',
'Created',
'Updated',
+172 -86
View File
@@ -8,9 +8,16 @@ import {
import { t } from '@vegaprotocol/i18n';
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import { AgGrid } from '@vegaprotocol/datagrid';
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import {
CopyWithTooltip,
Tooltip,
TooltipCellComponent,
VegaIcon,
VegaIconNames,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import type {
ColDef,
ColGroupDef,
ITooltipParams,
ValueFormatterParams,
} from 'ag-grid-community';
@@ -24,6 +31,20 @@ const percentageFormatter = ({ value }: ValueFormatterParams) => {
return formatNumberPercentage(new BigNumber(value).times(100), 2) || '-';
};
const copyCellRenderer = ({ value }: { value?: string | null }) => {
if (!value) return '-';
return (
<CopyWithTooltip data-testid="copy-to-clipboard" text={value}>
<button className="flex gap-1">
<Tooltip description={value}>
<span className="break-words">{truncateMiddle(value)}</span>
</Tooltip>
<VegaIcon name={VegaIconNames.COPY} size={12} />
</button>
</CopyWithTooltip>
);
};
const dateValueFormatter = ({ value }: { value?: string | null }) => {
if (!value) {
return '-';
@@ -89,103 +110,168 @@ export const LiquidityTable = ({
)}`;
};
const defs: ColDef[] = [
const defs: ColGroupDef[] = [
{
headerName: t('Party'),
field: 'party.id',
headerTooltip: t('The public key of the party making this commitment.'),
headerName: '',
children: [
{
headerName: t('Party'),
field: 'partyId',
headerTooltip: t(
'The public key of the party making this commitment.'
),
cellRenderer: copyCellRenderer,
},
],
},
{
headerName: t(`Commitment (${symbol})`),
field: 'commitmentAmount',
type: 'rightAligned',
headerTooltip: t(
'The amount committed to the market by this liquidity provider.'
),
valueFormatter: assetDecimalsQuantumFormatter,
tooltipValueGetter: assetDecimalsFormatter,
headerName: t('Commitment details'),
marryChildren: true,
children: [
{
headerName: t(`Commitment (${symbol})`),
field: 'commitmentAmount',
type: 'rightAligned',
headerTooltip: t(
'The amount committed to the market by this liquidity provider.'
),
valueFormatter: assetDecimalsQuantumFormatter,
tooltipValueGetter: assetDecimalsFormatter,
},
{
headerName: t('Obligation'),
field: 'commitmentAmount',
type: 'rightAligned',
headerTooltip: t(
`The liquidity provider's obligation to the market, calculated as the liquidity commitment amount multiplied by the value of the stake_to_ccy_volume network parameter to convert into units of liquidity volume. The obligation can be met by a combination of LP orders and limit orders on the order book.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t('Fee'),
headerTooltip: t(
'The fee percentage (per trade) proposed by each liquidity provider.'
),
field: 'fee',
type: 'rightAligned',
valueFormatter: percentageFormatter,
},
{
headerName: t('Adjusted stake share'),
field: 'feeShare.virtualStake',
type: 'rightAligned',
headerTooltip: t('The virtual stake of the liquidity provider.'),
valueFormatter: assetDecimalsQuantumFormatter,
tooltipValueGetter: assetDecimalsFormatter,
},
{
headerName: t(`Share`),
field: 'feeShare.equityLikeShare',
type: 'rightAligned',
headerTooltip: t(
'The equity-like share of liquidity of the market used to determine allocation of LP fees. Calculated based on share of total liquidity, with a premium added for length of commitment.'
),
valueFormatter: percentageFormatter,
},
],
},
{
headerName: t(`Share`),
field: 'equityLikeShare',
type: 'rightAligned',
headerTooltip: t(
'The equity-like share of liquidity of the market used to determine allocation of LP fees. Calculated based on share of total liquidity, with a premium added for length of commitment.'
),
valueFormatter: percentageFormatter,
headerName: t('Live liquidity details'),
marryChildren: true,
children: [
{
headerName: t('Live supplied liquidity'),
field: 'balance',
type: 'rightAligned',
headerTooltip: t(
`The amount of liquidity volume supplied by the LP order in order to meet the obligation. If the obligation is already met in full by other limit orders from the same Vega key the LP order is not required and this value will be zero. Also note if the target stake for the market is less than the obligation the full value of the obligation may not be required.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t(`Live time fraction on book`),
field: 'sla.currentEpochFractionOfTimeOnBook',
type: 'rightAligned',
headerTooltip: t('Current epoch fraction of time on the book.'),
valueFormatter: percentageFormatter,
},
{
headerName: t('Live liquidity quality score (%)'),
field: 'feeShare.averageScore',
type: 'rightAligned',
headerTooltip: t('The average score of the liquidity provider.'),
valueFormatter: percentageFormatter,
},
],
},
{
headerName: t('Proposed fee'),
headerTooltip: t(
'The fee percentage (per trade) proposed by each liquidity provider.'
),
field: 'fee',
type: 'rightAligned',
valueFormatter: percentageFormatter,
headerName: t('Last epoch SLA details'),
marryChildren: true,
children: [
{
headerName: t(`Last time fraction on the book`),
field: 'sla.lastEpochFractionOfTimeOnBook',
type: 'rightAligned',
headerTooltip: t('Last epoch fraction of time on the book.'),
valueFormatter: percentageFormatter,
},
{
headerName: t(`Last fee penalty`),
field: 'sla.lastEpochFeePenalty',
type: 'rightAligned',
headerTooltip: t('Last epoch fee penalty.'),
valueFormatter: percentageFormatter,
},
{
headerName: t(`Last bond penalty`),
field: 'sla.lastEpochBondPenalty',
type: 'rightAligned',
headerTooltip: t('Last epoch bond penalty.'),
valueFormatter: percentageFormatter,
},
],
},
{
headerName: t('Market valuation at entry'),
field: 'averageEntryValuation',
type: 'rightAligned',
headerTooltip: t(
'The valuation of the market at the time the liquidity commitment was made. Commitments made at a lower valuation earlier in the lifetime of the market would be expected to have a higher equity-like share if the market has grown. If a commitment is amended, value will reflect the average of the market valuations across the lifetime of the commitment.'
),
valueFormatter: assetDecimalsQuantumFormatter,
tooltipValueGetter: assetDecimalsFormatter,
},
{
headerName: t('Obligation'),
field: 'commitmentAmount',
type: 'rightAligned',
headerTooltip: t(
`The liquidity provider's obligation to the market, calculated as the liquidity commitment amount multiplied by the value of the stake_to_ccy_volume network parameter to convert into units of liquidity volume. The obligation can be met by a combination of LP orders and limit orders on the order book.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t('Supplied'),
field: 'balance',
type: 'rightAligned',
headerTooltip: t(
`The amount of liquidity volume supplied by the LP order in order to meet the obligation. If the obligation is already met in full by other limit orders from the same Vega key the LP order is not required and this value will be zero. Also note if the target stake for the market is less than the obligation the full value of the obligation may not be required.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t('Status'),
headerTooltip: t('The current status of this liquidity provision.'),
field: 'status',
valueFormatter: ({ value }) => {
if (!value) return value;
return LiquidityProvisionStatusMapping[
value as LiquidityProvisionStatus
];
},
},
{
headerName: t('Created'),
headerTooltip: t(
'The date and time this liquidity provision was created.'
),
field: 'createdAt',
type: 'rightAligned',
valueFormatter: dateValueFormatter,
},
{
headerName: t('Updated'),
headerTooltip: t(
'The date and time this liquidity provision was last updated.'
),
field: 'updatedAt',
type: 'rightAligned',
valueFormatter: dateValueFormatter,
headerName: '',
marryChildren: true,
children: [
{
headerName: t('Status'),
headerTooltip: t('The current status of this liquidity provision.'),
field: 'status',
valueFormatter: ({ value }) => {
if (!value) return value;
return LiquidityProvisionStatusMapping[
value as LiquidityProvisionStatus
];
},
},
{
headerName: t('Created'),
headerTooltip: t(
'The date and time this liquidity provision was created.'
),
field: 'createdAt',
type: 'rightAligned',
valueFormatter: dateValueFormatter,
},
{
headerName: t('Updated'),
headerTooltip: t(
'The date and time this liquidity provision was last updated.'
),
field: 'updatedAt',
type: 'rightAligned',
valueFormatter: dateValueFormatter,
},
],
},
];
return defs;
}, [assetDecimalPlaces, quantum, stakeToCcyVolume, symbol]);
return (
<AgGrid
overlayNoRowsTemplate={t('No liquidity provisions')}
+52 -31
View File
@@ -2,7 +2,7 @@ import merge from 'lodash/merge';
import * as Schema from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
import type {
LiquidityProviderFeeShareQuery,
LiquidityProvidersQuery,
LiquidityProvisionsQuery,
} from './__generated__/MarketLiquidity';
import type { LiquidityProvisionFieldsFragment } from './__generated__/MarketLiquidity';
@@ -26,40 +26,61 @@ export const liquidityProvisionsQuery = (
return merge(defaultResult, override);
};
export const liquidityProviderFeeShareQuery = (
override?: PartialDeep<LiquidityProviderFeeShareQuery>
): LiquidityProviderFeeShareQuery => {
const defaultResult: LiquidityProviderFeeShareQuery = {
market: {
id: 'market-0',
data: {
market: {
id: 'market-0',
__typename: 'Market',
export const liquidityProvidersQuery = (
override?: PartialDeep<LiquidityProvidersQuery>
): LiquidityProvidersQuery => {
const defaultResult: LiquidityProvidersQuery = {
liquidityProviders: {
edges: [
{
node: {
partyId:
'69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
marketId:
'5ddb6f1570c0ef7aea41ebfef234dbded4ce2c11722cf033954459c45c30c057',
feeShare: {
equityLikeShare: '1',
averageEntryValuation: '3570452966575.2571864668476351',
averageScore: '0',
virtualStake: '296386536856.9999884883855020',
},
sla: {
currentEpochFractionOfTimeOnBook: '0',
lastEpochFractionOfTimeOnBook: '0',
lastEpochFeePenalty: '1',
lastEpochBondPenalty: '0.05',
hysteresisPeriodFeePenalties: ['1'],
requiredLiquidity: '',
notionalVolumeBuys: '',
notionalVolumeSells: '',
},
},
},
liquidityProviderFeeShare: [
{
party: {
id: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
__typename: 'Party',
{
node: {
partyId:
'cc464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
marketId:
'5ddb6f1570c0ef7aea41ebfef234dbded4ce2c11722cf033954459c45c30c057',
feeShare: {
equityLikeShare: '1',
averageEntryValuation: '3570452966575.2571864668476351',
averageScore: '0',
virtualStake: '296386536856.9999884883855020',
},
equityLikeShare: '1',
averageEntryValuation: '68585293691.5598054356207737',
__typename: 'LiquidityProviderFeeShare',
},
{
party: {
id: 'cc464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
__typename: 'Party',
sla: {
currentEpochFractionOfTimeOnBook: '0',
lastEpochFractionOfTimeOnBook: '0',
lastEpochFeePenalty: '1',
lastEpochBondPenalty: '0.05',
hysteresisPeriodFeePenalties: ['1'],
requiredLiquidity: '',
notionalVolumeBuys: '',
notionalVolumeSells: '',
},
equityLikeShare: '1',
averageEntryValuation: '68585293691.5598054356207737',
__typename: 'LiquidityProviderFeeShare',
},
],
__typename: 'MarketData',
},
__typename: 'Market',
},
],
},
};
return merge(defaultResult, override);
@@ -153,6 +153,8 @@ export const NetworkParams = {
'spam_protection_minimumWithdrawalQuantumMultiple',
spam_protection_voting_min_tokens: 'spam_protection_voting_min_tokens',
spam_protection_proposal_min_tokens: 'spam_protection_proposal_min_tokens',
market_fee_factors_infrastructureFee: 'market_fee_factors_infrastructureFee',
market_fee_factors_makerFee: 'market_fee_factors_makerFee',
market_liquidity_targetstake_triggering_ratio:
'market_liquidity_targetstake_triggering_ratio',
market_liquidity_bondPenaltyParameter:
+5
View File
@@ -49,6 +49,11 @@ query EstimatePosition(
openVolume: $openVolume
orders: $orders
collateralAvailable: $collateralAvailable
# Everywhere in the codebase we expect price values of the underlying to have the right
# number of digits for formatting with market.decimalPlaces. By default the estimatePosition
# query will return a full value requiring formatting using asset.decimals. For consistency
# we can set this variable to true so that we can format with market.decimalPlaces
scaleLiquidationPriceToMarketDecimals: true
) {
margin {
worstCase {
+1
View File
@@ -130,6 +130,7 @@ export const EstimatePositionDocument = gql`
openVolume: $openVolume
orders: $orders
collateralAvailable: $collateralAvailable
scaleLiquidationPriceToMarketDecimals: true
) {
margin {
worstCase {
@@ -0,0 +1,85 @@
import { MockedProvider } from '@apollo/client/testing';
import type { MockedResponse } from '@apollo/client/testing';
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { EstimatePositionDocument } from './__generated__/Positions';
import type { EstimatePositionQuery } from './__generated__/Positions';
import { LiquidationPrice } from './liquidation-price';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
describe('LiquidationPrice', () => {
const props = {
marketId: 'market-id',
openVolume: '100',
collateralAvailable: '1000',
decimalPlaces: 2,
};
const worstCaseOpenVolume = '200';
const bestCaseOpenVolume = '100';
const mock: MockedResponse<EstimatePositionQuery> = {
request: {
query: EstimatePositionDocument,
variables: {
marketId: props.marketId,
openVolume: props.openVolume,
collateralAvailable: props.collateralAvailable,
},
},
result: {
data: {
estimatePosition: {
margin: {
worstCase: {
maintenanceLevel: '100',
searchLevel: '100',
initialLevel: '100',
collateralReleaseLevel: '100',
},
bestCase: {
maintenanceLevel: '100',
searchLevel: '100',
initialLevel: '100',
collateralReleaseLevel: '100',
},
},
liquidation: {
worstCase: {
open_volume_only: worstCaseOpenVolume,
including_buy_orders: '100',
including_sell_orders: '100',
},
bestCase: {
open_volume_only: bestCaseOpenVolume,
including_buy_orders: '100',
including_sell_orders: '100',
},
},
},
},
},
};
it('correctly formats best and worst case values for the tooltip', async () => {
render(
<MockedProvider mocks={[mock]}>
<LiquidationPrice {...props} />
</MockedProvider>
);
expect(screen.getByText('-')).toBeInTheDocument();
const el = await screen.findByTestId('liquidation-price');
expect(el).toHaveTextContent(
addDecimalsFormatNumber(worstCaseOpenVolume, props.decimalPlaces)
);
await userEvent.hover(el);
const tooltip = within(await screen.findByRole('tooltip'));
expect(
tooltip.getByText('Worst case').nextElementSibling
).toHaveTextContent(
addDecimalsFormatNumber(worstCaseOpenVolume, props.decimalPlaces)
);
expect(tooltip.getByText('Best case').nextElementSibling).toHaveTextContent(
addDecimalsFormatNumber(bestCaseOpenVolume, props.decimalPlaces)
);
});
});
+4 -22
View File
@@ -8,20 +8,12 @@ export const LiquidationPrice = ({
openVolume,
collateralAvailable,
decimalPlaces,
formatDecimals,
}: {
marketId: string;
openVolume: string;
collateralAvailable: string;
decimalPlaces: number;
formatDecimals: number;
}) => {
// NOTE!
//
// The estimate order query API gives us the liquidation price unformatted but expecting to be converted
// using asset decimal placse.
//
// We need to convert it with asset decimals, but display it formatted with market decimals precision until the API changes.
const { data: currentData, previousData } = useEstimatePositionQuery({
variables: {
marketId,
@@ -38,21 +30,11 @@ export const LiquidationPrice = ({
return <span>-</span>;
}
let bestCase = '-';
let worstCase = '-';
let bestCase = data.estimatePosition.liquidation.bestCase.open_volume_only;
let worstCase = data.estimatePosition.liquidation.worstCase.open_volume_only;
bestCase =
data.estimatePosition?.liquidation?.bestCase.open_volume_only.replace(
/\..*/,
''
);
worstCase =
data.estimatePosition?.liquidation?.worstCase.open_volume_only.replace(
/\..*/,
''
);
worstCase = addDecimalsFormatNumber(worstCase, decimalPlaces, formatDecimals);
bestCase = addDecimalsFormatNumber(bestCase, decimalPlaces, formatDecimals);
worstCase = addDecimalsFormatNumber(worstCase, decimalPlaces, decimalPlaces);
bestCase = addDecimalsFormatNumber(bestCase, decimalPlaces, decimalPlaces);
return (
<Tooltip
+1 -5
View File
@@ -339,16 +339,12 @@ export const PositionsTable = ({
if (!data) {
return '-';
}
// The estimate order query API gives us the liquidation price unformatted but expecting
// conversion using asset decimals. We need to convert it with asset decimals, but format
// it with market decimals precision until the API changes.
return (
<LiquidationPrice
marketId={data.marketId}
openVolume={data.openVolume}
collateralAvailable={data.totalBalance}
decimalPlaces={data.assetDecimals}
formatDecimals={data.marketDecimalPlaces}
decimalPlaces={data.marketDecimalPlaces}
/>
);
},
@@ -53,7 +53,10 @@ export const Tooltip = ({
className={tooltipContentClasses}
sideOffset={sideOffset}
>
<div className="relative z-0" data-testid="tooltip-content">
<div
className="relative z-0 break-words"
data-testid="tooltip-content"
>
{description}
</div>
</Content>
@@ -1,4 +1,4 @@
import { act, fireEvent, render, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { generateAccount, generateAsset } from './test-helpers';
import type { WithdrawManagerProps } from './withdraw-manager';
@@ -57,10 +57,18 @@ describe('WithdrawManager', () => {
);
it('calls submit if valid form submission', async () => {
// 1002-WITH-002
// 1002-WITH-003
const { container } = render(generateJsx(props));
await act(async () => {
await submitValid(container);
});
const select = container.querySelector('select[name="asset"]') as Element;
await userEvent.selectOptions(select, props.assets[0].id);
await userEvent.clear(screen.getByLabelText('To (Ethereum address)'));
await userEvent.type(
screen.getByLabelText('To (Ethereum address)'),
ethereumAddress
);
await userEvent.type(screen.getByLabelText('Amount'), '0.01');
await userEvent.click(screen.getByTestId('submit-withdrawal'));
expect(props.submit).toHaveBeenCalledWith({
amount: '1000',
asset: props.assets[0].id,
@@ -70,58 +78,56 @@ describe('WithdrawManager', () => {
});
it('validates correctly', async () => {
render(generateJsx(props));
// 1002-WITH-010
// 1002-WITH-005
// 1002-WITH-008
// 1002-WITH-018
const { container } = render(generateJsx(props));
// Set other fields to be valid
fireEvent.change(screen.getByLabelText('Asset'), {
target: { value: props.assets[0].id },
});
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
target: { value: ethereumAddress },
});
const select = container.querySelector('select[name="asset"]') as Element;
await userEvent.selectOptions(select, props.assets[0].id);
expect(screen.getByTestId('connect-eth-wallet-btn')).toBeInTheDocument();
await userEvent.type(
screen.getByLabelText('To (Ethereum address)'),
ethereumAddress
);
// Min amount
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '0.00000001' },
});
fireEvent.submit(screen.getByTestId('withdraw-form'));
await userEvent.clear(screen.getByLabelText('Amount'));
await userEvent.type(screen.getByLabelText('Amount'), '0.00000001');
await userEvent.click(screen.getByTestId('submit-withdrawal'));
expect(
await screen.findByText('Value is below minimum')
).toBeInTheDocument();
expect(props.submit).not.toBeCalled();
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '0.00001' },
});
await userEvent.clear(screen.getByLabelText('Amount'));
await userEvent.type(screen.getByLabelText('Amount'), '0.00001');
// Max amount (balance is 1)
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '2' },
});
fireEvent.submit(screen.getByTestId('withdraw-form'));
await userEvent.clear(screen.getByLabelText('Amount'));
await userEvent.type(screen.getByLabelText('Amount'), '2');
await userEvent.click(screen.getByTestId('submit-withdrawal'));
expect(
await screen.findByText('Insufficient amount in account')
).toBeInTheDocument();
expect(props.submit).not.toBeCalled();
});
it('can set amount using use maximum button', async () => {
// 1002-WITH-004
render(generateJsx(props));
const submitValid = async (container: HTMLElement) => {
const select = container.querySelector('select[name="asset"]') as Element;
await userEvent.selectOptions(select, props.assets[0].id);
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
target: { value: ethereumAddress },
});
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '0.01' },
});
fireEvent.submit(screen.getByTestId('withdraw-form'));
};
await userEvent.click(screen.getByTestId('use-maximum'));
expect(screen.getByTestId('amount-input')).toHaveValue(1);
});
it('shows withdraw delay notification if amount greater than threshold', async () => {
render(generateJsx(props));
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '1001' },
});
await userEvent.type(screen.getByLabelText('Amount'), '1001');
expect(
await screen.findByTestId('amount-withdrawal-delay-notification')
).toBeInTheDocument();
@@ -130,9 +136,7 @@ describe('WithdrawManager', () => {
it('shows withdraw delay notification if threshold is 0', async () => {
withdrawAsset.threshold = new BigNumber(0);
render(generateJsx(props));
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '0.01' },
});
await userEvent.type(screen.getByLabelText('Amount'), '0.01');
expect(
await screen.findByTestId('withdrawals-delay-notification')
).toBeInTheDocument();
+1 -1
View File
@@ -71,7 +71,7 @@
"jsondiffpatch": "^0.4.1",
"lodash": "^4.17.21",
"next": "13.3.0",
"pennant": "1.13.4",
"pennant": "1.14.0",
"react": "18.2.0",
"react-copy-to-clipboard": "^5.0.4",
"react-dom": "18.2.0",
+4 -4
View File
@@ -20500,10 +20500,10 @@ pend@~1.2.0:
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
pennant@1.13.4:
version "1.13.4"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.13.4.tgz#33a5f3413634a2341a7b91c917f023c59ecc44c7"
integrity sha512-sqwkUiYHxmS97RY8jToMfgR9ePcEr5PWVQu9BPrhdUIa1Q/NztE36SWM5tVIsPPTx3pPIHYTNEVJm02Ubl8cZQ==
pennant@1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.14.0.tgz#4100c25a6d836d6f0ff425181fb6f812f9fe5778"
integrity sha512-9H0zWzFUSbD1BlDXnHFmKwkAxXGb1xTxjkUD+RwaMygtSwPXzQEyk2ScVyMqxdcz0RuJmI5HCVmZTOjdr1NwuA==
dependencies:
"@babel/runtime" "^7.13.10"
"@d3fc/d3fc-technical-indicator" "^8.0.1"