Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61aa45a9ed | ||
|
|
4c95db5fb3 | ||
|
|
3072b7824f | ||
|
|
0580e90171 | ||
|
|
c440abc77d | ||
|
|
fbafc726a4 | ||
|
|
dd1890d8c6 | ||
|
|
c8e624eaba | ||
|
|
cc6629ad27 | ||
|
|
9838efa00e | ||
|
|
dac7142a98 | ||
|
|
1397aafd25 | ||
|
|
b192603e57 | ||
|
|
8c2c8d987c | ||
|
|
52c96794f7 | ||
|
|
58ce016d4d | ||
|
|
ac16eb06f5 | ||
|
|
3319871617 | ||
|
|
37e850304a | ||
|
|
655131c744 | ||
|
|
f1cb1b9408 | ||
|
|
891ac527a2 | ||
|
|
546087fb9f | ||
|
|
f7a82d33ba | ||
|
|
d85f413e41 |
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -28,9 +28,10 @@ const headers = [
|
||||
'Adjusted stake share',
|
||||
'Share',
|
||||
'Live supplied liquidity',
|
||||
'Live time fraction on book',
|
||||
'Fees accrued this epoch',
|
||||
'Live time on book',
|
||||
'Live liquidity quality score (%)',
|
||||
'Last time fraction on the book',
|
||||
'Last time on the book',
|
||||
'Last fee penalty',
|
||||
'Last bond penalty',
|
||||
'Status',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
);
|
||||
+4
-8
@@ -3,17 +3,16 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
|
||||
NX_VEGA_ENV=TESTNET
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
|
||||
NX_VEGA_ENV=STAGNET1
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
|
||||
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
|
||||
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
|
||||
NX_VEGA_TOKEN_URL=https://governance.stagnet1.vega.rocks
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
|
||||
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
|
||||
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
|
||||
@@ -26,6 +25,3 @@ NX_ICEBERG_ORDERS=true
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_REFERRALS=true
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Routes } from '../../lib/links';
|
||||
import { useTransactionEventSubscription } from '@vegaprotocol/web3';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Statistics } from './referral-statistics';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
|
||||
const RELOAD_DELAY = 3000;
|
||||
|
||||
@@ -32,6 +33,7 @@ const validateCode = (value: string) => {
|
||||
};
|
||||
|
||||
export const ApplyCodeForm = () => {
|
||||
const program = useReferralProgram();
|
||||
const navigate = useNavigate();
|
||||
const openWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
@@ -237,7 +239,7 @@ export const ApplyCodeForm = () => {
|
||||
{previewData ? (
|
||||
<div className="mt-10">
|
||||
<h2 className="text-2xl mb-5">{t('You are joining')}</h2>
|
||||
<Statistics data={previewData} as="referee" />
|
||||
<Statistics data={previewData} program={program} as="referee" />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
query Referees($code: ID!, $aggregationDays: Int) {
|
||||
referralSetReferees(id: $code, aggregationDays: $aggregationDays) {
|
||||
query Referees($code: ID!, $aggregationEpochs: Int) {
|
||||
referralSetReferees(id: $code, aggregationEpochs: $aggregationEpochs) {
|
||||
edges {
|
||||
node {
|
||||
referralSetId
|
||||
|
||||
@@ -10,6 +10,7 @@ query ReferralSetStats($code: ID!, $epoch: Int) {
|
||||
referralSetRunningNotionalTakerVolume
|
||||
rewardsMultiplier
|
||||
rewardsFactorMultiplier
|
||||
referrerTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type RefereesQueryVariables = Types.Exact<{
|
||||
code: Types.Scalars['ID'];
|
||||
aggregationDays?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||
aggregationEpochs?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ export type RefereesQuery = { __typename?: 'Query', referralSetReferees: { __typ
|
||||
|
||||
|
||||
export const RefereesDocument = gql`
|
||||
query Referees($code: ID!, $aggregationDays: Int) {
|
||||
referralSetReferees(id: $code, aggregationDays: $aggregationDays) {
|
||||
query Referees($code: ID!, $aggregationEpochs: Int) {
|
||||
referralSetReferees(id: $code, aggregationEpochs: $aggregationEpochs) {
|
||||
edges {
|
||||
node {
|
||||
referralSetId
|
||||
@@ -42,7 +42,7 @@ export const RefereesDocument = gql`
|
||||
* const { data, loading, error } = useRefereesQuery({
|
||||
* variables: {
|
||||
* code: // value for 'code'
|
||||
* aggregationDays: // value for 'aggregationDays'
|
||||
* aggregationEpochs: // value for 'aggregationEpochs'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
+2
-1
@@ -9,7 +9,7 @@ export type ReferralSetStatsQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string } } | null> } };
|
||||
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string, referrerTakerVolume: string } } | null> } };
|
||||
|
||||
|
||||
export const ReferralSetStatsDocument = gql`
|
||||
@@ -25,6 +25,7 @@ export const ReferralSetStatsDocument = gql`
|
||||
referralSetRunningNotionalTakerVolume
|
||||
rewardsMultiplier
|
||||
rewardsFactorMultiplier
|
||||
referrerTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import compact from 'lodash/compact';
|
||||
import type { ReferralSetsQueryVariables } from './__generated__/ReferralSets';
|
||||
import { useReferralSetsQuery } from './__generated__/ReferralSets';
|
||||
|
||||
const DEFAULT_AGGREGATION_DAYS = 30;
|
||||
export const DEFAULT_AGGREGATION_DAYS = 30;
|
||||
|
||||
export type Role = 'referrer' | 'referee';
|
||||
type UseReferralArgs = (
|
||||
| { code: string }
|
||||
| { pubKey: string | null; role: Role }
|
||||
) & {
|
||||
aggregationDays?: number;
|
||||
aggregationEpochs?: number;
|
||||
};
|
||||
|
||||
const prepareVariables = (
|
||||
@@ -70,9 +70,9 @@ export const useReferral = (args: UseReferralArgs) => {
|
||||
} = useRefereesQuery({
|
||||
variables: {
|
||||
code: referralSet?.id as string,
|
||||
aggregationDays:
|
||||
args.aggregationDays != null
|
||||
? args.aggregationDays
|
||||
aggregationEpochs:
|
||||
args.aggregationEpochs != null
|
||||
? args.aggregationEpochs
|
||||
: DEFAULT_AGGREGATION_DAYS,
|
||||
},
|
||||
skip: !referralSet?.id,
|
||||
|
||||
@@ -8,22 +8,17 @@ 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">
|
||||
{t('Earn commission & stake rewards')}
|
||||
</h1>
|
||||
<p className="text-lg mb-10">
|
||||
{t(
|
||||
'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">
|
||||
{t(
|
||||
'Any friends that join using the code will receive discounts off trading fees.'
|
||||
'Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.'
|
||||
)}
|
||||
</p>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useReferral } from './hooks/use-referral';
|
||||
import { DEFAULT_AGGREGATION_DAYS, useReferral } from './hooks/use-referral';
|
||||
import { CreateCodeContainer } from './create-code-form';
|
||||
import classNames from 'classnames';
|
||||
import { Table } from './table';
|
||||
@@ -27,25 +27,30 @@ import { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
|
||||
export const ReferralStatistics = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const program = useReferralProgram();
|
||||
|
||||
const { data: referee } = useReferral({
|
||||
pubKey,
|
||||
role: 'referee',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
});
|
||||
const { data: referrer } = useReferral({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
});
|
||||
|
||||
if (referee?.code) {
|
||||
return <Statistics data={referee} as="referee" />;
|
||||
return <Statistics data={referee} program={program} as="referee" />;
|
||||
}
|
||||
|
||||
if (referrer?.code) {
|
||||
return <Statistics data={referrer} as="referrer" />;
|
||||
return <Statistics data={referrer} program={program} as="referrer" />;
|
||||
}
|
||||
|
||||
return <CreateCodeContainer />;
|
||||
@@ -53,14 +58,16 @@ export const ReferralStatistics = () => {
|
||||
|
||||
export const Statistics = ({
|
||||
data,
|
||||
program,
|
||||
as,
|
||||
}: {
|
||||
data: NonNullable<ReturnType<typeof useReferral>['data']>;
|
||||
program: ReturnType<typeof useReferralProgram>;
|
||||
as: 'referrer' | 'referee';
|
||||
}) => {
|
||||
const { benefitTiers, details } = program;
|
||||
const { data: epochData } = useCurrentEpochInfoQuery();
|
||||
const { stakeAvailable } = useStakeAvailable();
|
||||
const { benefitTiers } = useReferralProgram();
|
||||
const { data: statsData } = useReferralSetStatsQuery({
|
||||
variables: {
|
||||
code: data.code,
|
||||
@@ -71,6 +78,13 @@ export const Statistics = ({
|
||||
|
||||
const currentEpoch = Number(epochData?.epoch.id);
|
||||
|
||||
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
});
|
||||
|
||||
const stats =
|
||||
statsData?.referralSetStats.edges &&
|
||||
compact(removePaginationWrapper(statsData.referralSetStats.edges));
|
||||
@@ -86,10 +100,13 @@ export const Statistics = ({
|
||||
const runningVolumeValue = statsAvailable
|
||||
? Number(statsAvailable.referralSetRunningNotionalTakerVolume)
|
||||
: 0;
|
||||
const referrerVolumeValue = statsAvailable
|
||||
? Number(statsAvailable.referrerTakerVolume)
|
||||
: 0;
|
||||
const multiplier = statsAvailable
|
||||
? Number(statsAvailable.rewardsMultiplier)
|
||||
: 1;
|
||||
const finalCommissionValue = !isNaN(multiplier)
|
||||
const finalCommissionValue = isNaN(multiplier)
|
||||
? baseCommissionValue
|
||||
: multiplier * baseCommissionValue;
|
||||
|
||||
@@ -102,9 +119,9 @@ export const Statistics = ({
|
||||
!isNaN(t.discountFactor) &&
|
||||
t.discountFactor === discountFactorValue
|
||||
);
|
||||
const nextBenefitTierValue =
|
||||
currentBenefitTierValue &&
|
||||
benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1);
|
||||
const nextBenefitTierValue = currentBenefitTierValue
|
||||
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1)
|
||||
: maxBy(benefitTiers, (bt) => bt.tier); // max tier number is lowest tier
|
||||
const epochsValue =
|
||||
!isNaN(currentEpoch) && refereeInfo?.atEpoch
|
||||
? currentEpoch - refereeInfo?.atEpoch
|
||||
@@ -117,7 +134,13 @@ export const Statistics = ({
|
||||
: 0;
|
||||
|
||||
const baseCommissionTile = (
|
||||
<StatTile title={t('Base commission rate')}>
|
||||
<StatTile
|
||||
title={t('Base commission rate')}
|
||||
description={t('(Combined set volume %s over last %s epochs)', [
|
||||
compactNumFormat.format(runningVolumeValue),
|
||||
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString(),
|
||||
])}
|
||||
>
|
||||
{baseCommissionValue * 100}%
|
||||
</StatTile>
|
||||
);
|
||||
@@ -133,7 +156,16 @@ export const Statistics = ({
|
||||
</StatTile>
|
||||
);
|
||||
const finalCommissionTile = (
|
||||
<StatTile title={t('Final commission rate')}>
|
||||
<StatTile
|
||||
title={t('Final commission rate')}
|
||||
description={
|
||||
!isNaN(multiplier)
|
||||
? `(${baseCommissionValue * 100}% ⨉ ${multiplier} = ${
|
||||
finalCommissionValue * 100
|
||||
}%)`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{finalCommissionValue * 100}%
|
||||
</StatTile>
|
||||
);
|
||||
@@ -142,12 +174,21 @@ export const Statistics = ({
|
||||
<StatTile title={t('Number of traders')}>{numberOfTradersValue}</StatTile>
|
||||
);
|
||||
|
||||
const codeTile = <CodeTile code={data?.code} />;
|
||||
const createdAtTile = (
|
||||
<StatTile title={t('Created at')}>
|
||||
<span className="text-3xl">
|
||||
{getDateFormat().format(new Date(data.createdAt))}
|
||||
</span>
|
||||
const codeTile = (
|
||||
<CodeTile
|
||||
code={data?.code}
|
||||
createdAt={getDateFormat().format(new Date(data.createdAt))}
|
||||
/>
|
||||
);
|
||||
|
||||
const referrerVolumeTile = (
|
||||
<StatTile
|
||||
title={t(
|
||||
'My volume (last %s epochs)',
|
||||
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString()
|
||||
)}
|
||||
>
|
||||
{compactNumFormat.format(referrerVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
@@ -156,7 +197,10 @@ export const Statistics = ({
|
||||
.reduce((all, r) => all.plus(r), new BigNumber(0));
|
||||
const totalCommissionTile = (
|
||||
<StatTile
|
||||
title={t('Total commission (last 30 days)')}
|
||||
title={t(
|
||||
'Total commission (last %s epochs)',
|
||||
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString()
|
||||
)}
|
||||
description={t('(qUSD)')}
|
||||
>
|
||||
{getNumberFormat(0).format(Number(totalCommissionValue))}
|
||||
@@ -173,23 +217,16 @@ export const Statistics = ({
|
||||
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{codeTile}
|
||||
{createdAtTile}
|
||||
{referrerVolumeTile}
|
||||
{numberOfTradersTile}
|
||||
{totalCommissionTile}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
});
|
||||
|
||||
const currentBenefitTierTile = (
|
||||
<StatTile title={t('Current tier')}>
|
||||
{currentBenefitTierValue?.tier || '-'}
|
||||
{currentBenefitTierValue?.tier || 'None'}
|
||||
</StatTile>
|
||||
);
|
||||
const discountFactorTile = (
|
||||
@@ -204,14 +241,24 @@ export const Statistics = ({
|
||||
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
|
||||
);
|
||||
const nextTierVolumeTile = (
|
||||
<StatTile title={t('Volume to next tier')}>
|
||||
<StatTile
|
||||
title={t(
|
||||
'Volume to next tier %s',
|
||||
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
|
||||
)}
|
||||
>
|
||||
{nextBenefitTierVolumeValue <= 0
|
||||
? '0'
|
||||
: compactNumFormat.format(nextBenefitTierVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
const nextTierEpochsTile = (
|
||||
<StatTile title={t('Epochs to next tier')}>
|
||||
<StatTile
|
||||
title={t(
|
||||
'Epochs to next tier %s',
|
||||
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
|
||||
)}
|
||||
>
|
||||
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
|
||||
</StatTile>
|
||||
);
|
||||
@@ -255,7 +302,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">{t('Referees')}</h2>
|
||||
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
|
||||
<div
|
||||
className={classNames(
|
||||
collapsed && [
|
||||
@@ -281,10 +328,23 @@ export const Statistics = ({
|
||||
columns={[
|
||||
{ name: 'party', displayName: t('Trader') },
|
||||
{ name: 'joined', displayName: t('Date Joined') },
|
||||
{ name: 'volume', displayName: t('Volume (last 30 days)') },
|
||||
{
|
||||
name: 'volume',
|
||||
displayName: t(
|
||||
'Volume (last %s epochs)',
|
||||
(
|
||||
details?.windowLength || DEFAULT_AGGREGATION_DAYS
|
||||
).toString()
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'commission',
|
||||
displayName: t('Commission earned (last 30 days)'),
|
||||
displayName: t(
|
||||
'Commission earned (last %s epochs)',
|
||||
(
|
||||
details?.windowLength || DEFAULT_AGGREGATION_DAYS
|
||||
).toString()
|
||||
),
|
||||
},
|
||||
]}
|
||||
data={sortBy(
|
||||
|
||||
@@ -109,6 +109,7 @@ export const TiersContainer = () => {
|
||||
<Loading variant="large" />
|
||||
) : (
|
||||
<TiersTable
|
||||
windowLength={details?.windowLength}
|
||||
data={benefitTiers.map((bt) => ({
|
||||
...bt,
|
||||
tierElement: (
|
||||
@@ -162,6 +163,7 @@ const StakingTiers = ({
|
||||
|
||||
const TiersTable = ({
|
||||
data,
|
||||
windowLength,
|
||||
}: {
|
||||
data: Array<{
|
||||
tier: number;
|
||||
@@ -170,6 +172,7 @@ const TiersTable = ({
|
||||
discount: string;
|
||||
volume: string;
|
||||
}>;
|
||||
windowLength?: number;
|
||||
}) => {
|
||||
return (
|
||||
<Table
|
||||
@@ -181,7 +184,15 @@ const TiersTable = ({
|
||||
tooltip: t('A percentage of commission earned by the referrer'),
|
||||
},
|
||||
{ name: 'discount', displayName: t('Referrer trading discount') },
|
||||
{ name: 'volume', displayName: t('Min. trading volume') },
|
||||
{
|
||||
name: 'volume',
|
||||
displayName: t(
|
||||
'Min. trading volume %s',
|
||||
windowLength
|
||||
? t('(last %s epochs)', windowLength.toString())
|
||||
: undefined
|
||||
),
|
||||
},
|
||||
{ name: 'epochs', displayName: t('Min. epochs') },
|
||||
]}
|
||||
data={data.map((d) => ({
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { Button } from './buttons';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export const Tile = ({
|
||||
className,
|
||||
@@ -54,13 +55,18 @@ const FADE_OUT_STYLE = classNames(
|
||||
|
||||
export const CodeTile = ({
|
||||
code,
|
||||
createdAt,
|
||||
className,
|
||||
}: {
|
||||
code: string;
|
||||
createdAt?: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<StatTile title="Your referral code">
|
||||
<StatTile
|
||||
title={t('Your referral code')}
|
||||
description={createdAt ? t('(Created at: %s)', createdAt) : undefined}
|
||||
>
|
||||
<div className="flex gap-2 items-center justify-between">
|
||||
<Tooltip
|
||||
description={
|
||||
@@ -82,7 +88,7 @@ export const CodeTile = ({
|
||||
</Tooltip>
|
||||
<CopyWithTooltip text={code}>
|
||||
<Button className="text-sm no-underline !py-0 !px-0 h-fit !bg-transparent">
|
||||
<span className="sr-only">Copy</span>
|
||||
<span className="sr-only">{t('Copy')}</span>
|
||||
<VegaIcon size={24} name={VegaIconNames.COPY} />
|
||||
</Button>
|
||||
</CopyWithTooltip>
|
||||
|
||||
@@ -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, formatNumberRounded } 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={formatNumberRounded(new BigNumber(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,104 @@
|
||||
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';
|
||||
import { useNavigateWithMeta } from '../../lib/hooks/use-market-click-handler';
|
||||
import { Links } from '../../lib/links';
|
||||
|
||||
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 navigateWithMeta = useNavigateWithMeta();
|
||||
|
||||
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 {
|
||||
id: m.id,
|
||||
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}
|
||||
getRowId={({ data }) => data.id}
|
||||
defaultColDef={feesTableDefaultColDef}
|
||||
domLayout="autoHeight"
|
||||
components={components}
|
||||
rowHeight={45}
|
||||
rowClass="cursor-pointer"
|
||||
onRowClicked={({ data, event }) => {
|
||||
navigateWithMeta(
|
||||
Links.MARKET(data.id),
|
||||
// @ts-ignore metaKey and ctrlKey exist
|
||||
event.metaKey || event.ctrlKey
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</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,74 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { getAdjustedFee } from './utils';
|
||||
|
||||
describe('getAdjustedFee', () => {
|
||||
it('simple', () => {
|
||||
const volumeDiscount = 0.5;
|
||||
const referralDiscount = 0.5;
|
||||
|
||||
const infraFee = 0.1;
|
||||
const makerFee = 0.1;
|
||||
const liqFee = 0.1;
|
||||
|
||||
const fees = [
|
||||
new BigNumber(infraFee),
|
||||
new BigNumber(makerFee),
|
||||
new BigNumber(liqFee),
|
||||
];
|
||||
|
||||
const discounts = [
|
||||
new BigNumber(volumeDiscount),
|
||||
new BigNumber(referralDiscount),
|
||||
];
|
||||
|
||||
// 1 - 0.5 - 0.5
|
||||
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
|
||||
|
||||
// 1 - 0.5 = 0.5
|
||||
const r = new BigNumber(1).minus(new BigNumber(referralDiscount));
|
||||
|
||||
// 0.5 * 0.5 = 0.25
|
||||
// 1 - 0.25 = 0.75
|
||||
const factor = new BigNumber(1).minus(v.times(r));
|
||||
|
||||
// 0.1 + 0.1 + 0.1 = 0.3
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
// 0.3 * 0.75 = 0.225
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
|
||||
it('combines discount factors multiplicativly', () => {
|
||||
const volumeDiscount = 0.4;
|
||||
const referralDiscount = 0.1;
|
||||
|
||||
const infraFee = 0.0005;
|
||||
const makerFee = 0.0002;
|
||||
const liqFee = 0.01;
|
||||
|
||||
const fees = [
|
||||
new BigNumber(infraFee),
|
||||
new BigNumber(makerFee),
|
||||
new BigNumber(liqFee),
|
||||
];
|
||||
|
||||
const discounts = [
|
||||
new BigNumber(volumeDiscount),
|
||||
new BigNumber(referralDiscount),
|
||||
];
|
||||
|
||||
// formula for calculating adjusted fees
|
||||
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
|
||||
const r = new BigNumber(1).minus(new BigNumber(referralDiscount));
|
||||
const factor = new BigNumber(1).minus(v.times(r));
|
||||
|
||||
// summed fees
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
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
|
||||
*
|
||||
* Formula for calculating the adjusted fees
|
||||
* total_discount_factor = 1 - (1 - volumeDiscount) * (1 - referralDiscount)
|
||||
*/
|
||||
export const getAdjustedFee = (fees: BigNumber[], discounts: BigNumber[]) => {
|
||||
const totalFee = fees.reduce((sum, f) => sum.plus(f), new BigNumber(0));
|
||||
|
||||
const combinedFactors = discounts.reduce((acc, d) => {
|
||||
return acc.times(new BigNumber(1).minus(d));
|
||||
}, new BigNumber(1));
|
||||
|
||||
const totalFactor = new BigNumber(1).minus(combinedFactors);
|
||||
|
||||
return totalFee.times(BigNumber.max(0, totalFactor)).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,
|
||||
})}
|
||||
|
||||
@@ -10,15 +10,28 @@ import {
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ExternalLink, Indicator } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
CopyWithTooltip,
|
||||
ExternalLink,
|
||||
Indicator,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
truncateMiddle,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { useCheckLiquidityStatus } from '@vegaprotocol/liquidity';
|
||||
import {
|
||||
useCheckLiquidityStatus,
|
||||
usePaidFeesQuery,
|
||||
} from '@vegaprotocol/liquidity';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
export const LiquidityHeader = () => {
|
||||
const { marketId } = useParams();
|
||||
const { data: market } = useMarket(marketId);
|
||||
const { data: marketData } = useStaticMarketData(marketId);
|
||||
const { data: feesPaidRes } = usePaidFeesQuery({
|
||||
variables: { marketId: marketId || '' },
|
||||
});
|
||||
const targetStake = marketData?.targetStake;
|
||||
const suppliedStake = marketData?.suppliedStake;
|
||||
|
||||
@@ -36,6 +49,10 @@ export const LiquidityHeader = () => {
|
||||
triggeringRatio,
|
||||
});
|
||||
|
||||
const feesObject = feesPaidRes?.paidLiquidityFees?.edges?.find(
|
||||
(e) => e?.node.marketId === marketId
|
||||
);
|
||||
|
||||
return (
|
||||
<Header
|
||||
title={
|
||||
@@ -82,9 +99,40 @@ export const LiquidityHeader = () => {
|
||||
<HeaderStat heading={t('Liquidity supplied')} testId="liquidity-supplied">
|
||||
<Indicator variant={status} /> {formatNumberPercentage(percentage, 2)}
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Market ID')} testId="liquidity-market-id">
|
||||
<div className="break-word">{marketId}</div>
|
||||
<HeaderStat
|
||||
heading={t('Fees paid')}
|
||||
description={t(
|
||||
'The amount of fees paid to liquidity providers across the whole market during the last epoch %s.',
|
||||
feesObject?.node.epoch.toString() || '-'
|
||||
)}
|
||||
testId="fees-paid"
|
||||
>
|
||||
<div>
|
||||
{feesObject?.node.totalFeesPaid
|
||||
? `${addDecimalsFormatNumber(
|
||||
feesObject?.node.totalFeesPaid,
|
||||
assetDecimalPlaces ?? 0
|
||||
)} ${symbol}`
|
||||
: '-'}
|
||||
</div>
|
||||
</HeaderStat>
|
||||
{marketId && (
|
||||
<HeaderStat heading={t('Market ID')} testId="liquidity-market-id">
|
||||
<div className="break-word">
|
||||
<CopyWithTooltip text={marketId}>
|
||||
<button
|
||||
data-testid="copy-eth-oracle-address"
|
||||
className="uppercase text-right"
|
||||
>
|
||||
<span className="flex gap-1">
|
||||
{truncateMiddle(marketId)}
|
||||
<VegaIcon name={VegaIconNames.COPY} size={16} />
|
||||
</span>
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</div>
|
||||
</HeaderStat>
|
||||
)}
|
||||
<HeaderStat heading={t('Learn more')} testId="liquidity-learn-more">
|
||||
{DocsLinks ? (
|
||||
<ExternalLink href={DocsLinks.LIQUIDITY}>
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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: (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { useUpdateNetworkParametersToasts } from '@vegaprotocol/proposals';
|
||||
import { useProposalToasts } from '@vegaprotocol/proposals';
|
||||
import { useVegaTransactionToasts } from '@vegaprotocol/web3';
|
||||
import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
|
||||
import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
|
||||
@@ -7,7 +7,7 @@ import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
|
||||
import { Links } from '../lib/links';
|
||||
|
||||
export const ToastsManager = () => {
|
||||
useUpdateNetworkParametersToasts();
|
||||
useProposalToasts();
|
||||
useVegaTransactionToasts();
|
||||
useEthereumTransactionToasts();
|
||||
useEthereumWithdrawApprovalsToasts();
|
||||
|
||||
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 |
@@ -1,31 +1,83 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { CandlesMenu } from './candles-menu';
|
||||
import {
|
||||
useCandlesChartSettingsStore,
|
||||
DEFAULT_CHART_SETTINGS,
|
||||
} from './use-candles-chart-settings';
|
||||
import { Overlay, Study, overlayLabels, studyLabels } from 'pennant';
|
||||
|
||||
describe('CandlesMenu', () => {
|
||||
it('should render with the correct default studies', async () => {
|
||||
render(<CandlesMenu />);
|
||||
|
||||
const openDropdown = async () => {
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Studies',
|
||||
name: 'Indicators',
|
||||
})
|
||||
);
|
||||
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
||||
expect(screen.getByText('Volume')).toHaveAttribute('data-state', 'checked');
|
||||
expect(screen.getByText('MACD')).toHaveAttribute('data-state', 'checked');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// clear store each time to avoid conditional testing of defaults
|
||||
useCandlesChartSettingsStore.setState({ overlays: [], studies: [] });
|
||||
});
|
||||
|
||||
it('should render with the correct default overlays', async () => {
|
||||
it.each(Object.values(Overlay))('can set %s overlay', async (overlay) => {
|
||||
render(<CandlesMenu />);
|
||||
|
||||
await openDropdown();
|
||||
|
||||
const menu = within(await screen.findByRole('menu'));
|
||||
|
||||
await userEvent.click(menu.getByText(overlayLabels[overlay as Overlay]));
|
||||
|
||||
// re-open the dropdown
|
||||
await openDropdown();
|
||||
|
||||
expect(screen.getByText(overlayLabels[overlay as Overlay])).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
});
|
||||
|
||||
it.each(Object.values(Study))('can set %s study', async (study) => {
|
||||
render(<CandlesMenu />);
|
||||
|
||||
await openDropdown();
|
||||
|
||||
const menu = within(await screen.findByRole('menu'));
|
||||
|
||||
await userEvent.click(menu.getByText(studyLabels[study as Study]));
|
||||
|
||||
// re-open the dropdown
|
||||
await openDropdown();
|
||||
|
||||
expect(screen.getByText(studyLabels[study as Study])).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render with the correct default studies and overlays', async () => {
|
||||
useCandlesChartSettingsStore.setState(DEFAULT_CHART_SETTINGS);
|
||||
|
||||
render(<CandlesMenu />);
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Overlays',
|
||||
name: 'Indicators',
|
||||
})
|
||||
);
|
||||
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
||||
expect(screen.getByText('Moving average')).toHaveAttribute(
|
||||
const menu = within(await screen.findByRole('menu'));
|
||||
|
||||
expect(menu.getByText(studyLabels.volume)).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
expect(menu.getByText(studyLabels.macd)).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
expect(menu.getByText(overlayLabels.movingAverage)).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
|
||||
@@ -107,7 +107,7 @@ export const CandlesMenu = () => {
|
||||
trigger={
|
||||
<TradingDropdownTrigger className={triggerClasses}>
|
||||
<TradingButton {...triggerButtonProps}>
|
||||
{t('Overlays')}
|
||||
{t('Indicators')}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
@@ -132,18 +132,6 @@ export const CandlesMenu = () => {
|
||||
<TradingDropdownItemIndicator />
|
||||
</TradingDropdownCheckboxItem>
|
||||
))}
|
||||
</TradingDropdownContent>
|
||||
</TradingDropdown>
|
||||
<TradingDropdown
|
||||
trigger={
|
||||
<TradingDropdownTrigger className={triggerClasses}>
|
||||
<TradingButton {...triggerButtonProps}>
|
||||
{t('Studies')}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
>
|
||||
<TradingDropdownContent align={contentAlign}>
|
||||
{Object.values(Study).map((study) => (
|
||||
<TradingDropdownCheckboxItem
|
||||
key={study}
|
||||
|
||||
@@ -24,7 +24,7 @@ const STUDY_ORDER: Study[] = [
|
||||
Study.VOLUME,
|
||||
];
|
||||
|
||||
const DEFAULT_CHART_SETTINGS = {
|
||||
export const DEFAULT_CHART_SETTINGS = {
|
||||
interval: Interval.I15M,
|
||||
type: ChartType.CANDLE,
|
||||
overlays: [Overlay.MOVING_AVERAGE],
|
||||
|
||||
@@ -3,7 +3,12 @@ import throttle from 'lodash/throttle';
|
||||
import isEqualWith from 'lodash/isEqualWith';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import type { OperationVariables } from '@apollo/client';
|
||||
import type { Subscribe, Load, UpdateCallback } from './generic-data-provider';
|
||||
import type {
|
||||
Subscribe,
|
||||
Load,
|
||||
UpdateCallback,
|
||||
PageInfo,
|
||||
} from './generic-data-provider';
|
||||
import { variablesIsEqualCustomizer } from './generic-data-provider';
|
||||
|
||||
export interface useDataProviderParams<
|
||||
@@ -12,13 +17,23 @@ export interface useDataProviderParams<
|
||||
Variables extends OperationVariables | undefined = undefined
|
||||
> {
|
||||
dataProvider: Subscribe<Data, Delta, Variables>;
|
||||
update?: ({ delta, data }: { delta?: Delta; data: Data | null }) => boolean;
|
||||
update?: ({
|
||||
delta,
|
||||
data,
|
||||
pageInfo,
|
||||
}: {
|
||||
delta?: Delta;
|
||||
data: Data | null;
|
||||
pageInfo: PageInfo | null;
|
||||
}) => boolean;
|
||||
insert?: ({
|
||||
insertionData,
|
||||
data,
|
||||
pageInfo,
|
||||
}: {
|
||||
insertionData?: Data | null;
|
||||
data: Data | null;
|
||||
pageInfo: PageInfo | null;
|
||||
}) => boolean;
|
||||
variables: Variables;
|
||||
skipUpdates?: boolean;
|
||||
@@ -30,7 +45,7 @@ export interface useDataProviderParams<
|
||||
* @param dataProvider subscribe function created by makeDataProvider
|
||||
* @param update optional function called on each delta received in subscription, if returns true updated data will be not passed from hook (component handles updates internally)
|
||||
* @param variables optional
|
||||
* @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
|
||||
* @returns state: data, loading, pageInfo, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
|
||||
*/
|
||||
export const useDataProvider = <
|
||||
Data,
|
||||
@@ -48,6 +63,7 @@ export const useDataProvider = <
|
||||
const [data, setData] = useState<Data | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(!skip);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [pageInfo, setPageInfo] = useState<PageInfo | null>(null);
|
||||
const flushRef = useRef<(() => void) | undefined>(undefined);
|
||||
const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined);
|
||||
const loadRef = useRef<Load<Data> | undefined>(undefined);
|
||||
@@ -93,6 +109,7 @@ export const useDataProvider = <
|
||||
isInsert,
|
||||
isUpdate,
|
||||
loaded,
|
||||
pageInfo,
|
||||
} = args;
|
||||
setError(error);
|
||||
setLoading(!loaded && loading);
|
||||
@@ -104,21 +121,22 @@ export const useDataProvider = <
|
||||
(skipUpdatesRef.current ||
|
||||
(!skipUpdatesRef.current &&
|
||||
updateRef.current &&
|
||||
updateRef.current({ delta, data })))
|
||||
updateRef.current({ delta, data, pageInfo })))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isInsert &&
|
||||
insertRef.current &&
|
||||
insertRef.current({ insertionData, data })
|
||||
insertRef.current({ insertionData, data, pageInfo })
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setData(data);
|
||||
setPageInfo(pageInfo);
|
||||
if (!loading && !isUpdate && updateRef.current) {
|
||||
updateRef.current({ data });
|
||||
updateRef.current({ data, pageInfo });
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -136,15 +154,13 @@ export const useDataProvider = <
|
||||
|
||||
useEffect(() => {
|
||||
setData(null);
|
||||
setPageInfo(null);
|
||||
setError(undefined);
|
||||
if (updateRef.current) {
|
||||
updateRef.current({ data: null });
|
||||
updateRef.current({ data: null, pageInfo: null });
|
||||
}
|
||||
if (skip) {
|
||||
setLoading(false);
|
||||
if (updateRef.current) {
|
||||
updateRef.current({ data: null });
|
||||
}
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -165,6 +181,7 @@ export const useDataProvider = <
|
||||
}, [client, dataProvider, callback, variables, skip]);
|
||||
return {
|
||||
data,
|
||||
pageInfo,
|
||||
loading,
|
||||
error,
|
||||
flush,
|
||||
|
||||
@@ -26,11 +26,7 @@ import {
|
||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
} from '../../constants';
|
||||
import {
|
||||
sumFees,
|
||||
sumFeesDiscounts,
|
||||
useEstimateFees,
|
||||
} from '../../hooks/use-estimate-fees';
|
||||
import { useEstimateFees } from '../../hooks/use-estimate-fees';
|
||||
import { KeyValue } from './key-value';
|
||||
import {
|
||||
Accordion,
|
||||
@@ -44,6 +40,7 @@ import {
|
||||
import classNames from 'classnames';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { FeesBreakdown } from '../fees-breakdown';
|
||||
import { getTotalDiscountFactor, getDiscountedFee } from '../discounts';
|
||||
|
||||
const emptyValue = '-';
|
||||
|
||||
@@ -63,48 +60,49 @@ export const DealTicketFeeDetails = ({
|
||||
const feeEstimate = useEstimateFees(order, isMarketInAuction);
|
||||
const asset = getAsset(market);
|
||||
const { decimals: assetDecimals, quantum } = asset;
|
||||
const totalFees = feeEstimate?.fees && sumFees(feeEstimate?.fees);
|
||||
const feesDiscounts =
|
||||
feeEstimate?.fees && sumFeesDiscounts(feeEstimate?.fees);
|
||||
|
||||
const totalPercentageDiscount =
|
||||
feesDiscounts &&
|
||||
totalFees &&
|
||||
feesDiscounts.total !== '0' &&
|
||||
totalFees !== '0' &&
|
||||
new BigNumber(feesDiscounts.total)
|
||||
.dividedBy(BigNumber.sum(totalFees, feesDiscounts.total))
|
||||
.times(100);
|
||||
const totalDiscountFactor = getTotalDiscountFactor(feeEstimate);
|
||||
const totalDiscountedFeeAmount =
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
getDiscountedFee(
|
||||
feeEstimate.totalFeeAmount,
|
||||
feeEstimate.referralDiscountFactor,
|
||||
feeEstimate.volumeDiscountFactor
|
||||
).discountedFee;
|
||||
|
||||
return (
|
||||
<KeyValue
|
||||
label={t('Fees')}
|
||||
value={
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
|
||||
totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals)}`
|
||||
}
|
||||
formattedValue={
|
||||
<>
|
||||
{totalPercentageDiscount && (
|
||||
{totalDiscountFactor && (
|
||||
<Pill size="xxs" intent={Intent.Warning} className="mr-1">
|
||||
-{formatNumberPercentage(totalPercentageDiscount, 2)}
|
||||
-
|
||||
{formatNumberPercentage(
|
||||
new BigNumber(totalDiscountFactor).multipliedBy(100),
|
||||
2
|
||||
)}
|
||||
</Pill>
|
||||
)}
|
||||
{feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(
|
||||
feeEstimate?.totalFeeAmount,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}`}
|
||||
{totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals, quantum)}`}
|
||||
</>
|
||||
}
|
||||
labelDescription={
|
||||
<>
|
||||
<span>
|
||||
<p className="mb-2">
|
||||
{t(
|
||||
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}. Fees estimated are "taker" fees and will only be payable if the order trades aggressively. Rebate equal to the maker portion will be paid to the trader if the order trades passively.`
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
<FeesBreakdown
|
||||
totalFeeAmount={feeEstimate?.totalFeeAmount}
|
||||
referralDiscountFactor={feeEstimate?.referralDiscountFactor}
|
||||
volumeDiscountFactor={feeEstimate?.volumeDiscountFactor}
|
||||
fees={feeEstimate?.fees}
|
||||
feeFactors={market.fees.factors}
|
||||
symbol={assetSymbol}
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('DealTicket', () => {
|
||||
variables: {
|
||||
partyId: 'pubKey',
|
||||
filter: { liveOnly: true },
|
||||
pagination: { first: 5000 },
|
||||
pagination: { first: 1000 },
|
||||
},
|
||||
},
|
||||
result: {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { getDiscountedFee, getTotalDiscountFactor } from './discounts';
|
||||
|
||||
describe('getDiscountedFee', () => {
|
||||
it('calculates values if volumeDiscount or referralDiscount is undefined', () => {
|
||||
expect(getDiscountedFee('100')).toEqual({
|
||||
discountedFee: '100',
|
||||
volumeDiscount: '0',
|
||||
referralDiscount: '0',
|
||||
});
|
||||
expect(getDiscountedFee('100', undefined, '0.1')).toEqual({
|
||||
discountedFee: '90',
|
||||
volumeDiscount: '10',
|
||||
referralDiscount: '0',
|
||||
});
|
||||
expect(getDiscountedFee('100', '0.1', undefined)).toEqual({
|
||||
discountedFee: '90',
|
||||
volumeDiscount: '0',
|
||||
referralDiscount: '10',
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates values using volumeDiscount or referralDiscount', () => {
|
||||
expect(getDiscountedFee('', '0.1', '0.2')).toEqual({
|
||||
discountedFee: '',
|
||||
volumeDiscount: '0',
|
||||
referralDiscount: '0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTotalDiscountFactor', () => {
|
||||
it('returns 0 if discounts are 0', () => {
|
||||
expect(
|
||||
getTotalDiscountFactor({
|
||||
volumeDiscountFactor: '0',
|
||||
referralDiscountFactor: '0',
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it('returns volumeDiscountFactor if referralDiscountFactor is 0', () => {
|
||||
expect(
|
||||
getTotalDiscountFactor({
|
||||
volumeDiscountFactor: '0.1',
|
||||
referralDiscountFactor: '0',
|
||||
})
|
||||
).toEqual(0.1);
|
||||
});
|
||||
it('returns referralDiscountFactor if volumeDiscountFactor is 0', () => {
|
||||
expect(
|
||||
getTotalDiscountFactor({
|
||||
volumeDiscountFactor: '0',
|
||||
referralDiscountFactor: '0.1',
|
||||
})
|
||||
).toEqual(0.1);
|
||||
});
|
||||
|
||||
it('calculates discount using referralDiscountFactor and volumeDiscountFactor', () => {
|
||||
expect(
|
||||
getTotalDiscountFactor({
|
||||
volumeDiscountFactor: '0.2',
|
||||
referralDiscountFactor: '0.1',
|
||||
})
|
||||
).toBeCloseTo(0.28);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
export const getDiscountedFee = (
|
||||
feeAmount: string,
|
||||
referralDiscountFactor?: string,
|
||||
volumeDiscountFactor?: string
|
||||
) => {
|
||||
if (
|
||||
((!referralDiscountFactor || referralDiscountFactor === '0') &&
|
||||
(!volumeDiscountFactor || volumeDiscountFactor === '0')) ||
|
||||
!feeAmount ||
|
||||
feeAmount === '0'
|
||||
) {
|
||||
return {
|
||||
discountedFee: feeAmount,
|
||||
volumeDiscount: '0',
|
||||
referralDiscount: '0',
|
||||
};
|
||||
}
|
||||
const referralDiscount = new BigNumber(referralDiscountFactor || '0')
|
||||
.multipliedBy(feeAmount)
|
||||
.toFixed(0, BigNumber.ROUND_FLOOR);
|
||||
const volumeDiscount = new BigNumber(volumeDiscountFactor || '0')
|
||||
.multipliedBy((BigInt(feeAmount) - BigInt(referralDiscount)).toString())
|
||||
.toFixed(0, BigNumber.ROUND_FLOOR);
|
||||
const discountedFee = (
|
||||
BigInt(feeAmount || '0') -
|
||||
BigInt(referralDiscount) -
|
||||
BigInt(volumeDiscount)
|
||||
).toString();
|
||||
return {
|
||||
referralDiscount,
|
||||
volumeDiscount,
|
||||
discountedFee,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTotalDiscountFactor = (feeEstimate?: {
|
||||
volumeDiscountFactor?: string;
|
||||
referralDiscountFactor?: string;
|
||||
}) => {
|
||||
if (!feeEstimate) {
|
||||
return 0;
|
||||
}
|
||||
const volumeFactor = Number(feeEstimate?.volumeDiscountFactor) || 0;
|
||||
const referralFactor = Number(feeEstimate?.referralDiscountFactor) || 0;
|
||||
if (!volumeFactor) {
|
||||
return referralFactor;
|
||||
}
|
||||
if (!referralFactor) {
|
||||
return volumeFactor;
|
||||
}
|
||||
return 1 - (1 - volumeFactor) * (1 - referralFactor);
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { sumFees, sumFeesDiscounts } from '../../hooks';
|
||||
import { getDiscountedFee } from '../discounts';
|
||||
|
||||
const formatValue = (
|
||||
value: string | number | null | undefined,
|
||||
@@ -24,7 +24,7 @@ const FeesBreakdownItem = ({
|
||||
decimals,
|
||||
}: {
|
||||
label: string;
|
||||
factor?: BigNumber;
|
||||
factor?: string;
|
||||
value: string;
|
||||
symbol?: string;
|
||||
decimals: number;
|
||||
@@ -43,76 +43,86 @@ const FeesBreakdownItem = ({
|
||||
);
|
||||
|
||||
export const FeesBreakdown = ({
|
||||
totalFeeAmount,
|
||||
fees,
|
||||
feeFactors,
|
||||
symbol,
|
||||
decimals,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor,
|
||||
}: {
|
||||
totalFeeAmount?: string;
|
||||
fees?: TradeFee;
|
||||
feeFactors?: FeeFactors;
|
||||
symbol?: string;
|
||||
decimals: number;
|
||||
referralDiscountFactor?: string;
|
||||
volumeDiscountFactor?: string;
|
||||
}) => {
|
||||
if (!fees) return null;
|
||||
const totalFees = sumFees(fees);
|
||||
const {
|
||||
total: totalDiscount,
|
||||
referral: referralDiscount,
|
||||
volume: volumeDiscount,
|
||||
} = sumFeesDiscounts(fees);
|
||||
if (totalFees === '0') return null;
|
||||
if (!fees || !totalFeeAmount || totalFeeAmount === '0') return null;
|
||||
|
||||
const { discountedFee: discountedInfrastructureFee } = getDiscountedFee(
|
||||
fees.infrastructureFee,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
);
|
||||
|
||||
const { discountedFee: discountedLiquidityFee } = getDiscountedFee(
|
||||
fees.liquidityFee,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
);
|
||||
|
||||
const { discountedFee: discountedMakerFee } = getDiscountedFee(
|
||||
fees.makerFee,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
);
|
||||
|
||||
const { volumeDiscount, referralDiscount } = getDiscountedFee(
|
||||
totalFeeAmount,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
);
|
||||
|
||||
return (
|
||||
<dl className="grid grid-cols-6">
|
||||
<FeesBreakdownItem
|
||||
label={t('Infrastructure fee')}
|
||||
factor={
|
||||
feeFactors?.infrastructureFee
|
||||
? new BigNumber(feeFactors?.infrastructureFee)
|
||||
: undefined
|
||||
}
|
||||
value={fees.infrastructureFee}
|
||||
factor={feeFactors?.infrastructureFee}
|
||||
value={discountedInfrastructureFee}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
|
||||
<FeesBreakdownItem
|
||||
label={t('Liquidity fee')}
|
||||
factor={
|
||||
feeFactors?.liquidityFee
|
||||
? new BigNumber(feeFactors?.liquidityFee)
|
||||
: undefined
|
||||
}
|
||||
value={fees.liquidityFee}
|
||||
factor={feeFactors?.liquidityFee}
|
||||
value={discountedLiquidityFee}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
|
||||
<FeesBreakdownItem
|
||||
label={t('Maker fee')}
|
||||
factor={
|
||||
feeFactors?.makerFee ? new BigNumber(feeFactors?.makerFee) : undefined
|
||||
}
|
||||
value={fees.makerFee}
|
||||
factor={feeFactors?.makerFee}
|
||||
value={discountedMakerFee}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
{volumeDiscount && volumeDiscount !== '0' && (
|
||||
{volumeDiscountFactor && volumeDiscount !== '0' && (
|
||||
<FeesBreakdownItem
|
||||
label={t('Volume discount')}
|
||||
factor={new BigNumber(volumeDiscount).dividedBy(
|
||||
BigNumber.sum(totalFees, totalDiscount)
|
||||
)}
|
||||
factor={volumeDiscountFactor}
|
||||
value={volumeDiscount}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
)}
|
||||
{referralDiscount && referralDiscount !== '0' && (
|
||||
{referralDiscountFactor && referralDiscount !== '0' && (
|
||||
<FeesBreakdownItem
|
||||
label={t('Referral discount')}
|
||||
factor={new BigNumber(referralDiscount).dividedBy(
|
||||
BigNumber.sum(totalFees, totalDiscount)
|
||||
)}
|
||||
factor={referralDiscountFactor}
|
||||
value={referralDiscount}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
@@ -120,8 +130,8 @@ export const FeesBreakdown = ({
|
||||
)}
|
||||
<FeesBreakdownItem
|
||||
label={t('Total fees')}
|
||||
factor={feeFactors ? sumFeesFactors(feeFactors) : undefined}
|
||||
value={totalFees}
|
||||
factor={feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined}
|
||||
value={totalFeeAmount}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
|
||||
@@ -31,4 +31,25 @@ query EstimateFees(
|
||||
}
|
||||
totalFeeAmount
|
||||
}
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(partyId: $partyId, pagination: { last: 1 }) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
runningVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(partyId: $partyId, pagination: { last: 1 }) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
referralSetRunningNotionalTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-1
@@ -15,7 +15,7 @@ export type EstimateFeesQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type EstimateFeesQuery = { __typename?: 'Query', estimateFees: { __typename?: 'FeeEstimate', totalFeeAmount: string, fees: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } } };
|
||||
export type EstimateFeesQuery = { __typename?: 'Query', estimateFees: { __typename?: 'FeeEstimate', totalFeeAmount: string, fees: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } }, epoch: { __typename?: 'Epoch', id: string }, volumeDiscountStats: { __typename?: 'VolumeDiscountStatsConnection', edges: Array<{ __typename?: 'VolumeDiscountStatsEdge', node: { __typename?: 'VolumeDiscountStats', atEpoch: number, discountFactor: string, runningVolume: string } } | null> }, referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, discountFactor: string, referralSetRunningNotionalTakerVolume: string } } | null> } };
|
||||
|
||||
|
||||
export const EstimateFeesDocument = gql`
|
||||
@@ -43,6 +43,27 @@ export const EstimateFeesDocument = gql`
|
||||
}
|
||||
totalFeeAmount
|
||||
}
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(partyId: $partyId, pagination: {last: 1}) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
runningVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(partyId: $partyId, pagination: {last: 1}) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
discountFactor
|
||||
referralSetRunningNotionalTakerVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -6,6 +6,15 @@ export const estimateFeesQuery = (
|
||||
override?: PartialDeep<EstimateFeesQuery>
|
||||
): EstimateFeesQuery => {
|
||||
const defaultResult: EstimateFeesQuery = {
|
||||
epoch: {
|
||||
id: '1',
|
||||
},
|
||||
referralSetStats: {
|
||||
edges: [],
|
||||
},
|
||||
volumeDiscountStats: {
|
||||
edges: [],
|
||||
},
|
||||
estimateFees: {
|
||||
__typename: 'FeeEstimate',
|
||||
totalFeeAmount: '0.0006',
|
||||
|
||||
@@ -5,6 +5,31 @@ import { Side, OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||
import type { EstimateFeesQuery } from './__generated__/EstimateOrder';
|
||||
|
||||
const data: EstimateFeesQuery = {
|
||||
epoch: {
|
||||
id: '2',
|
||||
},
|
||||
volumeDiscountStats: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 1,
|
||||
discountFactor: '0.1',
|
||||
runningVolume: '100',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
referralSetStats: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 1,
|
||||
discountFactor: '0.2',
|
||||
referralSetRunningNotionalTakerVolume: '100',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
estimateFees: {
|
||||
totalFeeAmount: '120',
|
||||
fees: {
|
||||
@@ -54,6 +79,8 @@ describe('useEstimateFees', () => {
|
||||
liquidityFee: '0',
|
||||
makerFee: '0',
|
||||
},
|
||||
referralDiscountFactor: '0',
|
||||
volumeDiscountFactor: '0',
|
||||
});
|
||||
expect(mockUseEstimateFeesQuery.mock.lastCall?.[0].skip).toBeTruthy();
|
||||
});
|
||||
@@ -85,6 +112,46 @@ describe('useEstimateFees', () => {
|
||||
makerFeeReferralDiscount: '5',
|
||||
makerFeeVolumeDiscount: '6',
|
||||
},
|
||||
referralDiscountFactor: '0',
|
||||
volumeDiscountFactor: '0',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns 0 discounts if discount stats are not at the current epoch', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useEstimateFees(
|
||||
{
|
||||
marketId: 'marketId',
|
||||
side: Side.SIDE_BUY,
|
||||
size: '1',
|
||||
price: '1',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
},
|
||||
true
|
||||
)
|
||||
);
|
||||
expect(result.current?.referralDiscountFactor).toEqual('0');
|
||||
expect(result.current?.volumeDiscountFactor).toEqual('0');
|
||||
});
|
||||
|
||||
it('returns discounts', () => {
|
||||
data.epoch.id = '1';
|
||||
const { result } = renderHook(() =>
|
||||
useEstimateFees(
|
||||
{
|
||||
marketId: 'marketId',
|
||||
side: Side.SIDE_BUY,
|
||||
size: '1',
|
||||
price: '1',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
},
|
||||
true
|
||||
)
|
||||
);
|
||||
|
||||
expect(result.current?.referralDiscountFactor).toEqual('0.2');
|
||||
expect(result.current?.volumeDiscountFactor).toEqual('0.1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,39 +5,22 @@ import type { EstimateFeesQuery } from './__generated__/EstimateOrder';
|
||||
import { useEstimateFeesQuery } from './__generated__/EstimateOrder';
|
||||
|
||||
const divideByTwo = (n: string) => (BigInt(n) / BigInt(2)).toString();
|
||||
export const sumFeesDiscounts = (
|
||||
fees: EstimateFeesQuery['estimateFees']['fees']
|
||||
) => {
|
||||
const volume = (
|
||||
BigInt(fees.makerFeeVolumeDiscount || '0') +
|
||||
BigInt(fees.infrastructureFeeVolumeDiscount || '0') +
|
||||
BigInt(fees.liquidityFeeVolumeDiscount || '0')
|
||||
).toString();
|
||||
const referral = (
|
||||
BigInt(fees.makerFeeReferralDiscount || '0') +
|
||||
BigInt(fees.infrastructureFeeReferralDiscount || '0') +
|
||||
BigInt(fees.liquidityFeeReferralDiscount || '0')
|
||||
).toString();
|
||||
return {
|
||||
volume,
|
||||
referral,
|
||||
total: (BigInt(volume) + BigInt(referral)).toString(),
|
||||
};
|
||||
};
|
||||
|
||||
export const sumFees = (fees: EstimateFeesQuery['estimateFees']['fees']) =>
|
||||
(
|
||||
BigInt(fees.makerFee || '0') +
|
||||
BigInt(fees.infrastructureFee || '0') +
|
||||
BigInt(fees.liquidityFee || '0')
|
||||
).toString();
|
||||
|
||||
export const useEstimateFees = (
|
||||
order?: OrderSubmissionBody['orderSubmission'],
|
||||
isMarketInAuction?: boolean
|
||||
): EstimateFeesQuery['estimateFees'] | undefined => {
|
||||
):
|
||||
| (EstimateFeesQuery['estimateFees'] & {
|
||||
referralDiscountFactor: string;
|
||||
volumeDiscountFactor: string;
|
||||
})
|
||||
| undefined => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { data } = useEstimateFeesQuery({
|
||||
const {
|
||||
data: currentData,
|
||||
previousData,
|
||||
loading,
|
||||
} = useEstimateFeesQuery({
|
||||
variables: order && {
|
||||
marketId: order.marketId,
|
||||
partyId: pubKey || '',
|
||||
@@ -50,8 +33,21 @@ export const useEstimateFees = (
|
||||
fetchPolicy: 'no-cache',
|
||||
skip: !pubKey || !order?.size || !order?.price || order.postOnly,
|
||||
});
|
||||
const data = loading ? currentData || previousData : currentData;
|
||||
const volumeDiscountFactor =
|
||||
(data?.volumeDiscountStats.edges[0]?.node.atEpoch.toString() ===
|
||||
data?.epoch.id &&
|
||||
data?.volumeDiscountStats.edges[0]?.node.discountFactor) ||
|
||||
'0';
|
||||
const referralDiscountFactor =
|
||||
(data?.referralSetStats.edges[0]?.node.atEpoch.toString() ===
|
||||
data?.epoch.id &&
|
||||
data?.referralSetStats.edges[0]?.node.discountFactor) ||
|
||||
'0';
|
||||
if (order?.postOnly) {
|
||||
return {
|
||||
volumeDiscountFactor,
|
||||
referralDiscountFactor,
|
||||
totalFeeAmount: '0',
|
||||
fees: {
|
||||
infrastructureFee: '0',
|
||||
@@ -60,8 +56,13 @@ export const useEstimateFees = (
|
||||
},
|
||||
};
|
||||
}
|
||||
return isMarketInAuction && data?.estimateFees
|
||||
if (!data?.estimateFees) {
|
||||
return undefined;
|
||||
}
|
||||
return isMarketInAuction
|
||||
? {
|
||||
volumeDiscountFactor,
|
||||
referralDiscountFactor,
|
||||
totalFeeAmount: divideByTwo(data.estimateFees.totalFeeAmount),
|
||||
fees: {
|
||||
infrastructureFee: divideByTwo(
|
||||
@@ -91,5 +92,9 @@ export const useEstimateFees = (
|
||||
divideByTwo(data.estimateFees.fees.makerFeeVolumeDiscount),
|
||||
},
|
||||
}
|
||||
: data?.estimateFees;
|
||||
: {
|
||||
volumeDiscountFactor,
|
||||
referralDiscountFactor,
|
||||
...data.estimateFees,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -42,8 +42,12 @@ fragment FillEdge on TradeEdge {
|
||||
cursor
|
||||
}
|
||||
|
||||
query Fills($filter: TradesFilter, $pagination: Pagination) {
|
||||
trades(filter: $filter, pagination: $pagination) {
|
||||
query Fills(
|
||||
$filter: TradesFilter
|
||||
$pagination: Pagination
|
||||
$dateRange: DateRange
|
||||
) {
|
||||
trades(filter: $filter, dateRange: $dateRange, pagination: $pagination) {
|
||||
edges {
|
||||
...FillEdge
|
||||
}
|
||||
|
||||
+4
-2
@@ -12,6 +12,7 @@ export type FillEdgeFragment = { __typename?: 'TradeEdge', cursor: string, node:
|
||||
export type FillsQueryVariables = Types.Exact<{
|
||||
filter?: Types.InputMaybe<Types.TradesFilter>;
|
||||
pagination?: Types.InputMaybe<Types.Pagination>;
|
||||
dateRange?: Types.InputMaybe<Types.DateRange>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -95,8 +96,8 @@ export const FillUpdateFieldsFragmentDoc = gql`
|
||||
}
|
||||
${TradeFeeFieldsFragmentDoc}`;
|
||||
export const FillsDocument = gql`
|
||||
query Fills($filter: TradesFilter, $pagination: Pagination) {
|
||||
trades(filter: $filter, pagination: $pagination) {
|
||||
query Fills($filter: TradesFilter, $pagination: Pagination, $dateRange: DateRange) {
|
||||
trades(filter: $filter, dateRange: $dateRange, pagination: $pagination) {
|
||||
edges {
|
||||
...FillEdge
|
||||
}
|
||||
@@ -124,6 +125,7 @@ export const FillsDocument = gql`
|
||||
* variables: {
|
||||
* filter: // value for 'filter'
|
||||
* pagination: // value for 'pagination'
|
||||
* dateRange: // value for 'dateRange'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { useRef } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { FillsTable } from './fills-table';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import { fillsWithMarketProvider } from './fills-data-provider';
|
||||
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface FillsManagerProps {
|
||||
partyId: string;
|
||||
@@ -22,26 +23,60 @@ export const FillsManager = ({
|
||||
const filter: Schema.TradesFilter | Schema.TradesSubscriptionFilter = {
|
||||
partyIds: [partyId],
|
||||
};
|
||||
const { data, error } = useDataProvider({
|
||||
dataProvider: fillsWithMarketProvider,
|
||||
update: ({ data }) => {
|
||||
if (data?.length && gridRef.current?.api) {
|
||||
gridRef.current?.api.setRowData(data);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
|
||||
undefined
|
||||
);
|
||||
const { onFilterChanged, ...props } = gridProps || {};
|
||||
const onRowDataUpdated = useCallback(
|
||||
({ api }: { api: AgGridReact['api'] }) => {
|
||||
setHasDisplayedRow(!!api.getDisplayedRowCount());
|
||||
},
|
||||
[]
|
||||
);
|
||||
const { data, error, load, pageInfo } = useDataProvider({
|
||||
dataProvider: fillsWithMarketProvider,
|
||||
variables: { filter },
|
||||
});
|
||||
|
||||
return (
|
||||
<FillsTable
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
partyId={partyId}
|
||||
onMarketClick={onMarketClick}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No fills')}
|
||||
{...gridProps}
|
||||
/>
|
||||
<div className="flex flex-col h-full">
|
||||
<FillsTable
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
onFilterChanged={(event) => {
|
||||
onRowDataUpdated(event);
|
||||
onFilterChanged(event);
|
||||
}}
|
||||
onRowDataUpdated={onRowDataUpdated}
|
||||
partyId={partyId}
|
||||
onMarketClick={onMarketClick}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No fills')}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between border-t border-default p-1 items-center">
|
||||
<div className="text-xs">
|
||||
{t(
|
||||
'Depending on data node retention you may not be able see the "full" history'
|
||||
)}
|
||||
</div>
|
||||
<div className="flex text-xs items-center">
|
||||
{data?.length && !pageInfo?.hasNextPage
|
||||
? t('all %s items loaded', [data.length.toString()])
|
||||
: t('%s items loaded', [
|
||||
data?.length ? data.length.toString() : ' ',
|
||||
])}
|
||||
{pageInfo?.hasNextPage ? (
|
||||
<Button size="extra-small" className="ml-1" onClick={() => load()}>
|
||||
{t('Load more')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{data?.length && hasDisplayedRow === false ? (
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
|
||||
{t('No fills matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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()]} />);
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
negativeClassNames,
|
||||
MarketNameCell,
|
||||
COL_DEFS,
|
||||
DateRangeFilter,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import type {
|
||||
VegaValueFormatterParams,
|
||||
@@ -120,6 +121,7 @@ export const FillsTable = forwardRef<AgGridReact, Props>(
|
||||
},
|
||||
{
|
||||
headerName: t('Date'),
|
||||
filter: DateRangeFilter,
|
||||
field: 'createdAt',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
|
||||
@@ -43,7 +43,7 @@ export const fundingPaymentsProvider = makeDataProvider<
|
||||
pagination: {
|
||||
getPageInfo,
|
||||
append,
|
||||
first: 100,
|
||||
first: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { useRef } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { FundingPaymentsTable } from './funding-payments-table';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { fundingPaymentsWithMarketProvider } from './funding-payments-data-provider';
|
||||
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface FundingPaymentsManagerProps {
|
||||
partyId: string;
|
||||
@@ -20,7 +21,17 @@ export const FundingPaymentsManager = ({
|
||||
gridProps,
|
||||
}: FundingPaymentsManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const { data, error } = useDataProvider({
|
||||
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
|
||||
undefined
|
||||
);
|
||||
const { onFilterChanged, ...props } = gridProps || {};
|
||||
const onRowDataUpdated = useCallback(
|
||||
({ api }: { api: AgGridReact['api'] }) => {
|
||||
setHasDisplayedRow(!!api.getDisplayedRowCount());
|
||||
},
|
||||
[]
|
||||
);
|
||||
const { data, error, load, pageInfo } = useDataProvider({
|
||||
dataProvider: fundingPaymentsWithMarketProvider,
|
||||
update: ({ data }) => {
|
||||
if (data?.length && gridRef.current?.api) {
|
||||
@@ -33,12 +44,43 @@ export const FundingPaymentsManager = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<FundingPaymentsTable
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
onMarketClick={onMarketClick}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No funding payments')}
|
||||
{...gridProps}
|
||||
/>
|
||||
<div className="flex flex-col h-full">
|
||||
<FundingPaymentsTable
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
onMarketClick={onMarketClick}
|
||||
onFilterChanged={(event) => {
|
||||
onRowDataUpdated(event);
|
||||
onFilterChanged(event);
|
||||
}}
|
||||
onRowDataUpdated={onRowDataUpdated}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No funding payments')}
|
||||
{...props}
|
||||
/>
|
||||
<div className="flex justify-between border-t border-default p-1 items-center">
|
||||
<div className="text-xs">
|
||||
{t(
|
||||
'Depending on data node retention you may not be able see the "full" history'
|
||||
)}
|
||||
</div>
|
||||
<div className="flex text-xs items-center">
|
||||
{data?.length && !pageInfo?.hasNextPage
|
||||
? t('all %s items loaded', [data.length.toString()])
|
||||
: t('%s items loaded', [
|
||||
data?.length ? data.length.toString() : ' ',
|
||||
])}
|
||||
{pageInfo?.hasNextPage ? (
|
||||
<Button size="extra-small" className="ml-1" onClick={() => load()}>
|
||||
{t('Load more')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{data?.length && hasDisplayedRow === false ? (
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
|
||||
{t('No funding payments matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ fragment LiquidityProvisionFields on LiquidityProvision {
|
||||
id
|
||||
party {
|
||||
id
|
||||
accountsConnection(marketId: $marketId, type: ACCOUNT_TYPE_BOND) {
|
||||
accountsConnection(marketId: $marketId) {
|
||||
edges {
|
||||
node {
|
||||
type
|
||||
@@ -20,12 +20,36 @@ fragment LiquidityProvisionFields on LiquidityProvision {
|
||||
status
|
||||
}
|
||||
|
||||
query PaidFees($marketId: ID) {
|
||||
paidLiquidityFees(marketId: $marketId) {
|
||||
edges {
|
||||
node {
|
||||
marketId
|
||||
assetId
|
||||
epoch
|
||||
totalFeesPaid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query LiquidityProvisions($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
liquidityProvisionsConnection(live: true) {
|
||||
liquiditySLAParameters {
|
||||
priceRange
|
||||
commitmentMinTimeFraction
|
||||
performanceHysteresisEpochs
|
||||
slaCompetitionFactor
|
||||
}
|
||||
liquidityProvisions(live: true) {
|
||||
edges {
|
||||
node {
|
||||
...LiquidityProvisionFields
|
||||
current {
|
||||
...LiquidityProvisionFields
|
||||
}
|
||||
pending {
|
||||
...LiquidityProvisionFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
fragment MarketNode on Market {
|
||||
id
|
||||
liquidityProvisionsConnection(live: true) {
|
||||
liquidityProvisions(live: true) {
|
||||
edges {
|
||||
node {
|
||||
commitmentAmount
|
||||
fee
|
||||
current {
|
||||
commitmentAmount
|
||||
fee
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+64
-4
@@ -5,12 +5,19 @@ import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type LiquidityProvisionFieldsFragment = { __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 } };
|
||||
|
||||
export type PaidFeesQueryVariables = Types.Exact<{
|
||||
marketId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type PaidFeesQuery = { __typename?: 'Query', paidLiquidityFees?: { __typename?: 'PaidLiquidityFeesConnection', edges: Array<{ __typename?: 'PaidLiquidityFeesEdge', node: { __typename?: 'PaidLiquidityFees', marketId: string, assetId: string, epoch: number, totalFeesPaid: string } } | null> } | null };
|
||||
|
||||
export type LiquidityProvisionsQueryVariables = Types.Exact<{
|
||||
marketId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
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 LiquidityProvisionsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', liquiditySLAParameters?: { __typename?: 'LiquiditySLAParameters', priceRange: string, commitmentMinTimeFraction: string, performanceHysteresisEpochs: number, slaCompetitionFactor: string } | null, liquidityProvisions?: { __typename?: 'LiquidityProvisionsWithPendingConnection', edges?: Array<{ __typename?: 'LiquidityProvisionWithPendingEdge', node: { __typename?: 'LiquidityProvisionWithPending', current: { __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 } }, pending?: { __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 } | null };
|
||||
|
||||
export type LiquidityProviderFeeShareFieldsFragment = { __typename?: 'LiquidityProviderFeeShare', equityLikeShare: string, averageEntryValuation: string, averageScore: string, virtualStake: string };
|
||||
|
||||
@@ -30,7 +37,7 @@ export const LiquidityProvisionFieldsFragmentDoc = gql`
|
||||
id
|
||||
party {
|
||||
id
|
||||
accountsConnection(marketId: $marketId, type: ACCOUNT_TYPE_BOND) {
|
||||
accountsConnection(marketId: $marketId) {
|
||||
edges {
|
||||
node {
|
||||
type
|
||||
@@ -79,13 +86,66 @@ export const LiquidityProviderFieldsFragmentDoc = gql`
|
||||
}
|
||||
${LiquidityProviderFeeShareFieldsFragmentDoc}
|
||||
${LiquidityProviderSLAFieldsFragmentDoc}`;
|
||||
export const PaidFeesDocument = gql`
|
||||
query PaidFees($marketId: ID) {
|
||||
paidLiquidityFees(marketId: $marketId) {
|
||||
edges {
|
||||
node {
|
||||
marketId
|
||||
assetId
|
||||
epoch
|
||||
totalFeesPaid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __usePaidFeesQuery__
|
||||
*
|
||||
* To run a query within a React component, call `usePaidFeesQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `usePaidFeesQuery` 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 } = usePaidFeesQuery({
|
||||
* variables: {
|
||||
* marketId: // value for 'marketId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function usePaidFeesQuery(baseOptions?: Apollo.QueryHookOptions<PaidFeesQuery, PaidFeesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<PaidFeesQuery, PaidFeesQueryVariables>(PaidFeesDocument, options);
|
||||
}
|
||||
export function usePaidFeesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<PaidFeesQuery, PaidFeesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<PaidFeesQuery, PaidFeesQueryVariables>(PaidFeesDocument, options);
|
||||
}
|
||||
export type PaidFeesQueryHookResult = ReturnType<typeof usePaidFeesQuery>;
|
||||
export type PaidFeesLazyQueryHookResult = ReturnType<typeof usePaidFeesLazyQuery>;
|
||||
export type PaidFeesQueryResult = Apollo.QueryResult<PaidFeesQuery, PaidFeesQueryVariables>;
|
||||
export const LiquidityProvisionsDocument = gql`
|
||||
query LiquidityProvisions($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
liquidityProvisionsConnection(live: true) {
|
||||
liquiditySLAParameters {
|
||||
priceRange
|
||||
commitmentMinTimeFraction
|
||||
performanceHysteresisEpochs
|
||||
slaCompetitionFactor
|
||||
}
|
||||
liquidityProvisions(live: true) {
|
||||
edges {
|
||||
node {
|
||||
...LiquidityProvisionFields
|
||||
current {
|
||||
...LiquidityProvisionFields
|
||||
}
|
||||
pending {
|
||||
...LiquidityProvisionFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-5
@@ -3,21 +3,23 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type MarketNodeFragment = { __typename?: 'Market', id: string, liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null };
|
||||
export type MarketNodeFragment = { __typename?: 'Market', id: string, liquidityProvisions?: { __typename?: 'LiquidityProvisionsWithPendingConnection', edges?: Array<{ __typename?: 'LiquidityProvisionWithPendingEdge', node: { __typename?: 'LiquidityProvisionWithPending', current: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null };
|
||||
|
||||
export type LiquidityProvisionMarketsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type LiquidityProvisionMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null } }> } | null };
|
||||
export type LiquidityProvisionMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, liquidityProvisions?: { __typename?: 'LiquidityProvisionsWithPendingConnection', edges?: Array<{ __typename?: 'LiquidityProvisionWithPendingEdge', node: { __typename?: 'LiquidityProvisionWithPending', current: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null } }> } | null };
|
||||
|
||||
export const MarketNodeFragmentDoc = gql`
|
||||
fragment MarketNode on Market {
|
||||
id
|
||||
liquidityProvisionsConnection(live: true) {
|
||||
liquidityProvisions(live: true) {
|
||||
edges {
|
||||
node {
|
||||
commitmentAmount
|
||||
fee
|
||||
current {
|
||||
commitmentAmount
|
||||
fee
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import type { LiquidityProvisionFields } from './liquidity-data-provider';
|
||||
import { getLiquidityProvision } from './liquidity-data-provider';
|
||||
import type {
|
||||
LiquidityProviderFieldsFragment,
|
||||
LiquidityProvisionFieldsFragment,
|
||||
} from './__generated__/MarketLiquidity';
|
||||
import type { LiquidityProviderFieldsFragment } from './__generated__/MarketLiquidity';
|
||||
|
||||
const input = {
|
||||
liquidityProvisions: [
|
||||
{
|
||||
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
|
||||
party: {
|
||||
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
|
||||
accountsConnection: {
|
||||
@@ -31,7 +30,11 @@ const input = {
|
||||
fee: '0.001',
|
||||
status: 'STATUS_ACTIVE',
|
||||
__typename: 'LiquidityProvision',
|
||||
} as LiquidityProvisionFieldsFragment,
|
||||
priceRange: '0',
|
||||
commitmentMinTimeFraction: '0.5',
|
||||
performanceHysteresisEpochs: 5678,
|
||||
slaCompetitionFactor: '0',
|
||||
} as unknown as LiquidityProvisionFields,
|
||||
],
|
||||
liquidityProviders: [
|
||||
{
|
||||
@@ -49,9 +52,12 @@ const input = {
|
||||
const result = [
|
||||
{
|
||||
__typename: undefined,
|
||||
balance: '1.8003328918633596575e+22',
|
||||
balance: 1.8003328918633597e22,
|
||||
earmarkedFees: 0,
|
||||
commitmentAmount: '18003328918633596575000',
|
||||
createdAt: '2022-12-16T09:28:29.071781Z',
|
||||
commitmentMinTimeFraction: '0.5',
|
||||
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
|
||||
feeShare: {
|
||||
equityLikeShare: '1',
|
||||
__typename: 'LiquidityProviderFeeShare',
|
||||
@@ -59,6 +65,9 @@ const result = [
|
||||
},
|
||||
fee: '0.001',
|
||||
partyId: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
|
||||
performanceHysteresisEpochs: 5678,
|
||||
priceRange: '0',
|
||||
slaCompetitionFactor: '0',
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
accountsConnection: {
|
||||
@@ -106,7 +115,9 @@ describe('getLiquidityProvision', () => {
|
||||
{
|
||||
__typename: 'LiquidityProvision',
|
||||
commitmentAmount: '18003328918633596575000',
|
||||
commitmentMinTimeFraction: '0.5',
|
||||
createdAt: '2022-12-16T09:28:29.071781Z',
|
||||
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
|
||||
fee: '0.001',
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
@@ -125,6 +136,9 @@ describe('getLiquidityProvision', () => {
|
||||
},
|
||||
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
|
||||
},
|
||||
performanceHysteresisEpochs: 5678,
|
||||
priceRange: '0',
|
||||
slaCompetitionFactor: '0',
|
||||
status: 'STATUS_ACTIVE',
|
||||
updatedAt: '2023-01-04T22:13:27.761985Z',
|
||||
},
|
||||
|
||||
@@ -20,20 +20,24 @@ import type {
|
||||
LiquidityProvisionsQueryVariables,
|
||||
} from './__generated__/MarketLiquidity';
|
||||
|
||||
export type LiquidityProvisionFields = LiquidityProvisionFieldsFragment &
|
||||
Schema.LiquiditySLAParameters;
|
||||
|
||||
export const liquidityProvisionsDataProvider = makeDataProvider<
|
||||
LiquidityProvisionsQuery,
|
||||
LiquidityProvisionFieldsFragment[],
|
||||
LiquidityProvisionFields[],
|
||||
never,
|
||||
never,
|
||||
LiquidityProvisionsQueryVariables
|
||||
>({
|
||||
query: LiquidityProvisionsDocument,
|
||||
getData: (responseData: LiquidityProvisionsQuery | null) => {
|
||||
return (
|
||||
responseData?.market?.liquidityProvisionsConnection?.edges?.map(
|
||||
(e) => e?.node
|
||||
) ?? []
|
||||
).filter((n) => !!n) as LiquidityProvisionFieldsFragment[];
|
||||
return (responseData?.market?.liquidityProvisions?.edges
|
||||
?.filter((n) => !!n)
|
||||
.map((e) => ({
|
||||
...e?.node.current,
|
||||
...responseData.market?.liquiditySLAParameters,
|
||||
})) ?? []) as LiquidityProvisionFields[];
|
||||
},
|
||||
});
|
||||
|
||||
@@ -81,10 +85,7 @@ export const lpAggregatedDataProvider = makeDerivedDataProvider<
|
||||
}
|
||||
);
|
||||
|
||||
export const matchFilter = (
|
||||
filter: Filter,
|
||||
lp: LiquidityProvisionFieldsFragment
|
||||
) => {
|
||||
export const matchFilter = (filter: Filter, lp: LiquidityProvisionData) => {
|
||||
if (filter.partyId && lp.party.id !== filter.partyId) {
|
||||
return false;
|
||||
}
|
||||
@@ -104,15 +105,18 @@ export const matchFilter = (
|
||||
};
|
||||
|
||||
export interface LiquidityProvisionData
|
||||
extends Omit<LiquidityProvisionFieldsFragment, '__typename'> {
|
||||
extends Omit<LiquidityProvisionFieldsFragment, '__typename'>,
|
||||
Partial<LiquidityProviderFieldsFragment>,
|
||||
Omit<Schema.LiquiditySLAParameters, '__typename'> {
|
||||
assetDecimalPlaces?: number;
|
||||
balance?: string;
|
||||
balance?: number;
|
||||
averageEntryValuation?: string;
|
||||
equityLikeShare?: string;
|
||||
earmarkedFees?: number;
|
||||
}
|
||||
|
||||
export const getLiquidityProvision = (
|
||||
liquidityProvisions: LiquidityProvisionFieldsFragment[],
|
||||
liquidityProvisions: LiquidityProvisionFields[],
|
||||
liquidityProvider: LiquidityProviderFieldsFragment[],
|
||||
filter?: Filter
|
||||
): LiquidityProvisionData[] => {
|
||||
@@ -141,17 +145,29 @@ export const getLiquidityProvision = (
|
||||
const bondAccounts = accounts?.filter(
|
||||
(a) => a?.type === Schema.AccountType.ACCOUNT_TYPE_BOND
|
||||
);
|
||||
const feeAccounts = accounts?.filter(
|
||||
(a) => a?.type === Schema.AccountType.ACCOUNT_TYPE_LP_LIQUIDITY_FEES
|
||||
);
|
||||
const balance =
|
||||
bondAccounts
|
||||
?.reduce(
|
||||
(acc, a) => acc.plus(new BigNumber(a.balance ?? 0)),
|
||||
new BigNumber(0)
|
||||
)
|
||||
.toString() || '0';
|
||||
.toNumber() ?? 0;
|
||||
|
||||
const earmarkedFees =
|
||||
feeAccounts
|
||||
?.reduce(
|
||||
(acc, a) => acc.plus(new BigNumber(a.balance ?? 0)),
|
||||
new BigNumber(0)
|
||||
)
|
||||
.toNumber() ?? 0;
|
||||
return {
|
||||
...lp,
|
||||
...lpObj,
|
||||
balance,
|
||||
earmarkedFees,
|
||||
__typename: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -55,9 +55,10 @@ describe('LiquidityTable', () => {
|
||||
'Adjusted stake share',
|
||||
'Share',
|
||||
'Live supplied liquidity',
|
||||
'Live time fraction on book',
|
||||
'Fees accrued this epoch',
|
||||
'Live time on book',
|
||||
'Live liquidity quality score (%)',
|
||||
'Last time fraction on the book',
|
||||
'Last time on the book',
|
||||
'Last fee penalty',
|
||||
'Last bond penalty',
|
||||
'Status',
|
||||
|
||||
@@ -96,6 +96,58 @@ export const LiquidityTable = ({
|
||||
return `${addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0)}`;
|
||||
};
|
||||
|
||||
const feesAccruedTooltip = ({ value, data }: ITooltipParams) => {
|
||||
if (!value) return '-';
|
||||
const newValue = new BigNumber(value)
|
||||
.times(Number(stakeToCcyVolume) || 1)
|
||||
.toString();
|
||||
let lessThanFull = false,
|
||||
lessThanMinimum = false;
|
||||
if (data.sla) {
|
||||
lessThanFull =
|
||||
data.sla &&
|
||||
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).isLessThan(
|
||||
1
|
||||
);
|
||||
lessThanMinimum =
|
||||
data.sla &&
|
||||
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).isLessThan(
|
||||
data.commitmentMinTimeFraction
|
||||
);
|
||||
}
|
||||
if (lessThanMinimum) {
|
||||
return t(
|
||||
`This LP's time on the book in the current epoch (%s) is less than the minimum required (%s), so they could lose all fee revenue for this epoch.`,
|
||||
[
|
||||
formatNumberPercentage(
|
||||
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).times(
|
||||
100
|
||||
),
|
||||
2
|
||||
),
|
||||
formatNumberPercentage(
|
||||
new BigNumber(data.commitmentMinTimeFraction).times(100),
|
||||
2
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
if (lessThanFull) {
|
||||
return t(
|
||||
`This LP's time on the book in the current epoch (%s) is less than 100%, so they could lose some fees to a better performing LP.`,
|
||||
[
|
||||
formatNumberPercentage(
|
||||
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).times(
|
||||
100
|
||||
),
|
||||
2
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
return addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0);
|
||||
};
|
||||
|
||||
const stakeToCcyVolumeQuantumFormatter = ({
|
||||
value,
|
||||
}: ValueFormatterParams) => {
|
||||
@@ -178,7 +230,7 @@ export const LiquidityTable = ({
|
||||
],
|
||||
},
|
||||
{
|
||||
headerName: t('Live liquidity details'),
|
||||
headerName: t('Live liquidity data'),
|
||||
marryChildren: true,
|
||||
children: [
|
||||
{
|
||||
@@ -192,7 +244,41 @@ export const LiquidityTable = ({
|
||||
tooltipValueGetter: stakeToCcyVolumeFormatter,
|
||||
},
|
||||
{
|
||||
headerName: t(`Live time fraction on book`),
|
||||
headerName: t('Fees accrued this epoch'),
|
||||
field: 'earmarkedFees',
|
||||
type: 'rightAligned',
|
||||
headerTooltip: t(
|
||||
`The liquidity fees accrued by each provider, which will be distributed at the end of the epoch after applying any penalties.`
|
||||
),
|
||||
valueFormatter: stakeToCcyVolumeQuantumFormatter,
|
||||
tooltipValueGetter: feesAccruedTooltip,
|
||||
cellClassRules: {
|
||||
'text-warning': ({ data }: { data: LiquidityProvisionData }) => {
|
||||
if (!data.sla) return false;
|
||||
return (
|
||||
new BigNumber(
|
||||
data.sla.currentEpochFractionOfTimeOnBook
|
||||
).isLessThan(1) &&
|
||||
new BigNumber(
|
||||
data.sla.currentEpochFractionOfTimeOnBook
|
||||
).isGreaterThan(data.commitmentMinTimeFraction)
|
||||
);
|
||||
},
|
||||
'text-red-500': ({ data }: { data: LiquidityProvisionData }) => {
|
||||
if (!data.sla) return false;
|
||||
return (
|
||||
new BigNumber(
|
||||
data.sla.currentEpochFractionOfTimeOnBook
|
||||
).isLessThan(data.commitmentMinTimeFraction) &&
|
||||
new BigNumber(
|
||||
data.sla.currentEpochFractionOfTimeOnBook
|
||||
).isGreaterThan(0)
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t(`Live time on book`),
|
||||
field: 'sla.currentEpochFractionOfTimeOnBook',
|
||||
type: 'rightAligned',
|
||||
headerTooltip: t('Current epoch fraction of time on the book.'),
|
||||
@@ -212,7 +298,7 @@ export const LiquidityTable = ({
|
||||
marryChildren: true,
|
||||
children: [
|
||||
{
|
||||
headerName: t(`Last time fraction on the book`),
|
||||
headerName: t(`Last time on the book`),
|
||||
field: 'sla.lastEpochFractionOfTimeOnBook',
|
||||
type: 'rightAligned',
|
||||
headerTooltip: t('Last epoch fraction of time on the book.'),
|
||||
@@ -275,7 +361,9 @@ export const LiquidityTable = ({
|
||||
return (
|
||||
<AgGrid
|
||||
overlayNoRowsTemplate={t('No liquidity provisions')}
|
||||
getRowId={({ data }: { data: LiquidityProvisionData }) => data.id || ''}
|
||||
getRowId={({ data }: { data: LiquidityProvisionData }) => {
|
||||
return data.id || '';
|
||||
}}
|
||||
tooltipShowDelay={500}
|
||||
defaultColDef={defaultColDef}
|
||||
{...props}
|
||||
|
||||
@@ -12,12 +12,14 @@ export const liquidityProvisionsQuery = (
|
||||
): LiquidityProvisionsQuery => {
|
||||
const defaultResult: LiquidityProvisionsQuery = {
|
||||
market: {
|
||||
liquidityProvisionsConnection: {
|
||||
__typename: 'LiquidityProvisionsConnection',
|
||||
liquidityProvisions: {
|
||||
__typename: 'LiquidityProvisionsWithPendingConnection',
|
||||
edges: liquidityFields.map((node) => {
|
||||
return {
|
||||
__typename: 'LiquidityProvisionsEdge',
|
||||
node,
|
||||
__typename: 'LiquidityProvisionWithPendingEdge',
|
||||
node: {
|
||||
current: node,
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -106,10 +106,9 @@ export const getLiquidityForMarket = (
|
||||
markets: LiquidityProvisionMarket[]
|
||||
) => {
|
||||
const liquidity =
|
||||
markets.find((m) => m.id === marketId)?.liquidityProvisionsConnection
|
||||
?.edges || [];
|
||||
markets.find((m) => m.id === marketId)?.liquidityProvisions?.edges || [];
|
||||
|
||||
return liquidity.map((l) => l?.node);
|
||||
return liquidity.map((l) => l?.node.current);
|
||||
};
|
||||
|
||||
export const getTargetStake = (
|
||||
|
||||
@@ -1001,7 +1001,7 @@ export const LiquiditySLAParametersInfoPanel = ({
|
||||
|
||||
const { params: networkParams } = useNetworkParams([
|
||||
NetworkParams.market_liquidity_bondPenaltyParameter,
|
||||
NetworkParams.market_liquidity_nonPerformanceBondPenaltySlope,
|
||||
NetworkParams.market_liquidity_sla_nonPerformanceBondPenaltySlope,
|
||||
NetworkParams.market_liquidity_sla_nonPerformanceBondPenaltyMax,
|
||||
NetworkParams.market_liquidity_maximumLiquidityFeeFactorLevel,
|
||||
NetworkParams.market_liquidity_stakeToCcyVolume,
|
||||
@@ -1017,7 +1017,7 @@ export const LiquiditySLAParametersInfoPanel = ({
|
||||
bondPenaltyParameter:
|
||||
networkParams['market_liquidity_bondPenaltyParameter'],
|
||||
nonPerformanceBondPenaltySlope:
|
||||
networkParams['market_liquidity_nonPerformanceBondPenaltySlope'],
|
||||
networkParams['market_liquidity_sla_nonPerformanceBondPenaltySlope'],
|
||||
nonPerformanceBondPenaltyMax:
|
||||
networkParams['market_liquidity_sla_nonPerformanceBondPenaltyMax'],
|
||||
maxLiquidityFeeFactorLevel:
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Market, MarketMaybeWithDataAndCandles } from './markets-provider';
|
||||
import {
|
||||
calcTradedFactor,
|
||||
filterAndSortMarkets,
|
||||
sumFeesFactors,
|
||||
totalFeesFactorsPercentage,
|
||||
} from './market-utils';
|
||||
const { MarketState, MarketTradingMode } = Schema;
|
||||
@@ -132,3 +133,15 @@ describe('calcTradedFactor', () => {
|
||||
expect(fa > fb).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sumFeesFactors', () => {
|
||||
it('does not result in flop errors', () => {
|
||||
expect(
|
||||
sumFeesFactors({
|
||||
makerFee: '0.1',
|
||||
infrastructureFee: '0.2',
|
||||
liquidityFee: '0.3',
|
||||
})
|
||||
).toEqual(0.6);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,16 +50,19 @@ export const getQuoteName = (market: Partial<Market>) => {
|
||||
};
|
||||
|
||||
export const sumFeesFactors = (fees: Market['fees']['factors']) => {
|
||||
return fees
|
||||
? new BigNumber(fees.makerFee)
|
||||
.plus(fees.liquidityFee)
|
||||
.plus(fees.infrastructureFee)
|
||||
: undefined;
|
||||
if (!fees) return;
|
||||
|
||||
return new BigNumber(fees.makerFee)
|
||||
.plus(fees.liquidityFee)
|
||||
.plus(fees.infrastructureFee)
|
||||
.toNumber();
|
||||
};
|
||||
|
||||
export const totalFeesFactorsPercentage = (fees: Market['fees']['factors']) => {
|
||||
const total = fees && sumFeesFactors(fees);
|
||||
return total ? formatNumberPercentage(total.times(100)) : undefined;
|
||||
return total
|
||||
? formatNumberPercentage(new BigNumber(total).times(100))
|
||||
: undefined;
|
||||
};
|
||||
|
||||
export const filterAndSortMarkets = (markets: MarketMaybeWithData[]) => {
|
||||
|
||||
@@ -153,12 +153,14 @@ 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:
|
||||
'market_liquidity_bondPenaltyParameter',
|
||||
market_liquidity_nonPerformanceBondPenaltySlope:
|
||||
'market_liquidity_nonPerformanceBondPenaltySlope',
|
||||
market_liquidity_sla_nonPerformanceBondPenaltySlope:
|
||||
'market_liquidity_sla_nonPerformanceBondPenaltySlope',
|
||||
market_liquidity_sla_nonPerformanceBondPenaltyMax:
|
||||
'market_liquidity_sla_nonPerformanceBondPenaltyMax',
|
||||
market_liquidity_maximumLiquidityFeeFactorLevel:
|
||||
|
||||
@@ -166,7 +166,7 @@ export const ordersProvider = makeDataProvider<
|
||||
pagination: {
|
||||
getPageInfo,
|
||||
append,
|
||||
first: 5000,
|
||||
first: 1000,
|
||||
},
|
||||
resetDelay: 1000,
|
||||
additionalContext: { isEnlargedTimeout: true },
|
||||
|
||||
@@ -27,6 +27,7 @@ describe('OrderListManager', () => {
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
pageInfo: null,
|
||||
});
|
||||
render(generateJsx());
|
||||
|
||||
@@ -45,6 +46,7 @@ describe('OrderListManager', () => {
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
pageInfo: null,
|
||||
});
|
||||
|
||||
render(generateJsx());
|
||||
@@ -62,6 +64,7 @@ describe('OrderListManager', () => {
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
pageInfo: null,
|
||||
});
|
||||
|
||||
render(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { FilterChangedEvent } from 'ag-grid-community';
|
||||
import { OrderListTable } from '../order-list';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
@@ -12,7 +11,7 @@ import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/web3';
|
||||
import { OrderEditDialog } from '../order-list/order-edit-dialog';
|
||||
import type { Order } from '../order-data-provider';
|
||||
import { OrderViewDialog } from '../order-list/order-view-dialog';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { Splash, TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export enum Filter {
|
||||
'Open' = 'Open',
|
||||
@@ -48,33 +47,11 @@ export const OrderListManager = ({
|
||||
? { partyId, filter: { liveOnly: true } }
|
||||
: { partyId };
|
||||
|
||||
const { data, error } = useDataProvider({
|
||||
const { data, error, pageInfo, load } = useDataProvider({
|
||||
dataProvider: ordersWithMarketProvider,
|
||||
variables,
|
||||
update: ({ data }) => {
|
||||
if (data && gridRef.current?.api) {
|
||||
gridRef.current.api.setRowData(data);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
const onFilterChanged = useCallback(
|
||||
(event: FilterChangedEvent) => {
|
||||
gridProps?.onFilterChanged?.(event);
|
||||
if (event.api) {
|
||||
const isEmpty = event.api.getDisplayedRowCount() === 0;
|
||||
if (isEmpty) {
|
||||
event.api.showNoRowsOverlay();
|
||||
} else {
|
||||
event.api.hideOverlay();
|
||||
}
|
||||
}
|
||||
},
|
||||
[gridProps]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || !data.length) {
|
||||
gridRef.current?.api?.showNoRowsOverlay();
|
||||
@@ -96,6 +73,17 @@ export const OrderListManager = ({
|
||||
[create]
|
||||
);
|
||||
|
||||
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
|
||||
undefined
|
||||
);
|
||||
const { onFilterChanged, ...props } = gridProps || {};
|
||||
const onRowDataUpdated = useCallback(
|
||||
({ api }: { api: AgGridReact['api'] }) => {
|
||||
setHasDisplayedRow(!!api.getDisplayedRowCount());
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Splash>{t(`Something went wrong: ${error.message}`)}</Splash>;
|
||||
}
|
||||
@@ -103,20 +91,60 @@ export const OrderListManager = ({
|
||||
return (
|
||||
<>
|
||||
<div className="h-full relative">
|
||||
<OrderListTable
|
||||
rowData={data}
|
||||
ref={gridRef}
|
||||
filter={filter}
|
||||
onCancel={cancel}
|
||||
onEdit={setEditOrder}
|
||||
onView={setViewOrder}
|
||||
onMarketClick={onMarketClick}
|
||||
onOrderTypeClick={onOrderTypeClick}
|
||||
isReadOnly={isReadOnly}
|
||||
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
|
||||
{...gridProps}
|
||||
onFilterChanged={onFilterChanged}
|
||||
/>
|
||||
<div className="flex flex-col h-full">
|
||||
<OrderListTable
|
||||
rowData={data}
|
||||
ref={gridRef}
|
||||
filter={filter}
|
||||
onCancel={cancel}
|
||||
onEdit={setEditOrder}
|
||||
onView={setViewOrder}
|
||||
onMarketClick={onMarketClick}
|
||||
onOrderTypeClick={onOrderTypeClick}
|
||||
onFilterChanged={(event) => {
|
||||
onRowDataUpdated(event);
|
||||
if (onFilterChanged) {
|
||||
onFilterChanged(event);
|
||||
}
|
||||
}}
|
||||
onRowDataUpdated={onRowDataUpdated}
|
||||
isReadOnly={isReadOnly}
|
||||
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
|
||||
{...props}
|
||||
/>
|
||||
<div className="flex justify-between border-t border-default p-1 items-center">
|
||||
<div className="text-xs">
|
||||
{variables.filter?.liveOnly
|
||||
? null
|
||||
: t(
|
||||
'Depending on data node retention you may not be able see the "full" history'
|
||||
)}
|
||||
</div>
|
||||
{data ? (
|
||||
<div className="flex text-xs items-center">
|
||||
{data?.length && !pageInfo?.hasNextPage
|
||||
? t('all %s items loaded', [data.length.toString()])
|
||||
: t('%s items loaded', [
|
||||
data?.length ? data.length.toString() : ' ',
|
||||
])}
|
||||
{pageInfo?.hasNextPage ? (
|
||||
<Button
|
||||
size="extra-small"
|
||||
className="ml-1"
|
||||
onClick={() => load()}
|
||||
>
|
||||
{t('Load more')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{data?.length && hasDisplayedRow === false ? (
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
|
||||
{t('No orders matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{editOrder && (
|
||||
<OrderEditDialog
|
||||
|
||||
@@ -30,6 +30,7 @@ describe('StopOrdersManager', () => {
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
pageInfo: null,
|
||||
});
|
||||
await act(async () => {
|
||||
render(generateJsx());
|
||||
|
||||
@@ -12,10 +12,15 @@ subscription ProposalEvent($partyId: ID!) {
|
||||
}
|
||||
}
|
||||
|
||||
fragment UpdateNetworkParameterProposal on Proposal {
|
||||
fragment OnProposalFragment on Proposal {
|
||||
id
|
||||
state
|
||||
datetime
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
rejectionReason
|
||||
terms {
|
||||
enactmentDatetime
|
||||
change {
|
||||
@@ -26,9 +31,9 @@ fragment UpdateNetworkParameterProposal on Proposal {
|
||||
}
|
||||
}
|
||||
|
||||
subscription OnUpdateNetworkParameters {
|
||||
subscription OnProposal {
|
||||
proposals {
|
||||
...UpdateNetworkParameterProposal
|
||||
...OnProposalFragment
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-17
@@ -13,12 +13,12 @@ export type ProposalEventSubscriptionVariables = Types.Exact<{
|
||||
|
||||
export type ProposalEventSubscription = { __typename?: 'Subscription', proposals: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null } };
|
||||
|
||||
export type UpdateNetworkParameterProposalFragment = { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
|
||||
export type OnProposalFragmentFragment = { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
|
||||
|
||||
export type OnUpdateNetworkParametersSubscriptionVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
export type OnProposalSubscriptionVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type OnUpdateNetworkParametersSubscription = { __typename?: 'Subscription', proposals: { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } } };
|
||||
export type OnProposalSubscription = { __typename?: 'Subscription', proposals: { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } } };
|
||||
|
||||
export type ProposalOfMarketQueryVariables = Types.Exact<{
|
||||
marketId: Types.Scalars['ID'];
|
||||
@@ -64,11 +64,16 @@ export const ProposalEventFieldsFragmentDoc = gql`
|
||||
errorDetails
|
||||
}
|
||||
`;
|
||||
export const UpdateNetworkParameterProposalFragmentDoc = gql`
|
||||
fragment UpdateNetworkParameterProposal on Proposal {
|
||||
export const OnProposalFragmentFragmentDoc = gql`
|
||||
fragment OnProposalFragment on Proposal {
|
||||
id
|
||||
state
|
||||
datetime
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
rejectionReason
|
||||
terms {
|
||||
enactmentDatetime
|
||||
change {
|
||||
@@ -109,35 +114,35 @@ export function useProposalEventSubscription(baseOptions: Apollo.SubscriptionHoo
|
||||
}
|
||||
export type ProposalEventSubscriptionHookResult = ReturnType<typeof useProposalEventSubscription>;
|
||||
export type ProposalEventSubscriptionResult = Apollo.SubscriptionResult<ProposalEventSubscription>;
|
||||
export const OnUpdateNetworkParametersDocument = gql`
|
||||
subscription OnUpdateNetworkParameters {
|
||||
export const OnProposalDocument = gql`
|
||||
subscription OnProposal {
|
||||
proposals {
|
||||
...UpdateNetworkParameterProposal
|
||||
...OnProposalFragment
|
||||
}
|
||||
}
|
||||
${UpdateNetworkParameterProposalFragmentDoc}`;
|
||||
${OnProposalFragmentFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useOnUpdateNetworkParametersSubscription__
|
||||
* __useOnProposalSubscription__
|
||||
*
|
||||
* To run a query within a React component, call `useOnUpdateNetworkParametersSubscription` and pass it any options that fit your needs.
|
||||
* When your component renders, `useOnUpdateNetworkParametersSubscription` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* To run a query within a React component, call `useOnProposalSubscription` and pass it any options that fit your needs.
|
||||
* When your component renders, `useOnProposalSubscription` 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 } = useOnUpdateNetworkParametersSubscription({
|
||||
* const { data, loading, error } = useOnProposalSubscription({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useOnUpdateNetworkParametersSubscription(baseOptions?: Apollo.SubscriptionHookOptions<OnUpdateNetworkParametersSubscription, OnUpdateNetworkParametersSubscriptionVariables>) {
|
||||
export function useOnProposalSubscription(baseOptions?: Apollo.SubscriptionHookOptions<OnProposalSubscription, OnProposalSubscriptionVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useSubscription<OnUpdateNetworkParametersSubscription, OnUpdateNetworkParametersSubscriptionVariables>(OnUpdateNetworkParametersDocument, options);
|
||||
return Apollo.useSubscription<OnProposalSubscription, OnProposalSubscriptionVariables>(OnProposalDocument, options);
|
||||
}
|
||||
export type OnUpdateNetworkParametersSubscriptionHookResult = ReturnType<typeof useOnUpdateNetworkParametersSubscription>;
|
||||
export type OnUpdateNetworkParametersSubscriptionResult = Apollo.SubscriptionResult<OnUpdateNetworkParametersSubscription>;
|
||||
export type OnProposalSubscriptionHookResult = ReturnType<typeof useOnProposalSubscription>;
|
||||
export type OnProposalSubscriptionResult = Apollo.SubscriptionResult<OnProposalSubscription>;
|
||||
export const ProposalOfMarketDocument = gql`
|
||||
query ProposalOfMarket($marketId: ID!) {
|
||||
proposal(id: $marketId) {
|
||||
|
||||
@@ -3,7 +3,7 @@ export * from './use-proposal-event';
|
||||
export * from './use-vega-transaction';
|
||||
export * from './use-proposal-submit';
|
||||
export * from './use-update-proposal';
|
||||
export * from './use-update-network-paramaters-toasts';
|
||||
export * from './use-proposal-toasts';
|
||||
export * from './use-successor-market-proposal-details';
|
||||
export * from './use-new-transfer-proposal-details';
|
||||
export * from './use-cancel-transfer-proposal-details';
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { ProposalRejectionReason } from '@vegaprotocol/types';
|
||||
import {
|
||||
ProposalChangeMapping,
|
||||
ProposalState,
|
||||
ProposalStateMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
PROPOSAL_STATES_TO_TOAST,
|
||||
ProposalToastContent,
|
||||
useProposalToasts,
|
||||
} from './use-proposal-toasts';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { waitFor, renderHook, render } from '@testing-library/react';
|
||||
import {
|
||||
OnProposalDocument,
|
||||
type OnProposalFragmentFragment,
|
||||
type OnProposalSubscription,
|
||||
} from './__generated__/Proposal';
|
||||
import sample from 'lodash/sample';
|
||||
|
||||
const renderUseProposalToasts = (mocks?: MockedResponse[]) => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={mocks}>{children}</MockedProvider>
|
||||
);
|
||||
return renderHook(() => useProposalToasts(), { wrapper });
|
||||
};
|
||||
|
||||
type ProposalChange = OnProposalFragmentFragment['terms']['change'];
|
||||
|
||||
const NEW_MARKET_CHANGE: ProposalChange = { __typename: 'NewMarket' };
|
||||
const UPDATE_MARKET_CHANGE: ProposalChange = { __typename: 'UpdateMarket' };
|
||||
const UPDATE_NETWORK_PARAMETER_CHANGE: ProposalChange = {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'abc.def',
|
||||
value: '123',
|
||||
},
|
||||
};
|
||||
const NEW_ASSET_CHANGE: ProposalChange = { __typename: 'NewAsset' };
|
||||
const UPDATE_ASSET_CHANGE: ProposalChange = { __typename: 'UpdateAsset' };
|
||||
const NEW_FREEFORM_CHANGE: ProposalChange = { __typename: 'NewFreeform' };
|
||||
const NEW_TRANSFER_CHANGE: ProposalChange = { __typename: 'NewTransfer' };
|
||||
const CANCEL_TRANSFER_CHANGE: ProposalChange = { __typename: 'CancelTransfer' };
|
||||
const UPDATE_MARKET_STATE_CHANGE: ProposalChange = {
|
||||
__typename: 'UpdateMarketState',
|
||||
};
|
||||
const NEW_SPOT_MARKET_CHANGE: ProposalChange = { __typename: 'NewSpotMarket' };
|
||||
const UPDATE_SPOT_MARKET_CHANGE: ProposalChange = {
|
||||
__typename: 'UpdateSpotMarket',
|
||||
};
|
||||
const UPDATE_VOLUME_DISCOUNT_PROGRAM_CHANGE: ProposalChange = {
|
||||
__typename: 'UpdateVolumeDiscountProgram',
|
||||
};
|
||||
const UPDATE_REFERRAL_PROGRAM_CHANGE: ProposalChange = {
|
||||
__typename: 'UpdateReferralProgram',
|
||||
};
|
||||
|
||||
const GenericToastProposals = [
|
||||
NEW_MARKET_CHANGE,
|
||||
UPDATE_MARKET_CHANGE,
|
||||
NEW_ASSET_CHANGE,
|
||||
UPDATE_ASSET_CHANGE,
|
||||
NEW_FREEFORM_CHANGE,
|
||||
NEW_TRANSFER_CHANGE,
|
||||
CANCEL_TRANSFER_CHANGE,
|
||||
UPDATE_MARKET_STATE_CHANGE,
|
||||
NEW_SPOT_MARKET_CHANGE,
|
||||
UPDATE_SPOT_MARKET_CHANGE,
|
||||
UPDATE_VOLUME_DISCOUNT_PROGRAM_CHANGE,
|
||||
UPDATE_REFERRAL_PROGRAM_CHANGE,
|
||||
];
|
||||
|
||||
const generateProposal = (
|
||||
title: string,
|
||||
state: ProposalState = ProposalState.STATE_OPEN,
|
||||
change: ProposalChange = { __typename: undefined },
|
||||
rejectionReason: ProposalRejectionReason | null = null
|
||||
): OnProposalFragmentFragment => ({
|
||||
__typename: 'Proposal',
|
||||
id: Math.random().toString(),
|
||||
datetime: Math.random().toString(),
|
||||
rationale: {
|
||||
title,
|
||||
description: '',
|
||||
},
|
||||
rejectionReason,
|
||||
state,
|
||||
terms: {
|
||||
__typename: 'ProposalTerms',
|
||||
enactmentDatetime: '2022-12-09T14:40:38Z',
|
||||
change,
|
||||
},
|
||||
});
|
||||
|
||||
const INITIAL = useToasts.getState();
|
||||
|
||||
const clear = () => {
|
||||
useToasts.setState(INITIAL);
|
||||
};
|
||||
|
||||
describe('useProposalToasts', () => {
|
||||
beforeEach(clear);
|
||||
afterAll(clear);
|
||||
|
||||
it.each(PROPOSAL_STATES_TO_TOAST)(
|
||||
'renders toast for %s proposal',
|
||||
async (state) => {
|
||||
const mockProposal: MockedResponse<OnProposalSubscription> = {
|
||||
request: {
|
||||
query: OnProposalDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals: generateProposal(
|
||||
'Things to change',
|
||||
state,
|
||||
NEW_MARKET_CHANGE
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = renderUseProposalToasts([mockProposal]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(1);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const IGNORE_STATES = Object.keys(ProposalState).filter((state) => {
|
||||
return !PROPOSAL_STATES_TO_TOAST.includes(state as ProposalState);
|
||||
}) as ProposalState[];
|
||||
it.each(IGNORE_STATES)(
|
||||
'does not render toast for %s proposal',
|
||||
async (state) => {
|
||||
const mockFailedProposal: MockedResponse<OnProposalSubscription> = {
|
||||
request: {
|
||||
query: OnProposalDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals: generateProposal('Things to change but ignored', state),
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = renderUseProposalToasts([mockFailedProposal]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(0);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it('does not render toast for empty proposal', async () => {
|
||||
const error = console.error;
|
||||
console.error = () => {
|
||||
/* no op */
|
||||
};
|
||||
const mockEmptyProposal: MockedResponse<OnProposalSubscription> = {
|
||||
request: {
|
||||
query: OnProposalDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals: undefined as unknown as OnProposalFragmentFragment,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { result } = renderUseProposalToasts([mockEmptyProposal]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(0);
|
||||
});
|
||||
console.error = error;
|
||||
});
|
||||
|
||||
const allTypes: [
|
||||
ProposalChange['__typename'],
|
||||
ProposalChange,
|
||||
ProposalState?
|
||||
][] = [...GenericToastProposals, UPDATE_NETWORK_PARAMETER_CHANGE].map(
|
||||
(ch) => [ch.__typename, ch, sample(PROPOSAL_STATES_TO_TOAST)]
|
||||
);
|
||||
it.each(allTypes)(
|
||||
'renders toast for %s proposal',
|
||||
async (_, change, state) => {
|
||||
const proposalData = generateProposal('Things to change', state, change);
|
||||
const mockProposal: MockedResponse<OnProposalSubscription> = {
|
||||
request: {
|
||||
query: OnProposalDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals: proposalData,
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = renderUseProposalToasts([mockProposal]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(1);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('ProposalToastContent', () => {
|
||||
const genericTypes: [
|
||||
ProposalChange['__typename'],
|
||||
ProposalChange,
|
||||
ProposalState?
|
||||
][] = GenericToastProposals.map((ch) => [
|
||||
ch.__typename,
|
||||
ch,
|
||||
sample(PROPOSAL_STATES_TO_TOAST),
|
||||
]);
|
||||
it.each(genericTypes)(
|
||||
'renders generic toast content for %s',
|
||||
async (_, change, state) => {
|
||||
const proposalData = generateProposal('Things to change', state, change);
|
||||
const { container } = render(
|
||||
<ProposalToastContent proposal={proposalData} />
|
||||
);
|
||||
const title = container.querySelector(
|
||||
'[data-testid="proposal-toast-title"]'
|
||||
);
|
||||
const rationale = container.querySelector(
|
||||
'[data-testid="proposal-toast-rationale-title"]'
|
||||
);
|
||||
const expectedChangeName = change.__typename
|
||||
? ProposalChangeMapping[change.__typename]
|
||||
: '';
|
||||
const expectedState =
|
||||
ProposalStateMapping[proposalData.state].toLocaleLowerCase();
|
||||
expect(title).toHaveTextContent(
|
||||
`${expectedChangeName} proposal ${expectedState}`
|
||||
);
|
||||
expect(rationale).toHaveTextContent('Things to change');
|
||||
}
|
||||
);
|
||||
|
||||
it('renders specific content for UpdateNetworkParameter proposal', () => {
|
||||
const proposalData = generateProposal(
|
||||
'Things to change',
|
||||
ProposalState.STATE_OPEN,
|
||||
UPDATE_NETWORK_PARAMETER_CHANGE
|
||||
);
|
||||
const { container } = render(
|
||||
<ProposalToastContent proposal={proposalData} />
|
||||
);
|
||||
const title = container.querySelector(
|
||||
'[data-testid="proposal-toast-title"]'
|
||||
);
|
||||
const rationale = container.querySelector(
|
||||
'[data-testid="proposal-toast-rationale-title"]'
|
||||
);
|
||||
const param = container.querySelector(
|
||||
'[data-testid="proposal-toast-network-param"]'
|
||||
);
|
||||
expect(title).toHaveTextContent('Update network parameter proposal open');
|
||||
expect(rationale).toBe(null);
|
||||
expect(param).toHaveTextContent('Update abc.def to 123');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
ProposalChangeMapping,
|
||||
ProposalRejectionReasonMapping,
|
||||
ProposalStateMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import type { Toast } from '@vegaprotocol/ui-toolkit';
|
||||
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
useOnProposalSubscription,
|
||||
type OnProposalFragmentFragment,
|
||||
} from './__generated__/Proposal';
|
||||
|
||||
export const PROPOSAL_STATES_TO_TOAST = [
|
||||
ProposalState.STATE_DECLINED,
|
||||
ProposalState.STATE_ENACTED,
|
||||
ProposalState.STATE_OPEN,
|
||||
ProposalState.STATE_PASSED,
|
||||
];
|
||||
const CLOSE_AFTER = 0;
|
||||
type Proposal = OnProposalFragmentFragment;
|
||||
|
||||
const ProposalDetails = ({ proposal }: { proposal: Proposal }) => {
|
||||
const change = proposal.terms.change;
|
||||
switch (change.__typename) {
|
||||
case 'UpdateNetworkParameter':
|
||||
return <UpdateNetworkParameterDetails proposal={proposal} />;
|
||||
default:
|
||||
// generic details: rationale title and rejection reason if rejected
|
||||
return (
|
||||
<>
|
||||
{proposal.rationale.title ? (
|
||||
<p data-testid="proposal-toast-rationale-title" className="italic">
|
||||
{proposal.rationale.title}
|
||||
</p>
|
||||
) : null}
|
||||
{proposal.state === ProposalState.STATE_REJECTED &&
|
||||
proposal.rejectionReason ? (
|
||||
<p data-testid="proposal-toast-rejection-reason">
|
||||
{t('Rejection reason:')}{' '}
|
||||
{ProposalRejectionReasonMapping[proposal.rejectionReason]}
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const UpdateNetworkParameterDetails = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
}) => {
|
||||
const change = proposal.terms.change;
|
||||
if (change.__typename !== 'UpdateNetworkParameter') return null;
|
||||
return (
|
||||
<p data-testid="proposal-toast-network-param" className="italic">
|
||||
'{t('Update ')}
|
||||
<span className="break-all">{change.networkParameter.key}</span>
|
||||
{t(' to ')}
|
||||
<span>{change.networkParameter.value}</span>'
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProposalToastContent = ({ proposal }: { proposal: Proposal }) => {
|
||||
const tokenLink = useLinks(DApp.Governance);
|
||||
const change = proposal.terms.change;
|
||||
|
||||
// Generates toast's title,
|
||||
// e.g. Update market proposal enacted, New transfer proposal open, ...
|
||||
const title = t('%s proposal %s', [
|
||||
change.__typename ? ProposalChangeMapping[change.__typename] : 'Unknown',
|
||||
ProposalStateMapping[proposal.state].toLowerCase(),
|
||||
]);
|
||||
|
||||
const enactment = Date.parse(proposal.terms.enactmentDatetime);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ToastHeading data-testid="proposal-toast-title">{title}</ToastHeading>
|
||||
<ProposalDetails proposal={proposal} />
|
||||
{!isNaN(enactment) && (
|
||||
<p>
|
||||
{t('Enactment date:')} {getDateTimeFormat().format(enactment)}
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<ExternalLink
|
||||
href={tokenLink(TOKEN_PROPOSAL).replace(':id', proposal?.id || '')}
|
||||
>
|
||||
{t('View proposal details')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const useProposalToasts = () => {
|
||||
const { setToast, remove } = useToasts((store) => ({
|
||||
setToast: store.setToast,
|
||||
remove: store.remove,
|
||||
}));
|
||||
|
||||
const fromProposal = useCallback(
|
||||
(proposal: Proposal): Toast => {
|
||||
const id = `proposal-toast-${proposal.id}`;
|
||||
return {
|
||||
id,
|
||||
intent: Intent.Warning,
|
||||
content: <ProposalToastContent proposal={proposal} />,
|
||||
onClose: () => {
|
||||
remove(id);
|
||||
},
|
||||
closeAfter: CLOSE_AFTER,
|
||||
};
|
||||
},
|
||||
[remove]
|
||||
);
|
||||
|
||||
return useOnProposalSubscription({
|
||||
onData: ({ data }) => {
|
||||
const proposal = data.data?.proposals;
|
||||
if (!proposal || !proposal.terms.change.__typename) return;
|
||||
if (PROPOSAL_STATES_TO_TOAST.includes(proposal.state)) {
|
||||
setToast(fromProposal(proposal));
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,96 +0,0 @@
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { UpdateNetworkParameter } from '@vegaprotocol/types';
|
||||
import { ProposalStateMapping } from '@vegaprotocol/types';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import type { Toast } from '@vegaprotocol/ui-toolkit';
|
||||
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback } from 'react';
|
||||
import type { UpdateNetworkParameterProposalFragment } from './__generated__/Proposal';
|
||||
import { useOnUpdateNetworkParametersSubscription } from './__generated__/Proposal';
|
||||
|
||||
export const PROPOSAL_STATES_TO_TOAST = [
|
||||
ProposalState.STATE_DECLINED,
|
||||
ProposalState.STATE_ENACTED,
|
||||
ProposalState.STATE_OPEN,
|
||||
ProposalState.STATE_PASSED,
|
||||
];
|
||||
const CLOSE_AFTER = 0;
|
||||
type Proposal = UpdateNetworkParameterProposalFragment;
|
||||
|
||||
const UpdateNetworkParameterToastContent = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
}) => {
|
||||
const tokenLink = useLinks(DApp.Governance);
|
||||
const change = proposal.terms.change as UpdateNetworkParameter;
|
||||
const title = t('Network change proposal %s').replace(
|
||||
'%s',
|
||||
ProposalStateMapping[proposal.state].toLowerCase()
|
||||
);
|
||||
const enactment = Date.parse(proposal.terms.enactmentDatetime);
|
||||
return (
|
||||
<div>
|
||||
<ToastHeading>{title}</ToastHeading>
|
||||
<p className="italic">
|
||||
'{t('Update ')}
|
||||
<span className="break-all">{change.networkParameter.key}</span>
|
||||
{t(' to ')}
|
||||
<span>{change.networkParameter.value}</span>'
|
||||
</p>
|
||||
{!isNaN(enactment) && (
|
||||
<p>
|
||||
{t('Enactment date:')} {getDateTimeFormat().format(enactment)}
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<ExternalLink
|
||||
href={tokenLink(TOKEN_PROPOSAL).replace(':id', proposal?.id || '')}
|
||||
>
|
||||
{t('View proposal details')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const useUpdateNetworkParametersToasts = () => {
|
||||
const { setToast, remove } = useToasts((store) => ({
|
||||
setToast: store.setToast,
|
||||
remove: store.remove,
|
||||
}));
|
||||
|
||||
const fromProposal = useCallback(
|
||||
(proposal: Proposal): Toast => {
|
||||
const id = `update-network-param-proposal-${proposal.id}`;
|
||||
return {
|
||||
id: `update-network-param-proposal-${proposal.id}`,
|
||||
intent: Intent.Warning,
|
||||
content: <UpdateNetworkParameterToastContent proposal={proposal} />,
|
||||
onClose: () => {
|
||||
remove(id);
|
||||
},
|
||||
closeAfter: CLOSE_AFTER,
|
||||
};
|
||||
},
|
||||
[remove]
|
||||
);
|
||||
|
||||
return useOnUpdateNetworkParametersSubscription({
|
||||
onData: ({ data }) => {
|
||||
// note proposals is poorly named, it is actually a single proposal
|
||||
const proposal = data.data?.proposals;
|
||||
if (!proposal) return;
|
||||
if (proposal.terms.change.__typename !== 'UpdateNetworkParameter') return;
|
||||
|
||||
// if one of the following states show a toast
|
||||
if (PROPOSAL_STATES_TO_TOAST.includes(proposal.state)) {
|
||||
setToast(fromProposal(proposal));
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,168 +0,0 @@
|
||||
import merge from 'lodash/merge';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
PROPOSAL_STATES_TO_TOAST,
|
||||
useUpdateNetworkParametersToasts,
|
||||
} from './use-update-network-paramaters-toasts';
|
||||
import type {
|
||||
UpdateNetworkParameterProposalFragment,
|
||||
OnUpdateNetworkParametersSubscription,
|
||||
} from './__generated__/Proposal';
|
||||
import { OnUpdateNetworkParametersDocument } from './__generated__/Proposal';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { waitFor, renderHook } from '@testing-library/react';
|
||||
|
||||
const render = (mocks?: MockedResponse[]) => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={mocks}>{children}</MockedProvider>
|
||||
);
|
||||
return renderHook(() => useUpdateNetworkParametersToasts(), { wrapper });
|
||||
};
|
||||
|
||||
const generateUpdateNetworkParametersProposal = (
|
||||
key: string,
|
||||
value: string,
|
||||
state: ProposalState = ProposalState.STATE_OPEN
|
||||
): UpdateNetworkParameterProposalFragment => ({
|
||||
__typename: 'Proposal',
|
||||
id: Math.random().toString(),
|
||||
datetime: Math.random().toString(),
|
||||
state,
|
||||
terms: {
|
||||
__typename: 'ProposalTerms',
|
||||
enactmentDatetime: '2022-12-09T14:40:38Z',
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
__typename: 'NetworkParameter',
|
||||
key,
|
||||
value,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const INITIAL = useToasts.getState();
|
||||
|
||||
const clear = () => {
|
||||
useToasts.setState(INITIAL);
|
||||
};
|
||||
|
||||
describe('useUpdateNetworkParametersToasts', () => {
|
||||
beforeEach(clear);
|
||||
afterAll(clear);
|
||||
|
||||
it.each(PROPOSAL_STATES_TO_TOAST)(
|
||||
'toasts for %s network param proposals',
|
||||
async (state) => {
|
||||
const mockOpenProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
|
||||
{
|
||||
request: {
|
||||
query: OnUpdateNetworkParametersDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals: generateUpdateNetworkParametersProposal(
|
||||
'abc.def',
|
||||
'123.456',
|
||||
state
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = render([mockOpenProposal]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(1);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const IGNORE_STATES = Object.keys(ProposalState).filter((state) => {
|
||||
return !PROPOSAL_STATES_TO_TOAST.includes(state as ProposalState);
|
||||
}) as ProposalState[];
|
||||
it.each(IGNORE_STATES)('does not toast for %s proposals', async (state) => {
|
||||
const mockFailedProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
|
||||
{
|
||||
request: {
|
||||
query: OnUpdateNetworkParametersDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals: generateUpdateNetworkParametersProposal(
|
||||
'abc.def',
|
||||
'123.456',
|
||||
state
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = render([mockFailedProposal]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not return toast for empty propsal', async () => {
|
||||
const error = console.error;
|
||||
console.error = () => {
|
||||
/* no op */
|
||||
};
|
||||
const mockEmptyProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
|
||||
{
|
||||
request: {
|
||||
query: OnUpdateNetworkParametersDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals:
|
||||
undefined as unknown as UpdateNetworkParameterProposalFragment,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { result } = render([mockEmptyProposal]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(0);
|
||||
});
|
||||
console.error = error;
|
||||
});
|
||||
|
||||
it('does not return toast for wrong proposal type', async () => {
|
||||
const wrongProposalType = merge(
|
||||
generateUpdateNetworkParametersProposal('a', 'b'),
|
||||
{
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewMarket',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
const mockWrongProposalType: MockedResponse<OnUpdateNetworkParametersSubscription> =
|
||||
{
|
||||
request: {
|
||||
query: OnUpdateNetworkParametersDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals: wrongProposalType,
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = render([mockWrongProposalType]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,10 +4,13 @@ import { TradesTable } from './trades-table';
|
||||
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface TradesContainerProps {
|
||||
marketId: string;
|
||||
gridProps?: ReturnType<typeof useDataGridEvents>;
|
||||
gridProps: ReturnType<typeof useDataGridEvents>;
|
||||
}
|
||||
|
||||
export const TradesManager = ({
|
||||
@@ -16,19 +19,60 @@ export const TradesManager = ({
|
||||
}: TradesContainerProps) => {
|
||||
const update = useDealTicketFormValues((state) => state.updateAll);
|
||||
|
||||
const { data, error } = useDataProvider({
|
||||
const { data, error, load, pageInfo } = useDataProvider({
|
||||
dataProvider: tradesWithMarketProvider,
|
||||
variables: { marketId },
|
||||
});
|
||||
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
|
||||
undefined
|
||||
);
|
||||
const { onFilterChanged, ...props } = gridProps || {};
|
||||
const onRowDataUpdated = useCallback(
|
||||
({ api }: { api: AgGridReact['api'] }) => {
|
||||
setHasDisplayedRow(!!api.getDisplayedRowCount());
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<TradesTable
|
||||
rowData={data}
|
||||
onClick={(price?: string) => {
|
||||
update(marketId, { price });
|
||||
}}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No trades')}
|
||||
{...gridProps}
|
||||
/>
|
||||
<div className="flex flex-col h-full">
|
||||
<TradesTable
|
||||
rowData={data}
|
||||
onClick={(price?: string) => {
|
||||
update(marketId, { price });
|
||||
}}
|
||||
onFilterChanged={(event) => {
|
||||
onRowDataUpdated(event);
|
||||
onFilterChanged(event);
|
||||
}}
|
||||
onRowDataUpdated={onRowDataUpdated}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No trades')}
|
||||
{...props}
|
||||
/>
|
||||
<div className="flex justify-between border-t border-default p-1 items-center">
|
||||
<div className="text-xs">
|
||||
{t(
|
||||
'Depending on data node retention you may not be able see the "full" history'
|
||||
)}
|
||||
</div>
|
||||
<div className="flex text-xs items-center">
|
||||
{data?.length && !pageInfo?.hasNextPage
|
||||
? t('all %s items loaded', [data.length.toString()])
|
||||
: t('%s items loaded', [
|
||||
data?.length ? data.length.toString() : ' ',
|
||||
])}
|
||||
{pageInfo?.hasNextPage ? (
|
||||
<Button size="extra-small" className="ml-1" onClick={() => load()}>
|
||||
{t('Load more')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{data?.length && hasDisplayedRow === false ? (
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
|
||||
{t('No trades matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Generated
+42
-3
@@ -813,7 +813,7 @@ export type DispatchStrategy = {
|
||||
/** Minimum notional time-weighted averaged position required for a party to be considered eligible */
|
||||
notionalTimeWeightedAveragePositionRequirement: Scalars['String'];
|
||||
/** Ascending order list of start rank and corresponding share ratio */
|
||||
rankTable?: Maybe<RankTable>;
|
||||
rankTable?: Maybe<Array<Maybe<RankTable>>>;
|
||||
/** Minimum number of governance tokens, e.g. VEGA, staked for a party to be considered eligible */
|
||||
stakingRequirement: Scalars['String'];
|
||||
/** The teams in scope for the reward, if the entity is teams */
|
||||
@@ -3387,6 +3387,8 @@ export type Party = {
|
||||
transfersConnection?: Maybe<TransferConnection>;
|
||||
/** The current reward vesting summary of the party for the last epoch */
|
||||
vestingBalancesSummary: PartyVestingBalancesSummary;
|
||||
/** The current statistics about a party's vesting rewards for the last epoch */
|
||||
vestingStats?: Maybe<PartyVestingStats>;
|
||||
/** All votes on proposals in the Vega network by the given party */
|
||||
votesConnection?: Maybe<ProposalVoteConnection>;
|
||||
/** The list of all withdrawals initiated by the party */
|
||||
@@ -3502,6 +3504,7 @@ export type PartytradesConnectionArgs = {
|
||||
/** Represents a party on Vega, could be an ethereum wallet address in the future */
|
||||
export type PartytransfersConnectionArgs = {
|
||||
direction?: InputMaybe<TransferDirection>;
|
||||
isReward?: InputMaybe<Scalars['Boolean']>;
|
||||
pagination?: InputMaybe<Pagination>;
|
||||
};
|
||||
|
||||
@@ -3616,6 +3619,17 @@ export type PartyVestingBalancesSummary = {
|
||||
vestingBalances?: Maybe<Array<PartyVestingBalance>>;
|
||||
};
|
||||
|
||||
/** Statistics about a party's vesting rewards */
|
||||
export type PartyVestingStats = {
|
||||
__typename?: 'PartyVestingStats';
|
||||
/** Epoch for which the statistics are valid */
|
||||
epochSeq: Scalars['Int'];
|
||||
/** The balance of the party, in quantum. */
|
||||
quantumBalance: Scalars['String'];
|
||||
/** The reward bonus multiplier */
|
||||
rewardBonusMultiplier: Scalars['String'];
|
||||
};
|
||||
|
||||
/** Create an order linked to an index rather than a price */
|
||||
export type PeggedOrder = {
|
||||
__typename?: 'PeggedOrder';
|
||||
@@ -4819,7 +4833,7 @@ export type QueryprotocolUpgradeProposalsArgs = {
|
||||
|
||||
/** Queries allow a caller to read data and filter data via GraphQL. */
|
||||
export type QueryreferralSetRefereesArgs = {
|
||||
aggregationDays?: InputMaybe<Scalars['Int']>;
|
||||
aggregationEpochs?: InputMaybe<Scalars['Int']>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
pagination?: InputMaybe<Pagination>;
|
||||
referee?: InputMaybe<Scalars['ID']>;
|
||||
@@ -4905,6 +4919,7 @@ export type QuerytransferArgs = {
|
||||
/** Queries allow a caller to read data and filter data via GraphQL. */
|
||||
export type QuerytransfersConnectionArgs = {
|
||||
direction?: InputMaybe<TransferDirection>;
|
||||
isReward?: InputMaybe<Scalars['Boolean']>;
|
||||
pagination?: InputMaybe<Pagination>;
|
||||
partyId?: InputMaybe<Scalars['ID']>;
|
||||
};
|
||||
@@ -5075,12 +5090,16 @@ export type ReferralSetStats = {
|
||||
partyId: Scalars['ID'];
|
||||
/** Running volume for the set based on the window length of the current referral program. */
|
||||
referralSetRunningNotionalTakerVolume: Scalars['String'];
|
||||
/** The referrer's taker volume */
|
||||
referrerTakerVolume: Scalars['String'];
|
||||
/** Reward factor applied to the party. */
|
||||
rewardFactor: Scalars['String'];
|
||||
/** The proportion of the referees taker fees to be rewarded to the referrer. */
|
||||
rewardsFactorMultiplier: Scalars['String'];
|
||||
/** The multiplier applied to the referral reward factor when calculating referral rewards due to the referrer. */
|
||||
rewardsMultiplier: Scalars['String'];
|
||||
/** Indicates if the referral set was eligible to be part of the referral program. */
|
||||
wasEligible: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
/** Connection type for retrieving cursor-based paginated referral set statistics information */
|
||||
@@ -6102,11 +6121,31 @@ export enum TransferDirection {
|
||||
export type TransferEdge = {
|
||||
__typename?: 'TransferEdge';
|
||||
cursor: Scalars['String'];
|
||||
node: Transfer;
|
||||
node: TransferNode;
|
||||
};
|
||||
|
||||
/** A transfer fee record */
|
||||
export type TransferFee = {
|
||||
__typename?: 'TransferFee';
|
||||
/** The fee amount */
|
||||
amount: Scalars['String'];
|
||||
/** The epoch when this fee was paid */
|
||||
epoch: Scalars['Int'];
|
||||
/** Transfer ID of the transfer for which the fee was paid */
|
||||
transferId: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type TransferKind = OneOffGovernanceTransfer | OneOffTransfer | RecurringGovernanceTransfer | RecurringTransfer;
|
||||
|
||||
/** A transfer record with the fee payments associated with the transfer */
|
||||
export type TransferNode = {
|
||||
__typename?: 'TransferNode';
|
||||
/** The list of fee payments made */
|
||||
fees?: Maybe<Array<Maybe<TransferFee>>>;
|
||||
/** The transfer record */
|
||||
transfer: Transfer;
|
||||
};
|
||||
|
||||
export type TransferResponse = {
|
||||
__typename?: 'TransferResponse';
|
||||
/** The balances of accounts involved in the transfer */
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
GovernanceTransferKind,
|
||||
GovernanceTransferType,
|
||||
PeggedReference,
|
||||
ProposalChange,
|
||||
} from './__generated__/types';
|
||||
import type { AccountType } from './__generated__/types';
|
||||
import type {
|
||||
@@ -299,6 +300,29 @@ export const OrderTypeMapping: {
|
||||
TYPE_NETWORK: 'Network',
|
||||
};
|
||||
|
||||
/**
|
||||
* Proposal change type mapping
|
||||
*/
|
||||
export const ProposalChangeMapping: Record<
|
||||
NonNullable<ProposalChange['__typename']>,
|
||||
string
|
||||
> = {
|
||||
NewMarket: 'New market',
|
||||
UpdateMarket: 'Update market',
|
||||
UpdateNetworkParameter: 'Update network parameter',
|
||||
NewAsset: 'New asset',
|
||||
UpdateAsset: 'Update asset',
|
||||
/* cspell:disable-next-line */
|
||||
NewFreeform: 'New free-form',
|
||||
NewTransfer: 'New transfer',
|
||||
CancelTransfer: 'Cancel transfer',
|
||||
UpdateMarketState: 'Update market state',
|
||||
NewSpotMarket: 'New spot market',
|
||||
UpdateSpotMarket: 'Update spot market',
|
||||
UpdateVolumeDiscountProgram: 'Update volume discount program',
|
||||
UpdateReferralProgram: 'Update referral program',
|
||||
};
|
||||
|
||||
/**
|
||||
* Reason for the proposal being rejected by the core node
|
||||
*/
|
||||
|
||||
@@ -1,77 +1,68 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { Sparkline } from './sparkline';
|
||||
import type { SparklineProps } from './sparkline';
|
||||
|
||||
const props = {
|
||||
data: [
|
||||
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
|
||||
10, 11, 12,
|
||||
],
|
||||
muted: true,
|
||||
};
|
||||
describe('Sparkline', () => {
|
||||
let props: SparklineProps = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
it('Renders an svg with a single path', () => {
|
||||
render(<Sparkline {...props} />);
|
||||
expect(screen.getByTestId('sparkline-svg')).toBeInTheDocument();
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toBeInTheDocument();
|
||||
expect(path).toHaveAttribute('d', expect.any(String));
|
||||
expect(path).toHaveAttribute('stroke', expect.any(String));
|
||||
expect(path).toHaveAttribute('stroke-width', '1');
|
||||
expect(path).toHaveAttribute('fill', 'transparent');
|
||||
});
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
data: [
|
||||
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8,
|
||||
9, 10, 11, 12,
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
it('Requires a data prop but width and height are optional', () => {
|
||||
render(<Sparkline {...props} />);
|
||||
const svg = screen.getByTestId('sparkline-svg');
|
||||
expect(svg).toHaveAttribute('width', '60');
|
||||
expect(svg).toHaveAttribute('height', '15');
|
||||
});
|
||||
it('Renders an svg with a single path', () => {
|
||||
render(<Sparkline {...props} />);
|
||||
expect(screen.getByTestId('sparkline-svg')).toBeInTheDocument();
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toBeInTheDocument();
|
||||
expect(path).toHaveAttribute('d', expect.any(String));
|
||||
expect(path).toHaveAttribute('stroke-width', '1');
|
||||
});
|
||||
|
||||
it('Renders a red line if the last value is less than the first', () => {
|
||||
props.data[0] = 10;
|
||||
props.data[props.data.length - 1] = 5;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-market-red dark:stroke-market-red'
|
||||
);
|
||||
});
|
||||
it('Requires a data prop but width and height are optional', () => {
|
||||
render(<Sparkline {...props} />);
|
||||
const svg = screen.getByTestId('sparkline-svg');
|
||||
expect(svg).toHaveAttribute('width', '60');
|
||||
expect(svg).toHaveAttribute('height', '15');
|
||||
});
|
||||
|
||||
it('Renders a green line if the last value is greater than the first', () => {
|
||||
props.data[0] = 5;
|
||||
props.data[props.data.length - 1] = 10;
|
||||
props.muted = true;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-market-green-600 dark:stroke-market-green'
|
||||
);
|
||||
});
|
||||
it('Renders a red line if the last value is less than the first', () => {
|
||||
props.data[0] = 10;
|
||||
props.data[props.data.length - 1] = 5;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-market-red dark:stroke-market-red'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders a white line if the first and last values are equal', () => {
|
||||
props.data[0] = 5;
|
||||
props.data[props.data.length - 1] = 5;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-black/40 dark:stroke-white/40'
|
||||
);
|
||||
});
|
||||
it('Renders a green line if the last value is greater than the first', () => {
|
||||
props.data[0] = 5;
|
||||
props.data[props.data.length - 1] = 10;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-market-green-600 dark:stroke-market-green'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders a gray line if there are not 24 values', () => {
|
||||
props.data = [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
|
||||
22, 23,
|
||||
];
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.queryAllByTestId('sparkline-path');
|
||||
expect(paths).toHaveLength(2);
|
||||
expect(paths[0]).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-black/40 dark:stroke-white/40'
|
||||
);
|
||||
it('Renders a white line if the first and last values are equal', () => {
|
||||
props.data[0] = 5;
|
||||
props.data[props.data.length - 1] = 5;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-black/40 dark:stroke-white/40'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user