Compare commits

...
Author SHA1 Message Date
m.ray 8e115ad36c Update libs/trades/src/lib/trades-data-provider.ts 2023-12-05 10:21:24 +00:00
Madalina Raicu 2cfbb4a16f chore(trading): update generic data provider 2023-12-04 14:53:57 +00:00
m.ray 80ab8821d0 fix(trading): live time fraction zero redundant check (#5420) 2023-12-02 11:36:58 +00:00
Art 7100b0e9fc chore(accounts): no assets avaiable in transfer form (#5358) 2023-12-01 17:05:22 +00:00
614a83b7d6 chore(trading): merge main back in develop (fees discounts, discount stats from prev epoch) (#5415)
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
Co-authored-by: asiaznik <artur@vegaprotocol.io>
2023-12-01 17:03:41 +00:00
m.ray 3dc77b0eff chore(trading): add close position button back to console (#5407) 2023-12-01 16:38:32 +00:00
Art 127e784ceb chore(trading): don't redirect to inactive markets (#5359) 2023-12-01 14:42:26 +01:00
EddandMadalina Raicu e06f4818fc feat(explorer): add signature viewer (#5264)
Co-authored-by: Madalina Raicu <madalina@raygroup.uk>
2023-11-30 11:54:18 +00:00
Bartłomiej Głownia 8182da3b31 feat(deal-ticket): fix postOnly mapping in mapFormValuesToOrderSubmission (#5375) 2023-11-30 11:25:25 +01:00
Ben c8c56307bb chore(trading): update vega version (#5396) 2023-11-30 09:52:05 +00:00
Ben a8cd7f157f chore(trading): migrate cypress tests to python (#5367) 2023-11-30 09:42:57 +00:00
Bartłomiej Głownia 4f18caa486 feat(trading): upgrade i18n, fix plurals (#5331) 2023-11-30 07:31:44 +00:00
Matthew Russell 5c7c626bbc Merge pull request #5393 from vegaprotocol/chore/sync-main
chore(trading, datagrid, liquidity, proposals, ui-toolkit): sync main
2023-11-29 18:08:50 -08:00
Matthew Russell e4c4c20631 fix: duplicate props in ag-grid-themed 2023-11-29 14:09:08 -08:00
Matthew Russell 0697302d07 Merge branch 'main' into chore/sync-main 2023-11-29 14:06:45 -08:00
ArtandMadalina Raicu 2d926c0ce0 fix(governance): sensible vote numbers (#5384)
Co-authored-by: Madalina Raicu <madalina@raygroup.uk>
2023-11-29 10:38:41 -08:00
Art f57d6a7c7b fix(trading): missing volume discount program issue, inverted tiers (#5378) 2023-11-29 17:23:14 +00:00
m.ray 15f905046f fix(trading): fees accrued tooltip (#5387) 2023-11-29 17:17:44 +00:00
Bartłomiej Głownia 4f7918f64e feat(trading): remove proposal warning from market header (#5385) 2023-11-29 17:05:53 +00:00
Bartłomiej Głownia 52ab0562b0 feat(trading): refactor ledger export form validation (#5379) 2023-11-29 15:27:00 +00:00
m.ray 4e2b0d1b1d fix(trading): fix ag-grid transparent filters (#5377) 2023-11-29 15:24:37 +00:00
m.ray 0b0bcad9b3 fix(trading): ag-grid compactness adjustment (#5382) 2023-11-29 14:29:02 +00:00
Bartłomiej GłowniaandMatthew Russell bcf17bb34e feat(trading): i18n language switcher (#5320)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-11-29 15:16:17 +01:00
Bartłomiej Głownia a2b9b0da05 feat(deal-ticket): enhance for fee discounts (#5353) 2023-11-29 12:59:42 +00:00
Bartłomiej Głownia 7588d0cd11 feat(trading): refactor ledger export form validation (#5362) 2023-11-29 13:28:31 +01:00
m.ray 5ee1748495 fix(trading): ag-grid styling updates and filter fixes (#5368) 2023-11-28 22:13:12 +00:00
m.ray eac26c1966 fix(trading): empty connect wallet dialog (#5356) 2023-11-28 14:43:31 +00:00
115 changed files with 1815 additions and 1613 deletions
@@ -12,7 +12,8 @@ import { type VegaICellRendererParams } from '@vegaprotocol/datagrid';
import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { useNavigate } from 'react-router-dom';
import { type RowClickedEvent, ColDef } from 'ag-grid-community';
import { type ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community';
type AssetsTableProps = {
data: AssetFieldsFragment[] | null;
@@ -8,7 +8,8 @@ import {
type VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { type RowClickedEvent, ColDef } from 'ag-grid-community';
import { type ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
@@ -0,0 +1,59 @@
import { useState } from 'react';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
import {
CopyWithTooltip,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
interface SignatureProps {
signature: BlockExplorerTransactionResult['signature'];
}
const valueClass =
'font-mono px-2.5 py-0.5 text-xs max-w-[200px] cursor-pointer';
const valueClassClosed = 'text-ellipsis overflow-hidden';
const valueClassOpen = 'break-words text-left';
/**
* Viewer component for a vega signature. Featuers copy and pasting, truncation
*
* @param signature
*/
export const Signature = ({ signature }: SignatureProps) => {
const [isOpen, setIsOpen] = useState(false);
if (!signature || !signature.value || !signature.version || !signature.algo) {
return null;
}
return (
<div className="inline-flex border rounded signature-component relative pr-[20px]">
<span
className="bg-gray-100 px-2.5 py-0.5 text-xs text-gray-500 select-none cursor-default"
title={`Version ${signature.version}`}
>
{signature.algo}
</span>
<div
className={
isOpen
? `${valueClass} ${valueClassOpen}`
: `${valueClass} ${valueClassClosed}`
}
>
<CopyWithTooltip text={signature.value}>
<span title={signature.value}>{signature.value}</span>
</CopyWithTooltip>
</div>
<button
onClick={() => setIsOpen(!isOpen)}
className="absolute top-[-3px] right-0 pr-2"
title={t('Show full signature')}
>
<VegaIcon name={isOpen ? VegaIconNames.EYE_OFF : VegaIconNames.EYE} />
</button>
</div>
);
};
@@ -9,6 +9,7 @@ import { Time } from '../../../time';
import { ChainResponseCode } from '../chain-response-code/chain-reponse.code';
import { TxDataView } from '../../tx-data-view';
import Hash from '../../../links/hash';
import { Signature } from '../../../signature/signature';
interface TxDetailsSharedProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -75,6 +76,12 @@ export const TxDetailsShared = ({
<BlockLink height={height} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Signature')}</TableCell>
<TableCell>
<Signature signature={txData.signature} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Time')}</TableCell>
<TableCell>
@@ -98,6 +98,8 @@ describe('TxDetailsTransfer', () => {
},
},
signature: {
version: '1',
algo: 'vega/ed25519',
value:
'610c2e196a7d4fed4413b9e82af267b1ff3e30e943df3a3d28096fd60604d430d752fbaf6dd4f84d496be78885bb6118f40560bff7832c06bd7a3d67b718b700',
},
@@ -20,6 +20,8 @@ const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
'4b782482f587d291e8614219eb9a5ee9280fa2c58982dee71d976782a9be1964',
type: 'Submit Order',
signature: {
version: '1',
algo: 'vega/ed25519',
value: '123',
},
code: 0,
@@ -23,6 +23,8 @@ const txData: BlockExplorerTransactionResult = {
type: 'type',
command: {} as ValidatorHeartbeat,
signature: {
version: '1',
algo: 'vega/ed25519',
value: '123',
},
};
@@ -11,6 +11,8 @@ export interface BlockExplorerTransactionResult {
cursor: string;
command: components['schemas']['blockexplorerv1transaction'];
signature: {
version: string;
algo: string;
value: string;
};
error?: string;
@@ -1,182 +0,0 @@
import * as Schema from '@vegaprotocol/types';
const expirtyTooltip = 'expiry-tooltip';
const externalLink = 'external-link';
const itemHeader = 'item-header';
const itemValue = 'item-value';
const link = 'link';
const liquidityLink = 'view-liquidity-link';
const liquiditySupplied = 'liquidity-supplied';
const liquiditySuppliedTooltip = 'liquidity-supplied-tooltip';
const marketChange = 'market-change';
const marketExpiry = 'market-expiry';
const marketMode = 'market-trading-mode';
const marketName = 'header-title';
const marketPrice = 'market-price';
const marketSettlement = 'market-settlement-asset';
const marketSummaryBlock = 'header-summary';
const marketVolume = 'market-volume';
const percentageValue = 'price-change-percentage';
const priceChangeValue = 'price-change';
const tradingModeTooltip = 'trading-mode-tooltip';
describe('Market trading page', () => {
before(() => {
cy.clearAllLocalStorage();
cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0');
cy.wait('@MarketData');
cy.getByTestId(marketSummaryBlock).should('be.visible');
});
describe('Market summary', { tags: '@smoke' }, () => {
// 7002-SORD-001
// 7002-SORD-002
it('must display market name', () => {
// 6002-MDET-001
cy.getByTestId(marketName).should('not.be.empty');
});
it('must see market expiry', () => {
// 6002-MDET-002
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketExpiry).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Expiry');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
it('must see market price', () => {
// 6002-MDET-003
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketPrice).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Mark Price');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
it('must see market change', () => {
// 6002-MDET-004
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketChange).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Change (24h)');
cy.getByTestId(percentageValue).should('not.be.empty');
cy.getByTestId(priceChangeValue).should('not.be.empty');
});
});
});
it('must see market volume', () => {
// 6002-MDET-005
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketVolume).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Volume (24h)');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
it('must see market settlement', () => {
// 6002-MDET-008
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketSettlement).within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Settlement asset');
cy.getByTestId(itemValue).should('not.be.empty');
});
});
});
});
describe('Market tooltips', { tags: '@smoke' }, () => {
it('should see expiry tooltip', () => {
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketExpiry).within(() => {
cy.getByTestId(itemValue)
.should('have.text', 'Not time-based')
.realHover();
});
});
cy.getByTestId(expirtyTooltip)
.eq(0)
.should(
'contain.text',
'This market expires when triggered by its oracle, not on a set date.'
)
.within(() => {
cy.getByTestId(link)
.should('have.attr', 'href')
.and('include', Cypress.env('EXPLORER_URL'));
});
});
it('should see trading conditions tooltip', () => {
const toolTipLabel = 'tooltip-label';
const toolTipValue = 'tooltip-value';
const auctionToolTipLabels = [
'Auction start',
'Est. auction end',
'Target liquidity',
'Current liquidity',
'Est. uncrossing price',
'Est. uncrossing vol',
];
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(marketMode).within(() => {
cy.getByTestId(itemValue)
.should('contain.text', 'Monitoring auction')
.and('contain.text', 'liquidity')
.realHover();
});
});
cy.getByTestId(tradingModeTooltip)
.should(
'contain.text',
'This market is in auction until it reaches sufficient liquidity.'
)
.eq(0)
.within(() => {
cy.getByTestId(externalLink)
.should('have.attr', 'href')
.and('include', Cypress.env('TRADING_MODE_LINK'));
for (let i = 0; i < 6; i++) {
cy.getByTestId(toolTipLabel)
.eq(i)
.should('have.text', auctionToolTipLabels[i]);
cy.getByTestId(toolTipValue).eq(i).should('not.be.empty');
}
});
});
it('should see liquidity supplied tooltip', () => {
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId(liquiditySupplied).within(() => {
cy.getByTestId(itemValue).realHover();
});
});
cy.getByTestId(liquiditySuppliedTooltip)
.should('contain.text', 'Supplied stake')
.and('contain.text', 'Target stake')
.first()
.within(() => {
cy.getByTestId(liquidityLink).should(
'have.text',
'View liquidity provision table'
);
cy.getByTestId(externalLink).should(
'have.text',
'Learn about providing liquidity'
);
});
});
});
});
@@ -1,103 +0,0 @@
import * as Schema from '@vegaprotocol/types';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { createOrder } from '../support/create-order';
describe(
'vega wallet - prompt',
{ tags: '@regression', testIsolation: true },
() => {
describe('must submit order', { tags: '@smoke' }, () => {
// 7002-SORD-039
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must see a prompt to check connected vega wallet to approve transaction', () => {
// 0003-WTXN-002
cy.mockVegaWalletTransaction(1000);
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_MARKET,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
size: '100',
};
createOrder(order);
cy.getByTestId('toast-content').should(
'contain.text',
'Please go to your Vega wallet application and approve or reject the transaction.'
);
});
it('must show error returned by wallet ', () => {
// 0003-WTXN-009
// 0003-WTXN-011
// 0002-WCON-016
// 0003-WTXN-008
//trigger error from the wallet
cy.intercept('POST', 'http://localhost:1789/api/v2/requests', (req) => {
req.on('response', (res) => {
res.send({
jsonrpc: '2.0',
id: '1',
});
});
});
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_MARKET,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
size: '100',
};
createOrder(order);
cy.getByTestId('toast-content').should(
'contain.text',
'The connection to your Vega Wallet has been lost.'
);
cy.getByTestId('connect-vega-wallet').click();
cy.getByTestId('dialog-content').should('be.visible');
});
it('must see that the order was rejected by the connected wallet', () => {
// 0003-WTXN-007
//trigger rejection error from the wallet
cy.intercept('POST', 'http://localhost:1789/api/v2/requests', (req) => {
req.alias = 'client.send_transaction';
req.reply({
statusCode: 400,
body: {
jsonrpc: '2.0',
error: {
code: 3001,
data: 'the user rejected the wallet connection',
message: 'User error',
},
id: '0',
},
});
});
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_MARKET,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
size: '100',
};
createOrder(order);
cy.getByTestId('toast-content').should(
'contain.text',
'Error occurredthe user rejected the wallet connection'
);
});
});
}
);
@@ -26,18 +26,6 @@ describe('ethereum wallet', { tags: '@smoke', testIsolation: true }, () => {
cy.getByTestId('tab-deposits').should('not.be.empty');
});
it.skip('should see QR code modal for WalletConnect', () => {
// 0004-EWAL-003
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
cy.getByTestId('connect-eth-wallet-btn').click();
cy.getByTestId('web3-connector-list').should('exist');
cy.getByTestId('web3-connector-WalletConnect').click();
// testing if exists rather than visible because of the long loading time
cy.get('#w3m-modal').should('exist');
});
it('able to disconnect eth wallet', () => {
// 0004-EWAL-004
// 0004-EWAL-005
@@ -1,91 +0,0 @@
import {
mockConnectWallet,
mockConnectWalletWithUserError,
} from '@vegaprotocol/cypress';
const connectVegaBtn = 'connect-vega-wallet';
const manageVegaBtn = 'manage-vega-wallet';
const dialogContent = 'dialog-content';
describe('connect vega wallet', { tags: '@smoke', testIsolation: true }, () => {
beforeEach(() => {
// Using portfolio page as it requires vega wallet connection
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/portfolio');
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
});
it('can connect', () => {
// 0002-WCON-002
// 0002-WCON-005
// 0002-WCON-007
// 0002-WCON-009
mockConnectWallet();
cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]')
.click();
cy.wait('@walletReq');
cy.getByTestId(dialogContent).should(
'contain.text',
'Approve the connection from your Vega wallet app.'
);
cy.getByTestId(dialogContent).should('not.exist');
cy.getByTestId(manageVegaBtn).should('exist');
});
it('can not connect', () => {
// 0002-WCON-002
// 0002-WCON-005
// 0002-WCON-007
// 0002-WCON-015
mockConnectWalletWithUserError();
cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]')
.click();
cy.getByTestId('dialog-content')
.should('contain.text', 'User error')
.and('contain.text', 'the user rejected the wallet connection');
});
it('can change selected public key and disconnect', () => {
// 0002-WCON-022
// 0002-WCON-023
// 0002-WCON-025
// 0002-WCON-026
// 0002-WCON-021
// 0002-WCON-027
// 0002-WCON-030
// 0002-WCON-029
// 0002-WCON-008
// 0002-WCON-035
// 0002-WCON-014
// 0002-WCON-010
// 0003-WTXN-004
mockConnectWallet();
const key2 = Cypress.env('VEGA_PUBLIC_KEY2');
const truncatedKey2 = Cypress.env('TRUNCATED_VEGA_PUBLIC_KEY2');
cy.connectVegaWallet();
cy.getByTestId('manage-vega-wallet').click();
cy.getByTestId('keypair-list').should('exist');
cy.getByTestId(`key-${key2}`).should('contain.text', truncatedKey2);
cy.getByTestId(`key-${key2}`)
.find('[data-testid="copy-vega-public-key"]')
.should('be.visible');
cy.get(`[data-testid="key-${key2}"] > .mr-2`).click();
cy.getByTestId('keypair-list')
.find('[data-state="checked"]')
.should('be.visible');
cy.getByTestId('disconnect').click();
cy.getByTestId('connect-vega-wallet').should('exist');
cy.getByTestId('manage-vega-wallet').should('not.exist');
cy.getByTestId('connect-vega-wallet').click();
cy.contains('Enter a custom wallet location');
});
});
+1
View File
@@ -28,3 +28,4 @@ NX_REFERRALS=false
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
NX_DISABLE_CLOSE_POSITION=true
+2 -25
View File
@@ -1,34 +1,11 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { useGlobalStore } from '../../stores';
import { useTopTradedMarkets } from '../../lib/hooks/use-top-traded-markets';
import { Links } from '../../lib/links';
import { useNavigateToLastMarket } from '../../lib/hooks/use-navigate-to-last-market';
// The home pages only purpose is to redirect to the users last market,
// the top traded if they are new, or fall back to the list of markets.
// Thats why we just render a loader here
export const Home = () => {
const navigate = useNavigate();
const { data } = useTopTradedMarkets();
const marketId = useGlobalStore((store) => store.marketId);
useEffect(() => {
if (marketId) {
navigate(Links.MARKET(marketId), {
replace: true,
});
} else if (data) {
const marketDataId = data[0]?.id;
if (marketDataId) {
navigate(Links.MARKET(marketDataId), {
replace: true,
});
} else {
navigate(Links.MARKETS());
}
}
}, [marketId, data, navigate]);
useNavigateToLastMarket();
return (
<Splash>
@@ -1,7 +1,6 @@
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { DocsLinks, useEnvironment } from '@vegaprotocol/environment';
import { ButtonLink, ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
import { MarketProposalNotification } from '@vegaprotocol/proposals';
import type { Market } from '@vegaprotocol/markets';
import {
addDecimalsFormatNumber,
@@ -145,7 +144,6 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
/>
</HeaderStat>
)}
<MarketProposalNotification marketId={market.id} />
</>
);
};
+4 -3
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useMemo } from 'react';
import { addDecimalsFormatNumber, titlefy } from '@vegaprotocol/utils';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
import { ExternalLink, Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { Link, Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { getAsset, marketDataProvider, useMarket } from '@vegaprotocol/markets';
import { useGlobalStore, usePageTitleStore } from '../../stores';
import { TradeGrid } from './trade-grid';
@@ -117,12 +117,13 @@ export const MarketPage = () => {
defaults="Please choose another market from the <0>market list</0>"
ns={ns}
components={[
<ExternalLink
<Link
className="underline underline-offset-4 "
onClick={() => navigate(Links.MARKETS())}
key="link"
>
market list
</ExternalLink>,
</Link>,
]}
/>
</p>
@@ -43,7 +43,7 @@ export const OpenMarkets = () => {
if (!data) return;
// prevent navigating to the market page if any of the below cells are clicked
// event.preventDefault or event.stopPropagation dont seem to apply for aggird
// event.preventDefault or event.stopPropagation do not seem to apply for ag-grid
const colId = column.getColId();
if (
@@ -276,10 +276,12 @@ export const ApplyCodeForm = () => {
{/* TODO: Re-check plural forms once i18n is updated */}
{previewData && previewData.isEligible ? (
<div className="mt-10">
<h2 className="text-2xl mb-5">
{t('referralApplyPreviewMessage', {
count: nextBenefitTierEpochsValue,
})}
<h2 className="mb-5 text-2xl">
{t(
'youAreJoiningTheGroup',
'You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.',
{ count: nextBenefitTierEpochsValue }
)}
</h2>
<Statistics data={previewData} program={program} as="referee" />
</div>
@@ -75,22 +75,20 @@ export const useReferralProgram = () => {
const benefitTiers = sortBy(data.currentReferralProgram.benefitTiers, (t) =>
Number(t.referralRewardFactor)
)
.reverse()
.map((t, i) => {
return {
tier: i + 1,
rewardFactor: Number(t.referralRewardFactor),
commission: Number(t.referralRewardFactor) * 100 + '%',
discountFactor: Number(t.referralDiscountFactor),
discount: Number(t.referralDiscountFactor) * 100 + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: getNumberFormat(0).format(
Number(t.minimumRunningNotionalTakerVolume)
),
epochs: Number(t.minimumEpochs),
};
});
).map((t, i) => {
return {
tier: i + 1, // sorted in asc order, hence first is the lowest tier
rewardFactor: Number(t.referralRewardFactor),
commission: Number(t.referralRewardFactor) * 100 + '%',
discountFactor: Number(t.referralDiscountFactor),
discount: Number(t.referralDiscountFactor) * 100 + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: getNumberFormat(0).format(
Number(t.minimumRunningNotionalTakerVolume)
),
epochs: Number(t.minimumEpochs),
};
});
const stakingTiers = sortBy(
data.currentReferralProgram.stakingTiers,
@@ -1,3 +1,4 @@
import minBy from 'lodash/minBy';
import { CodeTile, StatTile } from './tile';
import {
VegaIcon,
@@ -28,7 +29,6 @@ import sortBy from 'lodash/sortBy';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
import BigNumber from 'bignumber.js';
import maxBy from 'lodash/maxBy';
import { DocsLinks } from '@vegaprotocol/environment';
import { useT, ns } from '../../lib/use-t';
import { Trans } from 'react-i18next';
@@ -127,8 +127,8 @@ export const useStats = ({
t.discountFactor === discountFactorValue
);
const nextBenefitTierValue = currentBenefitTierValue
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1)
: maxBy(benefitTiers, (bt) => bt.tier); // max tier number is lowest tier
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier + 1)
: minBy(benefitTiers, (bt) => bt.tier); // min tier number is lowest tier
const epochsValue =
!isNaN(currentEpoch) && refereeInfo?.atEpoch
? currentEpoch - refereeInfo?.atEpoch
@@ -261,7 +261,7 @@ export const Statistics = ({
const referrerVolumeTile = (
<StatTile
title={t('My volume (last {{count}} epochs)', {
title={t('myVolume', 'My volume (last {{count}} epochs)', {
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
})}
>
@@ -274,7 +274,7 @@ export const Statistics = ({
.reduce((all, r) => all.plus(r), new BigNumber(0));
const totalCommissionTile = (
<StatTile
title={t('Total commission (last {{count}}} epochs)', {
title={t('totalCommission', 'Total commission (last {{count}}} epochs)', {
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
})}
description={<QUSDTooltip />}
@@ -317,9 +317,13 @@ export const Statistics = ({
);
const runningVolumeTile = (
<StatTile
title={t('Combined volume (last {{count}} epochs)', {
count: details?.windowLength,
})}
title={t(
'runningNotionalOverEpochs',
'Combined volume (last {{count}} epochs)',
{
count: details?.windowLength,
}
)}
>
{compactNumFormat.format(runningVolumeValue)}
</StatTile>
@@ -439,15 +443,19 @@ export const RefereesTable = ({
{ name: 'joined', displayName: t('Date Joined') },
{
name: 'volume',
displayName: t('Volume (last {{count}} epochs)', {
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
}),
displayName: t(
'volumeLastEpochs',
'Volume (last {{count}} epochs)',
{
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
}
),
},
{
name: 'commission',
displayName: (
<Trans
i18nKey="referral-statistics-commission"
i18nKey="referralStatisticsCommission"
defaults="Commission earned in <0>qUSD</0> (last {{count}} epochs)"
values={{
count:
+10 -6
View File
@@ -208,9 +208,13 @@ const TiersTable = ({
{ name: 'discount', displayName: t('Referrer trading discount') },
{
name: 'volume',
displayName: t('Min. trading volume (last {{count}} epochs)', {
count: windowLength,
}),
displayName: t(
'minTradingVolume',
'Min. trading volume (last {{count}} epochs)',
{
count: windowLength,
}
),
},
{ name: 'epochs', displayName: t('Min. epochs') },
]}
@@ -218,13 +222,13 @@ const TiersTable = ({
...d,
className: classNames({
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
d.tier === 1,
d.tier >= 3,
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
d.tier === 2,
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
d.tier === 3,
d.tier === 1,
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
d.tier > 3,
d.tier == 0,
}),
}))}
/>
@@ -16,18 +16,11 @@ query DiscountPrograms {
}
}
query Fees(
$partyId: ID!
$volumeDiscountEpochs: Int!
$referralDiscountEpochs: Int!
) {
query Fees($partyId: ID!) {
epoch {
id
}
volumeDiscountStats(
partyId: $partyId
pagination: { last: $volumeDiscountEpochs }
) {
volumeDiscountStats(partyId: $partyId, pagination: { last: 1 }) {
edges {
node {
atEpoch
@@ -59,10 +52,7 @@ query Fees(
}
}
}
referralSetStats(
partyId: $partyId
pagination: { last: $referralDiscountEpochs }
) {
referralSetStats(partyId: $partyId, pagination: { last: 1 }) {
edges {
node {
atEpoch
+3 -10
View File
@@ -10,8 +10,6 @@ export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgr
export type FeesQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
volumeDiscountEpochs: Types.Scalars['Int'];
referralDiscountEpochs: Types.Scalars['Int'];
}>;
@@ -65,14 +63,11 @@ export type DiscountProgramsQueryHookResult = ReturnType<typeof useDiscountProgr
export type DiscountProgramsLazyQueryHookResult = ReturnType<typeof useDiscountProgramsLazyQuery>;
export type DiscountProgramsQueryResult = Apollo.QueryResult<DiscountProgramsQuery, DiscountProgramsQueryVariables>;
export const FeesDocument = gql`
query Fees($partyId: ID!, $volumeDiscountEpochs: Int!, $referralDiscountEpochs: Int!) {
query Fees($partyId: ID!) {
epoch {
id
}
volumeDiscountStats(
partyId: $partyId
pagination: {last: $volumeDiscountEpochs}
) {
volumeDiscountStats(partyId: $partyId, pagination: {last: 1}) {
edges {
node {
atEpoch
@@ -104,7 +99,7 @@ export const FeesDocument = gql`
}
}
}
referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) {
referralSetStats(partyId: $partyId, pagination: {last: 1}) {
edges {
node {
atEpoch
@@ -129,8 +124,6 @@ export const FeesDocument = gql`
* const { data, loading, error } = useFeesQuery({
* variables: {
* partyId: // value for 'partyId'
* volumeDiscountEpochs: // value for 'volumeDiscountEpochs'
* referralDiscountEpochs: // value for 'referralDiscountEpochs'
* },
* });
*/
@@ -36,25 +36,25 @@ export const FeesContainer = () => {
const { data: markets, loading: marketsLoading } = useMarketList();
const { data: programData, loading: programLoading } =
useDiscountProgramsQuery();
useDiscountProgramsQuery({ errorPolicy: 'ignore' });
const volumeDiscountWindowLength =
programData?.currentVolumeDiscountProgram?.windowLength || 1;
const referralDiscountWindowLength =
programData?.currentReferralProgram?.windowLength || 1;
const { data: feesData, loading: feesLoading } = useFeesQuery({
variables: {
partyId: pubKey || '',
volumeDiscountEpochs: volumeDiscountWindowLength,
referralDiscountEpochs: referralDiscountWindowLength,
},
skip: !pubKey || !programData,
skip: !pubKey,
});
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
useVolumeStats(
feesData?.volumeDiscountStats,
previousEpoch,
feesData?.volumeDiscountStats.edges?.[0]?.node,
programData?.currentVolumeDiscountProgram
);
@@ -67,12 +67,12 @@ export const FeesContainer = () => {
code,
isReferrer,
} = useReferralStats(
feesData?.referralSetStats,
feesData?.referralSetReferees,
previousEpoch,
feesData?.referralSetStats.edges?.[0]?.node,
feesData?.referralSetReferees.edges?.[0]?.node,
programData?.currentReferralProgram,
feesData?.epoch,
feesData?.referrer,
feesData?.referee
feesData?.referrer.edges?.[0]?.node,
feesData?.referee.edges?.[0]?.node
);
const loading = paramsLoading || feesLoading || programLoading;
@@ -317,7 +317,7 @@ export const CurrentVolume = ({
<div className="flex flex-col gap-3 pt-4">
<CardStat
value={formatNumberRounded(new BigNumber(windowLengthVolume))}
text={t('Past {{count}} epochs', { count: windowLength })}
text={t('pastEpochs', 'Past {{count}} epochs', { count: windowLength })}
/>
{requiredForNextTier > 0 && (
<CardStat
@@ -344,9 +344,13 @@ const ReferralBenefits = ({
<CardStat
// all sets volume (not just current party)
value={formatNumber(setRunningNotionalTakerVolume)}
text={t('Combined running notional over the {{count}} epochs', {
count: epochs,
})}
text={t(
'runningNotionalOverEpochs',
'Combined running notional over the {{count}} epochs',
{
count: epochs,
}
)}
/>
<CardStat value={epochsInSet} text={t('epochs in referral set')} />
</div>
@@ -453,31 +457,27 @@ const VolumeTiers = ({
<Th>{t('Discount')}</Th>
<Th>{t('Min. trading volume')}</Th>
<Th>
{t('My volume (last {{count}} epochs)', { count: windowLength })}
{t('myVolume', 'My volume (last {{count}} epochs)', {
count: windowLength,
})}
</Th>
<Th />
</tr>
</THead>
<tbody>
{Array.from(tiers)
.reverse()
.map((tier, i) => {
const isUserTier = tiers.length - 1 - tierIndex === i;
{Array.from(tiers).map((tier, i) => {
const isUserTier = tierIndex === i;
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>
{formatPercentage(Number(tier.volumeDiscountFactor))}%
</Td>
<Td>
{formatNumber(tier.minimumRunningNotionalTakerVolume)}
</Td>
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
<Td>{isUserTier ? <YourTier /> : null}</Td>
</Tr>
);
})}
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>{formatPercentage(Number(tier.volumeDiscountFactor))}%</Td>
<Td>{formatNumber(tier.minimumRunningNotionalTakerVolume)}</Td>
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
<Td>{isUserTier ? <YourTier /> : null}</Td>
</Tr>
);
})}
</tbody>
</Table>
</div>
@@ -520,37 +520,33 @@ const ReferralTiers = ({
</tr>
</THead>
<tbody>
{Array.from(tiers)
.reverse()
.map((t, i) => {
const isUserTier = tiers.length - 1 - tierIndex === i;
{Array.from(tiers).map((t, i) => {
const isUserTier = tierIndex === i;
const requiredVolume = Number(
t.minimumRunningNotionalTakerVolume
const requiredVolume = Number(t.minimumRunningNotionalTakerVolume);
let unlocksIn = null;
if (
referralVolumeInWindow >= requiredVolume &&
epochsInSet < t.minimumEpochs
) {
unlocksIn = (
<span className="text-muted">
Unlocks in {t.minimumEpochs - epochsInSet} epochs
</span>
);
let unlocksIn = null;
}
if (
referralVolumeInWindow >= requiredVolume &&
epochsInSet < t.minimumEpochs
) {
unlocksIn = (
<span className="text-muted">
Unlocks in {t.minimumEpochs - epochsInSet} epochs
</span>
);
}
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
<Td>{t.minimumEpochs}</Td>
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
</Tr>
);
})}
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
<Td>{t.minimumEpochs}</Td>
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
</Tr>
);
})}
</tbody>
</Table>
</div>
@@ -2,46 +2,15 @@ import { renderHook } from '@testing-library/react';
import { useReferralStats } from './use-referral-stats';
describe('useReferralStats', () => {
const setStats = {
edges: [
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 9,
discountFactor: '0.2',
referralSetRunningNotionalTakerVolume: '100',
},
},
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: '200',
},
},
],
const stat = {
__typename: 'ReferralSetStats' as const,
atEpoch: 9,
discountFactor: '0.01',
referralSetRunningNotionalTakerVolume: '100',
};
const sets = {
edges: [
{
node: {
atEpoch: 3,
},
},
{
node: {
atEpoch: 4,
},
},
],
};
const epoch = {
id: '10',
const set = {
atEpoch: 4,
};
const program = {
@@ -78,102 +47,36 @@ describe('useReferralStats', () => {
});
});
it('returns formatted data and tiers', () => {
it('returns default values if set is not from previous epoch', () => {
const { result } = renderHook(() =>
useReferralStats(setStats, sets, program, epoch)
useReferralStats(10, stat, set, program)
);
// should use stats from latest epoch
const stats = setStats.edges[1].node;
const set = sets.edges[1].node;
expect(result.current).toEqual({
referralDiscount: Number(stats.discountFactor),
referralVolumeInWindow: Number(
stats.referralSetRunningNotionalTakerVolume
),
referralTierIndex: 1,
referralDiscount: 0,
referralVolumeInWindow: 0,
referralTierIndex: -1,
referralTiers: program.benefitTiers,
epochsInSet: Number(epoch.id) - set.atEpoch,
epochsInSet: 0,
code: undefined,
isReferrer: false,
});
});
it.each([
{ joinedAt: 2, index: -1 },
{ joinedAt: 3, index: -1 },
{ joinedAt: 4, index: 0 },
{ joinedAt: 5, index: 0 },
{ joinedAt: 6, index: 1 },
{ joinedAt: 7, index: 1 },
{ joinedAt: 8, index: 2 },
{ joinedAt: 9, index: 2 },
])('joined at epoch: $joinedAt should be index: $index', (obj) => {
const statsA = {
edges: [
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: '100000',
},
},
],
};
const setsA = {
edges: [
{
node: {
atEpoch: Number(epoch.id) - obj.joinedAt,
},
},
],
};
it('returns formatted data and tiers', () => {
const { result } = renderHook(() =>
useReferralStats(statsA, setsA, program, epoch)
useReferralStats(9, stat, set, program)
);
expect(result.current.referralTierIndex).toEqual(obj.index);
});
it.each([
{ volume: '50', index: -1 },
{ volume: '100', index: 0 },
{ volume: '150', index: 0 },
{ volume: '200', index: 1 },
{ volume: '250', index: 1 },
{ volume: '300', index: 2 },
{ volume: '999', index: 2 },
])('volume: $volume should be index: $index', (obj) => {
const statsA = {
edges: [
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: obj.volume,
},
},
],
};
const setsA = {
edges: [
{
node: {
atEpoch: 1,
},
},
],
};
const { result } = renderHook(() =>
useReferralStats(statsA, setsA, program, epoch)
);
expect(result.current.referralTierIndex).toEqual(obj.index);
expect(result.current).toEqual({
referralDiscount: Number(stat.discountFactor),
referralVolumeInWindow: Number(
stat.referralSetRunningNotionalTakerVolume
),
referralTierIndex: 0,
referralTiers: program.benefitTiers,
epochsInSet: stat.atEpoch - set.atEpoch,
code: undefined,
isReferrer: false,
});
});
});
@@ -1,20 +1,24 @@
import compact from 'lodash/compact';
import maxBy from 'lodash/maxBy';
import { getReferralBenefitTier } from './utils';
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
import { first } from 'lodash';
export const useReferralStats = (
setStats?: FeesQuery['referralSetStats'],
setReferees?: FeesQuery['referralSetReferees'],
previousEpoch?: number,
referralStats?: NonNullable<
FeesQuery['referralSetStats']['edges']['0']
>['node'],
setReferees?: NonNullable<
FeesQuery['referralSetReferees']['edges']['0']
>['node'],
program?: DiscountProgramsQuery['currentReferralProgram'],
epoch?: FeesQuery['epoch'],
setIfReferrer?: FeesQuery['referrer'],
setIfReferee?: FeesQuery['referee']
setIfReferrer?: NonNullable<FeesQuery['referrer']['edges']['0']>['node'],
setIfReferee?: NonNullable<FeesQuery['referee']['edges']['0']>['node']
) => {
const referralTiers = program?.benefitTiers || [];
if (!setStats || !setReferees || !program || !epoch) {
if (
!previousEpoch ||
referralStats?.atEpoch !== previousEpoch ||
!program ||
!setReferees
) {
return {
referralDiscount: 0,
referralVolumeInWindow: 0,
@@ -26,41 +30,22 @@ export const useReferralStats = (
};
}
const setIfReferrerData = first(
compact(setIfReferrer?.edges).map((e) => e.node)
);
const setIfRefereeData = first(
compact(setIfReferee?.edges).map((e) => e.node)
);
const referralSetsStats = compact(setStats.edges).map((e) => e.node);
const referralSets = compact(setReferees.edges).map((e) => e.node);
const referralSet = maxBy(referralSets, (s) => s.atEpoch);
const referralStats = maxBy(referralSetsStats, (s) => s.atEpoch);
const epochsInSet = referralSet ? Number(epoch.id) - referralSet.atEpoch : 0;
const referralDiscount = Number(referralStats?.discountFactor || 0);
const referralVolumeInWindow = Number(
referralStats?.referralSetRunningNotionalTakerVolume || 0
);
const referralTierIndex = referralStats
? getReferralBenefitTier(
epochsInSet,
Number(referralStats.referralSetRunningNotionalTakerVolume),
referralTiers
)
: -1;
const referralTierIndex = referralTiers.findIndex(
(tier) => tier.referralDiscountFactor === referralStats?.discountFactor
);
return {
referralDiscount,
referralVolumeInWindow,
referralTierIndex,
referralTiers,
epochsInSet,
code: (setIfReferrerData || setIfRefereeData)?.id,
isReferrer: Boolean(setIfReferrerData),
epochsInSet: referralStats.atEpoch - setReferees.atEpoch,
code: (setIfReferrer || setIfReferee)?.id,
isReferrer: Boolean(setIfReferrer),
};
};
@@ -2,27 +2,11 @@ import { renderHook } from '@testing-library/react';
import { useVolumeStats } from './use-volume-stats';
describe('useReferralStats', () => {
const statsList = {
edges: [
{
__typename: 'VolumeDiscountStatsEdge' as const,
node: {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 9,
discountFactor: '0.1',
runningVolume: '100',
},
},
{
__typename: 'VolumeDiscountStatsEdge' as const,
node: {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 10,
discountFactor: '0.3',
runningVolume: '200',
},
},
],
const stats = {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 10,
discountFactor: '0.05',
runningVolume: '200',
};
const program = {
@@ -44,7 +28,7 @@ describe('useReferralStats', () => {
};
it('returns correct default values', () => {
const { result } = renderHook(() => useVolumeStats());
const { result } = renderHook(() => useVolumeStats(10));
expect(result.current).toEqual({
volumeDiscount: 0,
volumeInWindow: 0,
@@ -53,11 +37,18 @@ describe('useReferralStats', () => {
});
});
it('returns formatted data and tiers', () => {
const { result } = renderHook(() => useVolumeStats(statsList, program));
it('returns default values if no stat is not from previous epoch', () => {
const { result } = renderHook(() => useVolumeStats(11, stats, program));
expect(result.current).toEqual({
volumeDiscount: 0,
volumeInWindow: 0,
volumeTierIndex: -1,
volumeTiers: program.benefitTiers,
});
});
// should use stats from latest epoch
const stats = statsList.edges[1].node;
it('returns formatted data and tiers', () => {
const { result } = renderHook(() => useVolumeStats(10, stats, program));
expect(result.current).toEqual({
volumeDiscount: Number(stats.discountFactor),
@@ -66,30 +57,4 @@ describe('useReferralStats', () => {
volumeTiers: program.benefitTiers,
});
});
it.each([
{ volume: '100', index: 0 },
{ volume: '150', index: 0 },
{ volume: '200', index: 1 },
{ volume: '250', index: 1 },
{ volume: '300', index: 2 },
{ volume: '350', index: 2 },
])('returns index: $index for the running volume: $volume', (obj) => {
const statsA = {
edges: [
{
__typename: 'VolumeDiscountStatsEdge' as const,
node: {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 10,
discountFactor: '0.3',
runningVolume: obj.volume,
},
},
],
};
const { result } = renderHook(() => useVolumeStats(statsA, program));
expect(result.current.volumeTierIndex).toBe(obj.index);
});
});
@@ -1,15 +1,15 @@
import compact from 'lodash/compact';
import maxBy from 'lodash/maxBy';
import { getVolumeTier } from './utils';
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
export const useVolumeStats = (
stats?: FeesQuery['volumeDiscountStats'],
previousEpoch: number,
lastEpochStats?: NonNullable<
FeesQuery['volumeDiscountStats']['edges']['0']
>['node'],
program?: DiscountProgramsQuery['currentVolumeDiscountProgram']
) => {
const volumeTiers = program?.benefitTiers || [];
if (!stats || !program) {
if (!lastEpochStats || lastEpochStats.atEpoch !== previousEpoch || !program) {
return {
volumeDiscount: 0,
volumeTierIndex: -1,
@@ -18,11 +18,11 @@ export const useVolumeStats = (
};
}
const volumeStats = compact(stats.edges).map((e) => e.node);
const lastEpochStats = maxBy(volumeStats, (s) => s.atEpoch);
const volumeDiscount = Number(lastEpochStats?.discountFactor || 0);
const volumeInWindow = Number(lastEpochStats?.runningVolume || 0);
const volumeTierIndex = getVolumeTier(volumeInWindow, volumeTiers);
const volumeTierIndex = volumeTiers.findIndex(
(tier) => tier.volumeDiscountFactor === lastEpochStats?.discountFactor
);
return {
volumeDiscount,
@@ -20,73 +20,6 @@ export const formatPercentage = (num: number) => {
return formatter.format(parseFloat(pct.toFixed(5)));
};
/**
* Return the index of the benefit tier for volume discounts. A user
* only needs to fulfill a minimum volume requirement for the tier
*/
export const getVolumeTier = (
volume: number,
tiers: Array<{
minimumRunningNotionalTakerVolume: string;
}>
) => {
return tiers.findIndex((tier, i) => {
const nextTier = tiers[i + 1];
const validVolume =
volume >= Number(tier.minimumRunningNotionalTakerVolume);
if (nextTier) {
return (
validVolume &&
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
);
}
return validVolume;
});
};
/**
* Return the index of the benefit tiers for referrals. A user must
* fulfill both the minimum epochs in the referral set, and the set
* must reach the combined total volume
*/
export const getReferralBenefitTier = (
epochsInSet: number,
volume: number,
tiers: Array<{
minimumRunningNotionalTakerVolume: string;
minimumEpochs: number;
}>
) => {
const indexByEpoch = tiers.findIndex((tier, i) => {
const nextTier = tiers[i + 1];
const validEpochs = epochsInSet >= tier.minimumEpochs;
if (nextTier) {
return validEpochs && epochsInSet < nextTier.minimumEpochs;
}
return validEpochs;
});
const indexByVolume = tiers.findIndex((tier, i) => {
const nextTier = tiers[i + 1];
const validVolume =
volume >= Number(tier.minimumRunningNotionalTakerVolume);
if (nextTier) {
return (
validVolume &&
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
);
}
return validVolume;
});
return Math.min(indexByEpoch, indexByVolume);
};
/**
* Given a set of fees and a set of discounts return
* the adjusted fee factor
@@ -16,15 +16,12 @@ export const LedgerContainer = () => {
});
const assets = (data?.party?.accountsConnection?.edges ?? [])
.map<PartyAssetFieldsFragment>(
(item) => item?.node?.asset ?? ({} as PartyAssetFieldsFragment)
)
.reduce((aggr, item) => {
if ('id' in item && 'symbol' in item) {
aggr[item.id as string] = item.symbol as string;
}
return aggr;
}, {} as Record<string, string>);
.map((item) => item?.node?.asset)
.filter((asset): asset is PartyAssetFieldsFragment => !!asset?.id)
.reduce(
(aggr, item) => Object.assign(aggr, { [item.id]: item.symbol }),
{} as Record<string, string>
);
if (!pubKey) {
return (
@@ -172,4 +172,20 @@ describe('Navbar', () => {
expect(mockDisconnect).toHaveBeenCalled();
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
});
it('does not render the language selector until we have more languages', () => {
renderComponent();
expect(screen.queryByTestId('icon-globe')).not.toBeInTheDocument();
});
it('renders the theme switcher', async () => {
renderComponent();
await userEvent.click(screen.getByTestId('icon-moon'));
expect(screen.queryByTestId('icon-moon')).not.toBeInTheDocument();
expect(screen.getByTestId('icon-sun')).toBeInTheDocument();
await userEvent.click(screen.getByTestId('icon-sun'));
expect(screen.queryByTestId('icon-sun')).not.toBeInTheDocument();
expect(screen.getByTestId('icon-moon')).toBeInTheDocument();
});
});
+19 -2
View File
@@ -11,7 +11,13 @@ import {
} from '@vegaprotocol/environment';
import { useGlobalStore } from '../../stores';
import { VegaWalletConnectButton } from '../vega-wallet-connect-button';
import { VegaIconNames, VegaIcon, VLogo } from '@vegaprotocol/ui-toolkit';
import {
VegaIconNames,
VegaIcon,
VLogo,
LanguageSelector,
ThemeSwitcher,
} from '@vegaprotocol/ui-toolkit';
import * as N from '@radix-ui/react-navigation-menu';
import * as D from '@radix-ui/react-dialog';
import { NavLink } from 'react-router-dom';
@@ -22,7 +28,8 @@ import { VegaWalletMenu } from '../vega-wallet';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { WalletIcon } from '../icons/wallet';
import { ProtocolUpgradeCountdown } from '@vegaprotocol/proposals';
import { useT } from '../../lib/use-t';
import { useT, useI18n } from '../../lib/use-t';
import { supportedLngs } from '../../lib/i18n';
type MenuState = 'wallet' | 'nav' | null;
type Theme = 'system' | 'yellow';
@@ -34,6 +41,7 @@ export const Navbar = ({
children?: ReactNode;
theme?: Theme;
}) => {
const i18n = useI18n();
const t = useT();
// menu state for small screens
const [menu, setMenu] = useState<MenuState>(null);
@@ -77,6 +85,15 @@ export const Navbar = ({
{/* Right section */}
<div className="ml-auto flex items-center justify-end gap-2">
<ProtocolUpgradeCountdown />
<div className="flex">
<ThemeSwitcher />
{supportedLngs.length > 1 ? (
<LanguageSelector
languages={supportedLngs}
onSelect={(language) => i18n.changeLanguage(language)}
/>
) : null}
</div>
<NavbarMobileButton
onClick={() => {
if (isConnected) {
+1 -1
View File
@@ -1,2 +1,2 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
VEGA_VERSION=v0.73.5
VEGA_VERSION=v0.73.6
+1 -1
View File
@@ -1,2 +1,2 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:develop
VEGA_VERSION=v0.73.5
VEGA_VERSION=v0.73.6
+1 -1
View File
@@ -1,2 +1,2 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:main
VEGA_VERSION=v0.73.5
VEGA_VERSION=v0.73.6
@@ -1,4 +1,5 @@
import pytest
import re
import vega_sim.api.governance as governance
from playwright.sync_api import Page, expect
from vega_sim.service import VegaService, PeggedOrder
@@ -10,6 +11,8 @@ from wallet_config import MM_WALLET, MM_WALLET2, GOVERNANCE_WALLET
@pytest.mark.usefixtures("vega", "page", "proposed_market", "risk_accepted")
def test_market_lifecycle(proposed_market, vega: VegaService, page: Page):
# 7002-SORD-001
# 7002-SORD-002
trading_mode = page.get_by_test_id("market-trading-mode").get_by_test_id(
"item-value"
)
@@ -18,12 +21,32 @@ def test_market_lifecycle(proposed_market, vega: VegaService, page: Page):
# setup market in proposed step, without liquidity provided
market_id = proposed_market
page.goto(f"/#/markets/{market_id}")
# 6002-MDET-001
expect(page.get_by_test_id("header-title")).to_have_text("BTC:DAI_2023Futr")
# 6002-MDET-002
expect(page.get_by_test_id("market-expiry")).to_have_text("ExpiryNot time-based")
page.get_by_test_id("market-expiry").hover()
expect(page.get_by_test_id("expiry-tooltip").first).to_have_text("This market expires when triggered by its oracle, not on a set date.View oracle specification")
expect(page.get_by_test_id("expiry-tooltip").first.get_by_test_id("link")).to_have_attribute("href", re.compile('.*'))
# 6002-MDET-003
expect(page.get_by_test_id("market-price")).to_have_text("Mark Price0.00")
# 6002-MDET-004
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
# 6002-MDET-005
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
# 6002-MDET-008
expect(page.get_by_test_id("market-settlement-asset")).to_have_text("Settlement assettDAI")
expect(page.get_by_test_id("liquidity-supplied")).to_have_text("Liquidity supplied 0.00 (0.00%)")
page.get_by_test_id("liquidity-supplied").hover()
expect(page.get_by_test_id("liquidity-supplied-tooltip").first).to_have_text("Supplied stake0.00Target stake0.00View liquidity provision tableLearn about providing liquidity")
expect(page.get_by_test_id("liquidity-supplied-tooltip").first.get_by_test_id("link").first).to_have_text("View liquidity provision table")
# check that market is in proposed state
# 6002-MDET-006
# 6002-MDET-007
# 7002-SORD-061
expect(trading_mode).to_have_text("No trading")
trading_mode.hover()
expect(page.get_by_test_id("trading-mode-tooltip").first).to_have_text("No trading enabled for this market.")
expect(market_state).to_have_text("Proposed")
# approve market
@@ -0,0 +1,116 @@
import pytest
import re
import json
from playwright.sync_api import Page, expect, Route
from vega_sim.service import VegaService
from conftest import init_vega
from fixtures.market import setup_continuous_market
order_size = "order-size"
order_price = "order-price"
place_order = "place-order"
order_side_sell = "order-side-SIDE_SELL"
market_order = "order-type-Market"
tif = "order-tif"
expire = "expire"
api_request_match = r"http://localhost:\d+/api/v2/requests"
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega:
yield vega
@pytest.fixture(scope="module")
def continuous_market(vega):
return setup_continuous_market(vega)
def handle_route_connection_lost(route: Route, request):
if request.method == "POST" and re.match(api_request_match, request.url):
route.fulfill(
status=200,
headers={"Content-Type": "application/json"},
body='{"jsonrpc": "2.0", "id": "1"}'
)
else:
route.continue_()
def handle_route_connection_rejected(route: Route, request):
if request.method == "POST" and re.match(api_request_match, request.url):
custom_response = {
"jsonrpc": "2.0",
"error": {
"code": 3001,
"data": "the user rejected the wallet connection",
"message": "User error"
},
"id": "0"
}
route.fulfill(
status=400,
headers={"Content-Type": "application/json"},
body=json.dumps(custom_response)
)
else:
route.continue_()
def assert_connection_approve(route: Route, request, page:Page):
if request.method == "POST" and re.match(api_request_match, request.url):
expect(page.get_by_test_id("toast-content")).to_have_text("Please go to your Vega wallet application and approve or reject the transaction.")
else:
route.continue_()
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_wallet_connection_error(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.route("**/*", handle_route_connection_lost)
page.get_by_test_id("connect-vega-wallet").click()
page.get_by_test_id("connector-jsonRpc").click()
expect(page.get_by_test_id("wallet-dialog-title")).to_have_text("Something went wrong")
@pytest.mark.usefixtures("page", "risk_accepted")
def test_wallet_connection_rejected(continuous_market, page: Page):
# 0002-WCON-002
# 0002-WCON-005
# 0002-WCON-007
# 0002-WCON-015
page.goto(f"/#/markets/{continuous_market}")
page.route("**/*", handle_route_connection_rejected)
page.get_by_test_id("connect-vega-wallet").click()
page.get_by_test_id("connector-jsonRpc").click()
expect(page.get_by_test_id("dialog-content").nth(1)).to_have_text("User errorthe user rejected the wallet connectionTry againAbout the Vega wallet | Supported browsers ")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_wallet_connection_error_transaction(continuous_market, vega: VegaService, page: Page):
# 0003-WTXN-009
# 0003-WTXN-011
# 0002-WCON-016
# 0003-WTXN-008
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(order_size).fill("10")
page.get_by_test_id(order_price).fill("120")
page.route("**/*", handle_route_connection_lost)
page.get_by_test_id(place_order).click()
expect(page.get_by_test_id("toast-content")).to_have_text("Wallet disconnectedThe connection to your Vega Wallet has been lost.Connect vega wallet")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_wallet_transaction_rejected(continuous_market, vega: VegaService, page: Page):
# 0003-WTXN-007
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(order_size).fill("10")
page.get_by_test_id(order_price).fill("120")
page.route("**/*", handle_route_connection_rejected)
page.get_by_test_id(place_order).click()
expect(page.get_by_test_id("toast-content")).to_have_text("Error occurredthe user rejected the wallet connection")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_wallet_connection_approve(continuous_market, vega: VegaService, page: Page):
# 0002-WCON-005
# 0002-WCON-007
# 0002-WCON-009
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(order_size).fill("10")
page.get_by_test_id(order_price).fill("120")
page.route("**/*", assert_connection_approve)
page.get_by_test_id(place_order).click()
@@ -0,0 +1,60 @@
import * as router from 'react-router';
import { useNavigateToLastMarket } from './use-navigate-to-last-market';
import { useGlobalStore } from '../../stores';
import { renderHook } from '@testing-library/react';
import { useTopTradedMarkets } from './use-top-traded-markets';
import { Links } from '../links';
const mockLastMarketId = 'LAST';
jest.mock('../../stores', () => {
const original = jest.requireActual('../../stores');
return {
...original,
useGlobalStore: jest.fn(),
};
});
jest.mock('./use-top-traded-markets', () => {
return {
useTopTradedMarkets: jest.fn(),
};
});
describe('useNavigateToLastMarket', () => {
const navigate = jest.fn();
beforeAll(() => {
jest.spyOn(router, 'useNavigate').mockImplementation(() => navigate);
});
it('navigates to the last market when it is active', () => {
(useGlobalStore as unknown as jest.Mock).mockReturnValue(mockLastMarketId);
(useTopTradedMarkets as jest.Mock).mockReturnValue({
data: [{ id: mockLastMarketId }],
});
renderHook(() => useNavigateToLastMarket());
expect(navigate).toHaveBeenCalledWith(Links.MARKET(mockLastMarketId), {
replace: true,
});
});
it('navigates to the top traded market if the last one is not active', () => {
(useGlobalStore as unknown as jest.Mock).mockReturnValue(mockLastMarketId);
(useTopTradedMarkets as jest.Mock).mockReturnValue({
data: [{ id: 'TOP' }],
});
renderHook(() => useNavigateToLastMarket());
expect(navigate).toHaveBeenCalledWith(Links.MARKET('TOP'), {
replace: true,
});
});
it('navigates to the list of markets when all of the markets are not active', () => {
(useGlobalStore as unknown as jest.Mock).mockReturnValue(mockLastMarketId);
(useTopTradedMarkets as jest.Mock).mockReturnValue({
data: [],
});
renderHook(() => useNavigateToLastMarket());
expect(navigate).toHaveBeenCalledWith(Links.MARKETS());
});
});
@@ -0,0 +1,40 @@
import { useGlobalStore } from '../../stores';
import { useNavigate } from 'react-router-dom';
import { useTopTradedMarkets } from './use-top-traded-markets';
import { useEffect } from 'react';
import { Links } from '../links';
export const useNavigateToLastMarket = () => {
const navigate = useNavigate();
// this returns a list of active markets ordered by traded factor
// hence there's no need to pull markets again or find out in separate
// query of the state of last market
const { data } = useTopTradedMarkets();
const lastMarketId = useGlobalStore((store) => store.marketId);
const isLastMarketActive = data?.some((m) => m.id === lastMarketId);
useEffect(() => {
if (!data) return;
// if last market id is set and it is active, navigate to that market
if (lastMarketId && isLastMarketActive) {
navigate(Links.MARKET(lastMarketId), {
replace: true,
});
return;
}
// otherwise if there's a top traded market, navigate to that market
const marketDataId = data[0]?.id;
if (marketDataId) {
navigate(Links.MARKET(marketDataId), {
replace: true,
});
return;
}
// otherwise navigate to the list of all markets
navigate(Links.MARKETS());
}, [lastMarketId, data, navigate, isLastMarketActive]);
};
+3 -2
View File
@@ -6,6 +6,8 @@ import type { HttpBackendOptions, RequestCallback } from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
export const supportedLngs = ['en'];
const isInDev = process.env.NODE_ENV === 'development';
const useLocize = isInDev && !!process.env.NX_USE_LOCIZE;
@@ -51,9 +53,8 @@ i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
lng: 'en',
fallbackLng: 'en',
supportedLngs: ['en'],
supportedLngs,
load: 'languageOnly',
// have a common namespace used around the full app
ns: [
+1
View File
@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
export const ns = 'trading';
export const useT = () => useTranslation('trading').t;
export const useI18n = () => useTranslation('trading').i18n;
-1
View File
@@ -32,7 +32,6 @@ import { SSRLoader } from './ssr-loader';
import { PartyActiveOrdersHandler } from './party-active-orders-handler';
import { MaybeConnectEagerly } from './maybe-connect-eagerly';
import { TransactionHandlers } from './transaction-handlers';
import '../lib/i18n';
import { useT } from '../lib/use-t';
const Title = () => {
+3 -3
View File
@@ -170,7 +170,7 @@ html [data-theme='dark'] {
.ag-theme-balham,
.ag-theme-balham-dark {
--ag-grid-size: 2px; /* Used for compactness */
--ag-grid-size: 3px; /* Used for compactness */
--ag-row-height: 36px;
--ag-header-height: 28px;
}
@@ -184,7 +184,7 @@ html [data-theme='dark'] {
/* Light variables */
.ag-theme-balham {
--ag-background-color: transparent;
--ag-background-color: theme(colors.vega.clight.900);
--ag-border-color: theme(colors.vega.clight.600);
--ag-header-background-color: theme(colors.vega.clight.700);
--ag-odd-row-background-color: transparent;
@@ -196,7 +196,7 @@ html [data-theme='dark'] {
/* Dark variables */
.ag-theme-balham-dark {
--ag-background-color: transparent;
--ag-background-color: theme(colors.vega.cdark.900);
--ag-border-color: theme(colors.vega.cdark.600);
--ag-header-background-color: theme(colors.vega.cdark.700);
--ag-odd-row-background-color: transparent;
@@ -13,10 +13,10 @@ import { type IterableElement } from 'type-fest';
import {
AccountEventsDocument,
AccountsDocument,
AccountFieldsFragment,
AccountsQuery,
AccountEventsSubscription,
AccountsQueryVariables,
type AccountFieldsFragment,
type AccountsQuery,
type AccountEventsSubscription,
type AccountsQueryVariables,
} from './__generated__/Accounts';
import { type Asset } from '@vegaprotocol/assets';
+2 -1
View File
@@ -23,7 +23,7 @@ export const ALLOWED_ACCOUNTS = [
export const TransferContainer = ({ assetId }: { assetId?: string }) => {
const t = useT();
const { pubKey, pubKeys } = useVegaWallet();
const { pubKey, pubKeys, isReadOnly } = useVegaWallet();
const { params } = useNetworkParams([
NetworkParams.transfer_fee_factor,
NetworkParams.transfer_minTransferQuantumMultiple,
@@ -70,6 +70,7 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
<TransferForm
pubKey={pubKey}
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
isReadOnly={isReadOnly}
assetId={assetId}
feeFactor={params.transfer_fee_factor}
minQuantumMultiple={params.transfer_minTransferQuantumMultiple}
@@ -73,6 +73,28 @@ describe('TransferForm', () => {
minQuantumMultiple: '1',
};
const propsNoAssets = {
pubKey,
pubKeys: [
pubKey,
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
],
feeFactor: '0.001',
submitTransfer: jest.fn(),
accounts: [],
minQuantumMultiple: '1',
};
it('renders no assets', async () => {
renderComponent(propsNoAssets);
expect(screen.getByTestId('no-assets-available')).toBeVisible();
});
it('renders no accounts', async () => {
renderComponent(propsNoAssets);
expect(screen.getByTestId('no-accounts-available')).toBeVisible();
});
it.each([
{
targetText: 'Include transfer fee',
+82 -62
View File
@@ -45,6 +45,7 @@ interface Asset {
export interface TransferFormProps {
pubKey: string | null;
pubKeys: string[] | null;
isReadOnly?: boolean;
accounts: Array<{
type: AccountType;
balance: string;
@@ -59,6 +60,7 @@ export interface TransferFormProps {
export const TransferForm = ({
pubKey,
pubKeys,
isReadOnly,
assetId: initialAssetId,
feeFactor,
submitTransfer,
@@ -201,27 +203,36 @@ export const TransferForm = ({
<Controller
control={control}
name="asset"
render={({ field }) => (
<TradingRichSelect
data-testid="select-asset"
id={field.name}
name={field.name}
onValueChange={(value) => {
field.onChange(value);
setValue('fromAccount', '');
}}
placeholder={t('Please select an asset')}
value={field.value}
>
{assets.map((a) => (
<AssetOption
key={a.key}
asset={a}
balance={<Balance balance={a.balance} symbol={a.symbol} />}
/>
))}
</TradingRichSelect>
)}
render={({ field }) =>
assets.length > 0 ? (
<TradingRichSelect
data-testid="select-asset"
id={field.name}
name={field.name}
onValueChange={(value) => {
field.onChange(value);
setValue('fromAccount', '');
}}
placeholder={t('Please select an asset')}
value={field.value}
>
{assets.map((a) => (
<AssetOption
key={a.key}
asset={a}
balance={<Balance balance={a.balance} symbol={a.symbol} />}
/>
))}
</TradingRichSelect>
) : (
<span
data-testid="no-assets-available"
className="text-xs text-vega-clight-100 dark:text-vega-cdark-100"
>
{t('No assets available')}
</span>
)
}
/>
{errors.asset?.message && (
<TradingInputError forInput="asset">
@@ -249,48 +260,57 @@ export const TransferForm = ({
},
},
}}
render={({ field }) => (
<TradingSelect
id="fromAccount"
defaultValue=""
{...field}
onChange={(e) => {
field.onChange(e);
render={({ field }) =>
accounts.length > 0 ? (
<TradingSelect
id="fromAccount"
defaultValue=""
{...field}
onChange={(e) => {
field.onChange(e);
const [type] = parseFromAccount(e.target.value);
const [type] = parseFromAccount(e.target.value);
// Enforce that if transferring from a vested rewards account it must go to
// the current connected general account
if (
type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS &&
pubKey
) {
setValue('toVegaKey', pubKey);
setToVegaKeyMode('select');
setIncludeFee(false);
}
}}
>
<option value="" disabled={true}>
{t('Please select')}
</option>
{accounts
.filter((a) => {
if (!selectedAssetId) return true;
return selectedAssetId === a.asset.id;
})
.map((a) => {
const id = `${a.type}-${a.asset.id}`;
return (
<option value={id} key={id}>
{AccountTypeMapping[a.type]} (
{addDecimal(a.balance, a.asset.decimals)} {a.asset.symbol}
)
</option>
);
})}
</TradingSelect>
)}
// Enforce that if transferring from a vested rewards account it must go to
// the current connected general account
if (
type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS &&
pubKey
) {
setValue('toVegaKey', pubKey);
setToVegaKeyMode('select');
setIncludeFee(false);
}
}}
>
<option value="" disabled={true}>
{t('Please select')}
</option>
{accounts
.filter((a) => {
if (!selectedAssetId) return true;
return selectedAssetId === a.asset.id;
})
.map((a) => {
const id = `${a.type}-${a.asset.id}`;
return (
<option value={id} key={id}>
{AccountTypeMapping[a.type]} (
{addDecimal(a.balance, a.asset.decimals)}{' '}
{a.asset.symbol})
</option>
);
})}
</TradingSelect>
) : (
<span
data-testid="no-accounts-available"
className="text-xs text-vega-clight-100 dark:text-vega-cdark-100"
>
{t('No accounts available')}
</span>
)
}
/>
{errors.fromAccount?.message && (
<TradingInputError forInput="fromAccount">
@@ -454,7 +474,7 @@ export const TransferForm = ({
decimals={asset?.decimals}
/>
)}
<TradingButton type="submit" fill={true}>
<TradingButton type="submit" fill={true} disabled={isReadOnly}>
{t('Confirm transfer')}
</TradingButton>
</form>
@@ -25,7 +25,7 @@ import {
import { ApolloError } from '@apollo/client';
import type { GraphQLErrors } from '@apollo/client/errors';
import { GraphQLError } from 'graphql';
import { Subscription, Observable } from 'zen-observable-ts';
import { type Subscription, type Observable } from 'zen-observable-ts';
import { waitFor } from '@testing-library/react';
type Item = {
@@ -153,7 +153,8 @@ interface DataProviderParams<
pagination?: {
getPageInfo: GetPageInfo<QueryData>;
append: Append<Data>;
first: number;
first?: number;
last?: number;
};
fetchPolicy?: FetchPolicy;
resetDelay?: number;
-15
View File
@@ -1,15 +0,0 @@
export const useTranslation = () => ({
t: (label: string, replacements?: Record<string, string>) => {
const replace =
replacements?.['replace'] && typeof replacements === 'object'
? replacements?.['replace']
: replacements;
let translatedLabel = replacements?.['defaultValue'] || label;
if (typeof replace === 'object' && replace !== null) {
Object.keys(replace).forEach((key) => {
translatedLabel = translatedLabel.replace(`{{${key}}}`, replace[key]);
});
}
return translatedLabel;
},
});
@@ -36,10 +36,10 @@ export const AgGridThemed = ({
<div className={wrapperClasses}>
<AgGridReact
defaultColDef={defaultColDef}
ref={gridRef}
overlayLoadingTemplate={t('Loading...')}
overlayNoRowsTemplate={t('No data')}
suppressDragLeaveHidesColumns
ref={gridRef}
{...defaultProps}
{...props}
/>
+1 -1
View File
@@ -25,7 +25,7 @@ describe('Pagination', () => {
const mockOnLoad = jest.fn();
const count = 10;
render(<Pagination {...props} count={count} onLoad={mockOnLoad} />);
expect(screen.getByText(`${count} rows loaded`)).toBeInTheDocument();
expect(screen.getByText('10 rows loaded')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
expect(mockOnLoad).toHaveBeenCalled();
});
+5 -9
View File
@@ -18,19 +18,15 @@ export const Pagination = ({
let rowMessage = '';
if (count && !pageInfo?.hasNextPage) {
rowMessage = t('paginationAllLoaded', {
replace: { count },
defaultValue: 'All {{count}} rows loaded',
rowMessage = t('paginationAllLoaded', 'all {{count}} rows loaded', {
count,
});
} else {
rowMessage = t('paginationLoaded', {
replace: { count },
defaultValue: '{{count}} rows loaded',
});
rowMessage = t('paginationLoaded', '{{count}} rows loaded', { count });
}
return (
<div className="flex items-center justify-between p-1 border-t border-default">
<div className="border-default flex items-center justify-between border-t p-1">
<div className="text-xs">
{false}
{showRetentionMessage &&
@@ -47,7 +43,7 @@ export const Pagination = ({
) : null}
</div>
{count && hasDisplayedRows === false ? (
<div className="absolute text-xs top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform text-xs">
{t('No rows matching selected filters')}
</div>
) : null}
+14
View File
@@ -1,4 +1,18 @@
import '@testing-library/jest-dom';
import ResizeObserver from 'resize-observer-polyfill';
import { locales } from '@vegaprotocol/i18n';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
// Set up i18n instance so that components have the correct default
// en translations
i18n.use(initReactI18next).init({
// we init with resources
resources: locales,
fallbackLng: 'en',
ns: ['datagrid'],
defaultNS: 'datagrid',
});
global.ResizeObserver = ResizeObserver;
@@ -77,12 +77,10 @@ export const DealTicketFeeDetails = ({
label={
<>
{t('Fees')}
{totalDiscountFactor ? (
{totalDiscountFactor !== '0' ? (
<Pill size="xxs" intent={Intent.Info} className="ml-1">
-
{formatNumberPercentage(
new BigNumber(totalDiscountFactor).multipliedBy(100),
2
new BigNumber(totalDiscountFactor).multipliedBy(100)
)}
</Pill>
) : null}
@@ -105,10 +103,7 @@ export const DealTicketFeeDetails = ({
)}
</p>
<FeesBreakdown
totalFeeAmount={feeEstimate?.totalFeeAmount}
referralDiscountFactor={feeEstimate?.referralDiscountFactor}
volumeDiscountFactor={feeEstimate?.volumeDiscountFactor}
fees={feeEstimate?.fees}
feeEstimate={feeEstimate}
feeFactors={market.fees.factors}
symbol={assetSymbol}
decimals={assetDecimals}
@@ -59,9 +59,7 @@ export const TimeInForceSelector = ({
components={[
<Tooltip
description={
<SimpleGrid
grid={compileGridData(t, market, marketData, t)}
/>
<SimpleGrid grid={compileGridData(t, market, marketData)} />
}
>
sufficient liquidity
@@ -83,9 +81,7 @@ export const TimeInForceSelector = ({
components={[
<Tooltip
description={
<SimpleGrid
grid={compileGridData(t, market, marketData, t)}
/>
<SimpleGrid grid={compileGridData(t, market, marketData)} />
}
>
high price volatility
@@ -6,16 +6,19 @@ describe('getDiscountedFee', () => {
discountedFee: '100',
volumeDiscount: '0',
referralDiscount: '0',
totalDiscount: '0',
});
expect(getDiscountedFee('100', undefined, '0.1')).toEqual({
discountedFee: '90',
volumeDiscount: '10',
referralDiscount: '0',
totalDiscount: '10',
});
expect(getDiscountedFee('100', '0.1', undefined)).toEqual({
discountedFee: '90',
volumeDiscount: '0',
referralDiscount: '10',
totalDiscount: '10',
});
});
@@ -24,6 +27,7 @@ describe('getDiscountedFee', () => {
discountedFee: '',
volumeDiscount: '0',
referralDiscount: '0',
totalDiscount: '0',
});
});
});
@@ -35,7 +39,7 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0',
referralDiscountFactor: '0',
})
).toEqual(0);
).toEqual('0');
});
it('returns volumeDiscountFactor if referralDiscountFactor is 0', () => {
@@ -44,7 +48,7 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0.1',
referralDiscountFactor: '0',
})
).toEqual(0.1);
).toEqual('-0.1');
});
it('returns referralDiscountFactor if volumeDiscountFactor is 0', () => {
expect(
@@ -52,7 +56,7 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0',
referralDiscountFactor: '0.1',
})
).toEqual(0.1);
).toEqual('-0.1');
});
it('calculates discount using referralDiscountFactor and volumeDiscountFactor', () => {
@@ -61,6 +65,6 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0.2',
referralDiscountFactor: '0.1',
})
).toBeCloseTo(0.28);
).toBe('-0.28');
});
});
+27 -12
View File
@@ -15,6 +15,7 @@ export const getDiscountedFee = (
discountedFee: feeAmount,
volumeDiscount: '0',
referralDiscount: '0',
totalDiscount: '0',
};
}
const referralDiscount = new BigNumber(referralDiscountFactor || '0')
@@ -23,12 +24,14 @@ export const getDiscountedFee = (
const volumeDiscount = new BigNumber(volumeDiscountFactor || '0')
.multipliedBy((BigInt(feeAmount) - BigInt(referralDiscount)).toString())
.toFixed(0, BigNumber.ROUND_FLOOR);
const totalDiscount = (
BigInt(referralDiscount) + BigInt(volumeDiscount)
).toString();
const discountedFee = (
BigInt(feeAmount || '0') -
BigInt(referralDiscount) -
BigInt(volumeDiscount)
BigInt(feeAmount || '0') - BigInt(totalDiscount)
).toString();
return {
totalDiscount,
referralDiscount,
volumeDiscount,
discountedFee,
@@ -39,16 +42,28 @@ export const getTotalDiscountFactor = (feeEstimate?: {
volumeDiscountFactor?: string;
referralDiscountFactor?: string;
}) => {
if (!feeEstimate) {
return 0;
if (
!feeEstimate ||
(feeEstimate.referralDiscountFactor === '0' &&
feeEstimate.volumeDiscountFactor === '0')
) {
return '0';
}
const volumeFactor = Number(feeEstimate?.volumeDiscountFactor) || 0;
const referralFactor = Number(feeEstimate?.referralDiscountFactor) || 0;
if (!volumeFactor) {
return referralFactor;
const volumeFactor = new BigNumber(
feeEstimate?.volumeDiscountFactor || 0
).minus(1);
const referralFactor = new BigNumber(
feeEstimate?.referralDiscountFactor || 0
).minus(1);
if (volumeFactor.isZero()) {
return feeEstimate.referralDiscountFactor
? `-${feeEstimate.referralDiscountFactor}`
: '0';
}
if (!referralFactor) {
return volumeFactor;
if (referralFactor.isZero()) {
return feeEstimate.volumeDiscountFactor
? `-${feeEstimate.volumeDiscountFactor}`
: '0';
}
return 1 - (1 - volumeFactor) * (1 - referralFactor);
return volumeFactor.multipliedBy(referralFactor).minus(1).toString();
};
@@ -14,13 +14,16 @@ describe('FeesBreakdown', () => {
liquidityFee: '100',
};
const props = {
totalFeeAmount: '100',
fees,
feeFactors,
symbol: 'USD',
decimals: 2,
referralDiscountFactor: '0.01',
volumeDiscountFactor: '0.01',
feeEstimate: {
referralDiscountFactor: '0.01',
volumeDiscountFactor: '0.01',
totalFeeAmount: '100',
fees,
},
};
render(<FeesBreakdown {...props} />);
expect(screen.getByText('Maker fee').nextElementSibling).toHaveTextContent(
@@ -1,12 +1,13 @@
import { sumFeesFactors } from '@vegaprotocol/markets';
import type { TradeFee, FeeFactors } from '@vegaprotocol/types';
import type { FeeFactors } from '@vegaprotocol/types';
import {
addDecimalsFormatNumber,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { getDiscountedFee } from '../discounts';
import { getDiscountedFee, getTotalDiscountFactor } from '../discounts';
import { useT } from '../../use-t';
import { type useEstimateFees } from '../../hooks/use-estimate-fees';
const formatValue = (
value: string | number | null | undefined,
@@ -24,18 +25,16 @@ const FeesBreakdownItem = ({
decimals,
}: {
label: string;
factor?: string;
factor?: string | number;
value: string;
symbol?: string;
decimals: number;
}) => (
<>
<dt className="col-span-2">{label}</dt>
{factor && (
<dd className="text-right col-span-1">
{formatNumberPercentage(new BigNumber(factor).times(100))}
</dd>
)}
<dd className="text-right col-span-1">
{factor ? formatNumberPercentage(new BigNumber(factor).times(100)) : ''}
</dd>
<dd className="text-right col-span-3">
{formatValue(value, decimals)} {symbol || ''}
</dd>
@@ -43,59 +42,35 @@ const FeesBreakdownItem = ({
);
export const FeesBreakdown = ({
totalFeeAmount,
fees,
feeEstimate,
feeFactors,
symbol,
decimals,
referralDiscountFactor,
volumeDiscountFactor,
}: {
totalFeeAmount?: string;
fees?: TradeFee;
feeEstimate: ReturnType<typeof useEstimateFees>;
feeFactors?: FeeFactors;
symbol?: string;
decimals: number;
referralDiscountFactor?: string;
volumeDiscountFactor?: string;
}) => {
const t = useT();
const { fees, totalFeeAmount, referralDiscountFactor, volumeDiscountFactor } =
feeEstimate || {};
if (!fees || !totalFeeAmount || totalFeeAmount === '0') return null;
const { discountedFee: discountedInfrastructureFee } = getDiscountedFee(
fees.infrastructureFee,
referralDiscountFactor,
volumeDiscountFactor
);
const { discountedFee: discountedLiquidityFee } = getDiscountedFee(
fees.liquidityFee,
referralDiscountFactor,
volumeDiscountFactor
);
const { discountedFee: discountedMakerFee } = getDiscountedFee(
fees.makerFee,
referralDiscountFactor,
volumeDiscountFactor
);
const {
discountedFee: discountedTotalFeeAmount,
volumeDiscount,
referralDiscount,
} = getDiscountedFee(
totalFeeAmount,
referralDiscountFactor,
volumeDiscountFactor
);
const totalDiscountFactor = getTotalDiscountFactor(feeEstimate);
const { discountedFee: discountedTotalFeeAmount, totalDiscount } =
getDiscountedFee(
totalFeeAmount,
referralDiscountFactor,
volumeDiscountFactor
);
return (
<dl className="grid grid-cols-6">
<FeesBreakdownItem
label={t('Infrastructure fee')}
factor={feeFactors?.infrastructureFee}
value={discountedInfrastructureFee}
value={fees.infrastructureFee}
symbol={symbol}
decimals={decimals}
/>
@@ -103,7 +78,7 @@ export const FeesBreakdown = ({
<FeesBreakdownItem
label={t('Liquidity fee')}
factor={feeFactors?.liquidityFee}
value={discountedLiquidityFee}
value={fees.liquidityFee}
symbol={symbol}
decimals={decimals}
/>
@@ -111,35 +86,50 @@ export const FeesBreakdown = ({
<FeesBreakdownItem
label={t('Maker fee')}
factor={feeFactors?.makerFee}
value={discountedMakerFee}
value={fees.makerFee}
symbol={symbol}
decimals={decimals}
/>
{volumeDiscountFactor && volumeDiscount !== '0' && (
<FeesBreakdownItem
label={t('Volume discount')}
factor={volumeDiscountFactor}
value={volumeDiscount}
symbol={symbol}
decimals={decimals}
/>
{totalDiscount && totalDiscount !== '0' ? (
<>
<FeesBreakdownItem
label={t('Subtotal')}
value={totalFeeAmount}
factor={
feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined
}
symbol={symbol}
decimals={decimals}
/>
<div className="col-span-6 mt-2"></div>
<FeesBreakdownItem
label={t('Discount')}
factor={totalDiscountFactor}
value={`-${totalDiscount}`}
symbol={symbol}
decimals={decimals}
/>
<FeesBreakdownItem
label={t('Total')}
value={discountedTotalFeeAmount}
symbol={symbol}
decimals={decimals}
/>
</>
) : (
<>
<div className="col-span-6 mt-2"></div>
<FeesBreakdownItem
label={t('Total')}
factor={
feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined
}
value={totalFeeAmount}
symbol={symbol}
decimals={decimals}
/>
</>
)}
{referralDiscountFactor && referralDiscount !== '0' && (
<FeesBreakdownItem
label={t('Referral discount')}
factor={referralDiscountFactor}
value={referralDiscount}
symbol={symbol}
decimals={decimals}
/>
)}
<FeesBreakdownItem
label={t('Total fees')}
factor={feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined}
value={discountedTotalFeeAmount}
symbol={symbol}
decimals={decimals}
/>
</dl>
);
};
@@ -32,15 +32,19 @@ export const mapFormValuesToOrderSubmission = (
? toNanoSeconds(order.expiresAt)
: undefined,
postOnly:
order.type === Schema.OrderType.TYPE_MARKET ? false : order.postOnly,
reduceOnly:
order.type === Schema.OrderType.TYPE_LIMIT &&
![
order.type === Schema.OrderType.TYPE_MARKET ||
[
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(order.timeInForce)
? false
: order.reduceOnly,
: order.postOnly,
reduceOnly: ![
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(order.timeInForce)
? false
: order.reduceOnly,
icebergOpts:
order.type === Schema.OrderType.TYPE_LIMIT &&
isPersistentOrder(order.timeInForce) &&
@@ -98,4 +98,91 @@ describe('mapFormValuesToOrderSubmission', () => {
).size
).toEqual('1000');
});
it.each([
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK, postOnly: false },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GFA, postOnly: true },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GFN, postOnly: true },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC, postOnly: true },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT, postOnly: true },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_IOC, postOnly: false },
])(
'sets postOnly correctly when TIF is $timeInForce',
({
timeInForce,
postOnly,
}: {
timeInForce: OrderTimeInForce;
postOnly: boolean;
}) => {
expect(
mapFormValuesToOrderSubmission(
{
type: OrderType.TYPE_LIMIT,
timeInForce,
postOnly: true,
} as OrderFormValues,
'marketId',
2,
2
).postOnly
).toEqual(postOnly);
// sets always false if type is market
expect(
mapFormValuesToOrderSubmission(
{
type: OrderType.TYPE_MARKET,
timeInForce,
postOnly: true,
} as OrderFormValues,
'marketId',
2,
2
).postOnly
).toEqual(false);
}
);
it.each([
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK, reduceOnly: true },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GFA, reduceOnly: false },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GFN, reduceOnly: false },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC, reduceOnly: false },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT, reduceOnly: false },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_IOC, reduceOnly: true },
])(
'sets reduceOnly correctly when TIF is $timeInForce',
({
timeInForce,
reduceOnly,
}: {
timeInForce: OrderTimeInForce;
reduceOnly: boolean;
}) => {
expect(
mapFormValuesToOrderSubmission(
{
type: OrderType.TYPE_MARKET,
timeInForce,
reduceOnly: true,
} as OrderFormValues,
'marketId',
2,
2
).reduceOnly
).toEqual(reduceOnly);
expect(
mapFormValuesToOrderSubmission(
{
type: OrderType.TYPE_LIMIT,
timeInForce,
reduceOnly: true,
} as OrderFormValues,
'marketId',
2,
2
).reduceOnly
).toEqual(reduceOnly);
}
);
});
@@ -216,12 +216,10 @@ const ApprovalTxFeedback = ({
<p>
{t(
'You approved deposits of up to {{assetSymbol}} {{approvedAllowanceValue}}.',
[
{
assetSymbol: selectedAsset?.symbol,
approvedAllowanceValue,
},
]
{
assetSymbol: selectedAsset?.symbol,
approvedAllowanceValue,
}
)}
</p>
{txLink && <p>{txLink}</p>}
@@ -1,15 +0,0 @@
export const useTranslation = () => ({
t: (label: string, replacements?: Record<string, string>) => {
const replace =
replacements?.replace && typeof replacements === 'object'
? replacements?.replace
: replacements;
let translatedLabel = replacements?.defaultValue || label;
if (typeof replace === 'object' && replace !== null) {
Object.keys(replace).forEach((key) => {
translatedLabel = translatedLabel.replace(`{{${key}}}`, replace[key]);
});
}
return translatedLabel;
},
});
@@ -5,7 +5,10 @@ import {
type NodeCheckTimeUpdateSubscription,
} from '../../utils/__generated__/NodeCheck';
import { Networks } from '../../types';
import { createMockClient, RequestHandlerResponse } from 'mock-apollo-client';
import {
createMockClient,
type RequestHandlerResponse,
} from 'mock-apollo-client';
export type MockRequestConfig = {
hasError?: boolean;
@@ -150,7 +150,7 @@ export const useEnvironment = create<EnvStore>()((set, get) => ({
* Initialize Vega app to dynamically select a node from the
* VEGA_CONFIG_URL
*
* This can be ommitted if you intend to only use a single node,
* This can be omitted if you intend to only use a single node,
* in those cases be sure to set NX_VEGA_URL
*/
export const useInitializeEnv = () => {
@@ -415,6 +415,12 @@ function compileFeatureFlags(): FeatureFlags {
REFERRALS: TRUTHY.includes(
windowOrDefault('NX_REFERRALS', process.env['NX_REFERRALS']) as string
),
DISABLE_CLOSE_POSITION: TRUTHY.includes(
windowOrDefault(
'NX_DISABLE_CLOSE_POSITION',
process.env['NX_DISABLE_CLOSE_POSITION']
) as string
),
UPDATE_MARKET_STATE: TRUTHY.includes(
windowOrDefault(
'NX_UPDATE_MARKET_STATE',
@@ -70,10 +70,7 @@ export const useNodeHealth = () => {
);
intent = Intent.Danger;
} else if (blockDiff >= BLOCK_THRESHOLD) {
text = t('blocksBehind', {
defaultValue: '{{count}} Blocks behind',
replace: { count: blockDiff },
});
text = t('blocksBehind', '{{count}} Blocks behind', { count: blockDiff });
intent = Intent.Warning;
} else if (blockUpdateMsLatency > WARNING_LATENCY) {
text = t(
+14
View File
@@ -5,6 +5,20 @@
import '@testing-library/jest-dom';
import ResizeObserver from 'resize-observer-polyfill';
import { locales } from '@vegaprotocol/i18n';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
// Set up i18n instance so that components have the correct default
// en translations
i18n.use(initReactI18next).init({
// we init with resources
resources: locales,
fallbackLng: 'en',
ns: ['environment'],
defaultNS: 'environment',
});
global.ResizeObserver = ResizeObserver;
// Required by radix-ui/react-dropdown-menu
+1
View File
@@ -27,6 +27,7 @@ export type CosmicElevatorFlags = Pick<
| 'UPDATE_MARKET_STATE'
| 'GOVERNANCE_TRANSFERS'
| 'VOLUME_DISCOUNTS'
| 'DISABLE_CLOSE_POSITION'
>;
export type Configuration = z.infer<typeof tomlConfigSchema>;
export const CUSTOM_NODE_KEY = 'custom' as const;
@@ -81,6 +81,7 @@ const COSMIC_ELEVATOR_FLAGS = {
UPDATE_MARKET_STATE: z.optional(z.boolean()),
GOVERNANCE_TRANSFERS: z.optional(z.boolean()),
VOLUME_DISCOUNTS: z.optional(z.boolean()),
DISABLE_CLOSE_POSITION: z.optional(z.boolean()),
};
const EXPLORER_FLAGS = {
+45 -98
View File
@@ -4,14 +4,8 @@ import { getDateTimeFormat } from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
import type { Trade } from './fills-data-provider';
import {
FeesDiscountBreakdownTooltip,
FillsTable,
getFeesBreakdown,
getTotalFeesDiscounts,
} from './fills-table';
import { FeesDiscountBreakdownTooltip, FillsTable } from './fills-table';
import { generateFill } from './test-helpers';
import type { TradeFeeFieldsFragment } from './__generated__/Fills';
const partyId = 'party-id';
const defaultFill: PartialDeep<Trade> = {
@@ -35,6 +29,7 @@ const defaultFill: PartialDeep<Trade> = {
},
createdAt: new Date('2022-02-02T14:00:00').toISOString(),
};
describe('FillsTable', () => {
it('correct columns are rendered', async () => {
// 7005-FILL-001
@@ -65,7 +60,7 @@ describe('FillsTable', () => {
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
});
it('formats cells correctly for buyer fill', async () => {
it('formats cells correctly for buyer fill for maker', async () => {
const buyerFill = generateFill({
...defaultFill,
buyer: {
@@ -89,7 +84,7 @@ describe('FillsTable', () => {
'3.00 BTC',
'Maker',
'2.00 BTC',
'0.27 BTC',
'0.09 BTC',
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
'', // action column
];
@@ -271,96 +266,48 @@ describe('FillsTable', () => {
.find((c) => c.getAttribute('col-id') === 'size');
expect(sizeCell).toHaveTextContent('3,000,000,000');
});
});
describe('FeesDiscountBreakdownTooltip', () => {
it('shows all discounts', () => {
const data = generateFill({
...defaultFill,
buyer: {
id: partyId,
},
});
const props = {
data,
partyId,
value: data.market,
} as Parameters<typeof FeesDiscountBreakdownTooltip>['0'];
const { container } = render(<FeesDiscountBreakdownTooltip {...props} />);
const dt = container.querySelectorAll('dt');
const dd = container.querySelectorAll('dd');
const expectedDt = [
'Infrastructure Fee',
'Referral Discount',
'Volume Discount',
'Liquidity Fee',
'Referral Discount',
'Volume Discount',
'Maker Fee',
'Referral Discount',
'Volume Discount',
];
const expectedDD = [
'0.05 BTC',
'0.06 BTC',
'0.01 BTC',
'0.02 BTC',
'0.03 BTC',
'0.04 BTC',
];
expectedDt.forEach((label, i) => {
expect(dt[i]).toHaveTextContent(label);
});
expectedDD.forEach((label, i) => {
expect(dd[i]).toHaveTextContent(label);
describe('FeesDiscountBreakdownTooltip', () => {
it('shows all discounts', () => {
const data = generateFill({
...defaultFill,
buyer: {
id: partyId,
},
});
const props = {
data,
partyId,
value: data.market,
} as Parameters<typeof FeesDiscountBreakdownTooltip>['0'];
const { container } = render(<FeesDiscountBreakdownTooltip {...props} />);
const dt = container.querySelectorAll('dt');
const dd = container.querySelectorAll('dd');
const expectedDt = [
'Infrastructure Fee',
'Referral Discount',
'Volume Discount',
'Liquidity Fee',
'Referral Discount',
'Volume Discount',
'Maker Fee',
'Referral Discount',
'Volume Discount',
];
const expectedDD = [
'0.05 BTC',
'0.06 BTC',
'0.01 BTC',
'0.02 BTC',
'0.03 BTC',
'0.04 BTC',
];
expectedDt.forEach((label, i) => {
expect(dt[i]).toHaveTextContent(label);
});
expectedDD.forEach((label, i) => {
expect(dd[i]).toHaveTextContent(label);
});
});
});
});
describe('getFeesBreakdown', () => {
it('should return correct fees breakdown for a taker', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '1000',
totalFee: '6000',
};
expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a maker', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '-1000',
totalFee: '4000',
};
expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown);
});
});
describe('getTotalFeesDiscounts', () => {
it('should return correct total value', () => {
const fees = {
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
};
expect(getTotalFeesDiscounts(fees as TradeFeeFieldsFragment)).toEqual(
(1 + 2 + 3 + 4 + 5 + 6).toString()
);
});
});
+34 -109
View File
@@ -28,20 +28,12 @@ import {
import { forwardRef } from 'react';
import BigNumber from 'bignumber.js';
import { type Trade } from './fills-data-provider';
import {
type FillFieldsFragment,
type TradeFeeFieldsFragment,
} from './__generated__/Fills';
import { FillActionsDropdown } from './fill-actions-dropdown';
import { getAsset } from '@vegaprotocol/markets';
import { useT } from './use-t';
import { MAKER, TAKER, getFeesBreakdown, getRoleAndFees } from './fills-utils';
const TAKER = 'Taker';
const MAKER = 'Maker';
export type Role = typeof TAKER | typeof MAKER | '-';
export type Props = (AgGridReactProps | AgReactUiProps) & {
type Props = (AgGridReactProps | AgReactUiProps) & {
partyId: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
};
@@ -262,63 +254,13 @@ const formatFeeDiscount = (partyId: string) => {
}: VegaValueFormatterParams<Trade, 'market'>) => {
if (!market || !data) return '-';
const asset = getAsset(market);
const { fees } = getRoleAndFees({ data, partyId });
if (!fees) return '-';
const total = getTotalFeesDiscounts(fees);
return addDecimalsFormatNumber(total, asset.decimals);
const { fees: roleFees, role } = getRoleAndFees({ data, partyId });
if (!roleFees) return '-';
const { totalFeeDiscount } = getFeesBreakdown(role, roleFees);
return addDecimalsFormatNumber(totalFeeDiscount, asset.decimals);
};
};
export const isEmptyFeeObj = (feeObj: Schema.TradeFee) => {
if (!feeObj) return true;
return (
feeObj.liquidityFee === '0' &&
feeObj.makerFee === '0' &&
feeObj.infrastructureFee === '0'
);
};
export const getRoleAndFees = ({
data,
partyId,
}: {
data: Pick<
FillFieldsFragment,
'buyerFee' | 'sellerFee' | 'buyer' | 'seller' | 'aggressor'
>;
partyId?: string;
}) => {
let role: Role;
let fees;
if (data?.buyer.id === partyId) {
if (data.aggressor === Schema.Side.SIDE_BUY) {
role = TAKER;
fees = data?.buyerFee;
} else if (data.aggressor === Schema.Side.SIDE_SELL) {
role = MAKER;
fees = data?.sellerFee;
} else {
role = '-';
fees = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee;
}
} else if (data?.seller.id === partyId) {
if (data.aggressor === Schema.Side.SIDE_SELL) {
role = TAKER;
fees = data?.sellerFee;
} else if (data.aggressor === Schema.Side.SIDE_BUY) {
role = MAKER;
fees = data?.buyerFee;
} else {
role = '-';
fees = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee;
}
} else {
return { role: '-', fees: undefined };
}
return { role, fees };
};
const FeesBreakdownTooltip = ({
data,
value: market,
@@ -331,16 +273,23 @@ const FeesBreakdownTooltip = ({
const asset = getAsset(market);
const { role, fees } = getRoleAndFees({ data, partyId }) ?? {};
const { role, fees, marketState } = getRoleAndFees({ data, partyId }) ?? {};
if (!fees) return null;
const { infrastructureFee, liquidityFee, makerFee, totalFee } =
getFeesBreakdown(role, fees);
getFeesBreakdown(role, fees, marketState);
return (
<div
data-testid="fee-breakdown-tooltip"
className="bg-vega-light-100 dark:bg-vega-dark-100 border-vega-light-200 dark:border-vega-dark-200 break-word z-20 max-w-sm rounded border px-4 py-2 text-sm text-black dark:text-white"
className="bg-vega-light-100 dark:bg-vega-dark-100 border-vega-light-200 dark:border-vega-dark-200 break-word z-20 max-w-sm rounded border px-4 py-2 text-xs text-black dark:text-white"
>
{marketState && (
<p className="mb-1 italic">
{t('If the market was {{state}}', {
state: Schema.MarketStateMapping[marketState].toLowerCase(),
})}
</p>
)}
{role === MAKER && (
<>
<p className="mb-1">{t('The maker will receive the maker fee.')}</p>
@@ -354,7 +303,7 @@ const FeesBreakdownTooltip = ({
{role === TAKER && (
<p className="mb-1">{t('Fees to be paid by the taker.')}</p>
)}
{role === '-' && (
{(role === '-' || marketState === Schema.MarketState.STATE_SUSPENDED) && (
<p className="mb-1">
{t(
'If the market is in monitoring auction, half of the infrastructure and liquidity fees will be paid.'
@@ -395,8 +344,8 @@ const FeesDiscountBreakdownTooltipItem = ({
}) =>
value && value !== '0' ? (
<>
<dt className="col-span-1">{label}</dt>
<dd className="col-span-1 text-right">
<dt className="col-span-2">{label}</dt>
<dd className="col-span-2 text-right">
{addDecimalsFormatNumber(value, asset.decimals)} {asset.symbol}
</dd>
</>
@@ -412,15 +361,19 @@ export const FeesDiscountBreakdownTooltip = ({
}
const asset = getAsset(data.market);
const { fees } = getRoleAndFees({ data, partyId }) ?? {};
if (!fees) return null;
const {
fees: roleFees,
marketState,
role,
} = getRoleAndFees({ data, partyId }) ?? {};
if (!roleFees) return null;
const fees = getFeesBreakdown(role, roleFees, marketState);
return (
<div
data-testid="fee-discount-breakdown-tooltip"
className="bg-vega-light-100 dark:bg-vega-dark-100 border-vega-light-200 dark:border-vega-dark-200 break-word z-20 max-w-sm rounded border px-4 py-2 text-sm text-black dark:text-white"
>
<dl className="grid grid-cols-2 gap-x-1">
<dl className="grid grid-cols-6 gap-x-1 text-xs">
{(fees.infrastructureFeeReferralDiscount || '0') !== '0' ||
(fees.infrastructureFeeVolumeDiscount || '0') !== '0' ? (
<dt className="col-span-2">{t('Infrastructure Fee')}</dt>
@@ -464,42 +417,14 @@ export const FeesDiscountBreakdownTooltip = ({
label={t('Volume Discount')}
asset={asset}
/>
<dt className="col-span-2">{t('Total Fee Discount')}</dt>
<FeesDiscountBreakdownTooltipItem
value={fees.totalFeeDiscount}
label={''}
asset={asset}
/>
</dl>
</div>
);
};
export const getTotalFeesDiscounts = (fees: TradeFeeFieldsFragment) => {
return (
BigInt(fees.infrastructureFeeReferralDiscount || '0') +
BigInt(fees.infrastructureFeeVolumeDiscount || '0') +
BigInt(fees.liquidityFeeReferralDiscount || '0') +
BigInt(fees.liquidityFeeVolumeDiscount || '0') +
BigInt(fees.makerFeeReferralDiscount || '0') +
BigInt(fees.makerFeeVolumeDiscount || '0')
).toString();
};
export const getFeesBreakdown = (
role: Role,
feesObj: TradeFeeFieldsFragment
) => {
const makerFee =
role === MAKER
? new BigNumber(feesObj.makerFee).times(-1).toString()
: feesObj.makerFee;
const infrastructureFee = feesObj.infrastructureFee;
const liquidityFee = feesObj.liquidityFee;
const totalFee = new BigNumber(infrastructureFee)
.plus(makerFee)
.plus(liquidityFee)
.toString();
return {
infrastructureFee,
liquidityFee,
makerFee,
totalFee,
};
};
+183
View File
@@ -0,0 +1,183 @@
import { getFeesBreakdown } from './fills-utils';
import * as Schema from '@vegaprotocol/types';
describe('getFeesBreakdown', () => {
it('should return correct fees breakdown for a taker', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '1000',
totalFee: '6000',
totalFeeDiscount: '0',
};
expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a maker if market is active', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '0',
liquidityFee: '0',
makerFee: '-1000',
totalFee: '-1000',
totalFeeDiscount: '0',
};
expect(
getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_ACTIVE)
).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a maker if the market is suspended', () => {
const fees = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '0',
};
const expectedBreakdown = {
infrastructureFee: '1000',
liquidityFee: '1500',
makerFee: '0',
totalFee: '2500',
totalFeeDiscount: '0',
};
expect(
getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_SUSPENDED)
).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a taker if the market is suspended', () => {
const fees = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '0',
};
const expectedBreakdown = {
infrastructureFee: '1000',
liquidityFee: '1500',
makerFee: '0',
totalFee: '2500',
totalFeeDiscount: '0',
};
expect(
getFeesBreakdown('Taker', fees, Schema.MarketState.STATE_SUSPENDED)
).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a taker if market is active', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '1000',
totalFee: '6000',
totalFeeDiscount: '0',
};
expect(
getFeesBreakdown('Taker', fees, Schema.MarketState.STATE_ACTIVE)
).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a maker', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '0',
liquidityFee: '0',
makerFee: '-1000',
totalFee: '-1000',
totalFeeDiscount: '0',
};
expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown);
});
it('should return correct total fees discount value for a taker (if the market is active - default)', () => {
const fees = {
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
infrastructureFee: '1000',
liquidityFee: '2000',
makerFee: '3000',
};
const { totalFeeDiscount } = getFeesBreakdown('Taker', fees);
expect(totalFeeDiscount).toEqual((1 + 2 + 3 + 4 + 5 + 6).toString());
});
it('should return correct total fees discount value for a maker (if the market is active - default)', () => {
const fees = {
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
infrastructureFee: '1000',
liquidityFee: '2000',
makerFee: '3000',
};
const { totalFeeDiscount } = getFeesBreakdown('Maker', fees);
// makerFeeReferralDiscount and makerFeeVolumeDiscount are added, infra and liq. fees are zeroed
expect(totalFeeDiscount).toEqual((5 + 6).toString());
});
it('should return correct total fees discount value for a maker (if the market is suspended)', () => {
const fees = {
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
infrastructureFee: '1000',
liquidityFee: '2000',
makerFee: '3000',
};
const { totalFeeDiscount } = getFeesBreakdown(
'Maker',
fees,
Schema.MarketState.STATE_SUSPENDED
);
// makerFeeReferralDiscount and makerFeeVolumeDiscount are zeroed, infra and liq. fees are halved
expect(totalFeeDiscount).toEqual(((1 + 2 + 3 + 4) / 2).toString());
});
it('should return correct total fees discount value for a taker (if the market is suspended)', () => {
const fees = {
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
infrastructureFee: '1000',
liquidityFee: '2000',
makerFee: '3000',
};
const { totalFeeDiscount } = getFeesBreakdown(
'Taker',
fees,
Schema.MarketState.STATE_SUSPENDED
);
// makerFeeReferralDiscount and makerFeeVolumeDiscount are zeroed, infra and liq. fees are halved
expect(totalFeeDiscount).toEqual(((1 + 2 + 3 + 4) / 2).toString());
});
});
+164
View File
@@ -0,0 +1,164 @@
import BigNumber from 'bignumber.js';
import type {
FillFieldsFragment,
TradeFeeFieldsFragment,
} from './__generated__/Fills';
import * as Schema from '@vegaprotocol/types';
export const TAKER = 'Taker';
export const MAKER = 'Maker';
export type Role = typeof TAKER | typeof MAKER | '-';
export const getRoleAndFees = ({
data,
partyId,
}: {
data: Pick<
FillFieldsFragment,
'buyerFee' | 'sellerFee' | 'buyer' | 'seller' | 'aggressor'
>;
partyId?: string;
}): {
role: Role;
fees?: TradeFeeFieldsFragment;
marketState?: Schema.MarketState;
} => {
let role: Role;
let fees;
if (data?.buyer.id === partyId) {
if (data.aggressor === Schema.Side.SIDE_BUY) {
role = TAKER;
fees = data?.buyerFee;
} else if (data.aggressor === Schema.Side.SIDE_SELL) {
role = MAKER;
fees = data?.sellerFee;
} else {
role = '-';
fees = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee;
}
} else if (data?.seller.id === partyId) {
if (data.aggressor === Schema.Side.SIDE_SELL) {
role = TAKER;
fees = data?.sellerFee;
} else if (data.aggressor === Schema.Side.SIDE_BUY) {
role = MAKER;
fees = data?.buyerFee;
} else {
role = '-';
fees = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee;
}
} else {
return { role: '-', fees: undefined };
}
// We make the assumption that the market state is active if the maker fee is zero on both sides
// This needs to be updated when we have a way to get the correct market state when that fill happened from the API
// because the maker fee factor can be set to 0 via governance
const marketState =
data?.buyerFee.makerFee === data.sellerFee.makerFee &&
new BigNumber(data?.buyerFee.makerFee).isZero()
? Schema.MarketState.STATE_SUSPENDED
: Schema.MarketState.STATE_ACTIVE;
return { role, fees, marketState };
};
export const getFeesBreakdown = (
role: Role,
fees: TradeFeeFieldsFragment,
marketState: Schema.MarketState = Schema.MarketState.STATE_ACTIVE
) => {
// If market is in auction we assume maker fee is zero
const isMarketActive = marketState === Schema.MarketState.STATE_ACTIVE;
// If role is taker, then these are the fees to be paid
let { makerFee, infrastructureFee, liquidityFee } = fees;
// If role is taker, then these are the fees discounts to be applied
let {
makerFeeVolumeDiscount,
makerFeeReferralDiscount,
infrastructureFeeVolumeDiscount,
infrastructureFeeReferralDiscount,
liquidityFeeVolumeDiscount,
liquidityFeeReferralDiscount,
} = fees;
if (isMarketActive) {
if (role === MAKER) {
makerFee = new BigNumber(fees.makerFee).times(-1).toString();
infrastructureFee = '0';
liquidityFee = '0';
// discounts are also zero or we can leave them undefined
infrastructureFeeReferralDiscount =
infrastructureFeeReferralDiscount && '0';
infrastructureFeeVolumeDiscount = infrastructureFeeVolumeDiscount && '0';
liquidityFeeReferralDiscount = liquidityFeeReferralDiscount && '0';
liquidityFeeVolumeDiscount = liquidityFeeVolumeDiscount && '0';
// we leave maker discount fees as they are defined
}
} else {
// If market is suspended (in monitoring auction), then half of the fees are paid
infrastructureFee = new BigNumber(infrastructureFee)
.dividedBy(2)
.toString();
liquidityFee = new BigNumber(liquidityFee).dividedBy(2).toString();
// maker fee is already zero
makerFee = '0';
// discounts are also halved
infrastructureFeeReferralDiscount =
infrastructureFeeReferralDiscount &&
new BigNumber(infrastructureFeeReferralDiscount).dividedBy(2).toString();
infrastructureFeeVolumeDiscount =
infrastructureFeeVolumeDiscount &&
new BigNumber(infrastructureFeeVolumeDiscount).dividedBy(2).toString();
liquidityFeeReferralDiscount =
liquidityFeeReferralDiscount &&
new BigNumber(liquidityFeeReferralDiscount).dividedBy(2).toString();
liquidityFeeVolumeDiscount =
liquidityFeeVolumeDiscount &&
new BigNumber(liquidityFeeVolumeDiscount).dividedBy(2).toString();
// maker discount fees should already be zero
makerFeeReferralDiscount = makerFeeReferralDiscount && '0';
makerFeeVolumeDiscount = makerFeeVolumeDiscount && '0';
}
const totalFee = new BigNumber(infrastructureFee)
.plus(makerFee)
.plus(liquidityFee)
.toString();
const totalFeeDiscount = new BigNumber(makerFeeVolumeDiscount || '0')
.plus(makerFeeReferralDiscount || '0')
.plus(infrastructureFeeReferralDiscount || '0')
.plus(infrastructureFeeVolumeDiscount || '0')
.plus(liquidityFeeReferralDiscount || '0')
.plus(liquidityFeeVolumeDiscount || '0')
.toString();
return {
infrastructureFee,
infrastructureFeeReferralDiscount,
infrastructureFeeVolumeDiscount,
liquidityFee,
liquidityFeeReferralDiscount,
liquidityFeeVolumeDiscount,
makerFee,
makerFeeReferralDiscount,
makerFeeVolumeDiscount,
totalFee,
totalFeeDiscount,
};
};
export const isEmptyFeeObj = (feeObj: Schema.TradeFee) => {
if (!feeObj) return true;
return (
feeObj.liquidityFee === '0' &&
feeObj.makerFee === '0' &&
feeObj.infrastructureFee === '0'
);
};
+3 -1
View File
@@ -21,6 +21,7 @@
"DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT": "To cover the required margin, this amount will be drawn from your general ({{assetSymbol}}) account.",
"Deposit {{assetSymbol}}": "Deposit {{assetSymbol}}",
"Devnet": "Devnet",
"Discount": "Discount",
"EST_TOTAL_MARGIN_TOOLTIP_TEXT": "Estimated total margin that will cover open positions, active orders and this order.",
"Est. uncrossing price": "Est. uncrossing price",
"Est. uncrossing vol": "Est. uncrossing vol",
@@ -78,6 +79,7 @@
"Size": "Size",
"Size cannot be lower than {{sizeStep}}": "Size cannot be lower than {{sizeStep}}",
"sizeAtPrice-market": "market",
"Subtotal": "Subtotal",
"Stagnet": "Stagnet",
"Stop": "Stop",
"Stop Limit": "Stop Limit",
@@ -104,7 +106,7 @@
"Time in force": "Time in force",
"TIME_IN_FORCE_SELECTOR_LIQUIDITY_MONITORING_AUCTION": "This market is in auction until it reaches <0>sufficient liquidity</0>. Until the auction ends, you can only place GFA, GTT, or GTC limit orders.",
"TIME_IN_FORCE_SELECTOR_PRICE_MONITORING_AUCTION": "This market is in auction due to <0>high price volatility</0>. Until the auction ends, you can only place GFA, GTT, or GTC limit orders.",
"Total fees": "Total fees",
"Total": "Total",
"Total margin available": "Total margin available",
"TOTAL_MARGIN_AVAILABLE": "Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).",
"Trading terminated": "Trading terminated",
@@ -2,12 +2,14 @@
"A release candidate for the staging environment": "A release candidate for the staging environment",
"Advanced": "Advanced",
"Block": "Block",
"blocksBehind": "{{count}} Blocks behind",
"blocksBehind_one": "{{count}} Block behind",
"blocksBehind_other": "{{count}} Blocks behind",
"Change node": "Change node",
"Check": "Check",
"Checking": "Checking",
"Connect to this node": "Connect to this node",
"Connected node": "Connected node",
"current": "current",
"Custom": "Custom",
"Devnet": "Devnet",
@@ -33,6 +35,7 @@
"The mainnet-mirror network": "The mainnet-mirror network",
"The validator deployed testnet": "The validator deployed testnet",
"The vega mainnet": "The vega mainnet",
"This app will only work on {{VEGA_ENV}}. Select a node to connect to.": "This app will only work on {{VEGA_ENV}}. Select a node to connect to.",
"VALIDATOR_TESTNET": "VALIDATOR_TESTNET",
"View on Etherscan (opens in a new tab)": "View on Etherscan (opens in a new tab)",
"Warning delay ( >{{warningLatency}} sec): {{blockUpdateLatency}} sec": "Warning delay ( >{{warningLatency}} sec): {{blockUpdateLatency}} sec",
+5
View File
@@ -1,6 +1,9 @@
{
"Date from": "Date from",
"Date from cannot be greater than date to": "Date from cannot be greater than date to",
"Date from cannot be in the future": "Date from cannot be in the future",
"Date to": "Date to",
"Date to cannot be in the future": "Date to cannot be in the future",
"Download": "Download",
"Download all to .csv file": "Download all to .csv file",
"Download has been started": "Download has been started",
@@ -13,6 +16,8 @@
"Still in progress": "Still in progress",
"The downloaded file uses the UTC time zone for all listed times. Your time zone is UTC{{offset}}.": "The downloaded file uses the UTC time zone for all listed times. Your time zone is UTC{{offset}}.",
"Try again later": "Try again later",
"You need to provide a date from": "You need to provide a date from",
"You need to select an asset": "You need to select an asset",
"You will be notified here when your file is ready.": "You will be notified here when your file is ready.",
"Your file is ready": "Your file is ready"
}
+5
View File
@@ -32,6 +32,7 @@
"Insurance pool": "Insurance pool",
"Internal conditions": "Internal conditions",
"Invalid data source": "Invalid data source",
"involvedInMarkets": "Involved in {{count}} markets",
"involvedInMarkets_other": "Involved in {{count}} markets",
"involvedInMarkets_one": "Involved in {{count}} market",
"Key": "Key",
@@ -53,6 +54,7 @@
"Maximum fraction of an LP's accrued fees that an LP would lose to liquidity providers that achieved a higher SLA performance than them. This is a market parameter.": "Maximum fraction of an LP's accrued fees that an LP would lose to liquidity providers that achieved a higher SLA performance than them. This is a market parameter.",
"Maximum value that a proposed fee amount can be, which is submitted as part of the LP commitment transaction. Note that a value of 0.05 = 5%. This is a network parameter.": "Maximum value that a proposed fee amount can be, which is submitted as part of the LP commitment transaction. Note that a value of 0.05 = 5%. This is a network parameter.",
"Metadata": "Metadata",
"moreProofs": "And {{count}} more proofs",
"moreProofs_one": "And {{count}} more proof",
"moreProofs_other": "And {{count}} more proofs",
"Multiplier used to translate an LP's commitment amount to their liquidity obligation. This is a network parameter.": "Multiplier used to translate an LP's commitment amount to their liquidity obligation. This is a network parameter.",
@@ -67,12 +69,14 @@
"Oracle repository": "Oracle repository",
"Oracle status for this market is <0>{{status}}</0>. {{description}} <1>Show more</1>": "Oracle status for this market is <0>{{status}}</0>. {{description}} <1>Show more</1>",
"Oracle status: {{status}}. {{description}}": "Oracle status: {{status}}. {{description}}",
"oracleInMarkets": "Oracle in {{count}} markets",
"oracleInMarkets_one": "Oracle in {{count}} market",
"oracleInMarkets_other": "Oracle in {{count}} markets",
"Price monitoring bounds {{index}}": "Price monitoring bounds {{index}}",
"Probability level for price projection, e.g. value of 0.95 will result in a price range such that over the specified projection horizon, the prices observed in the market should be in that range 95% of the time.": "Probability level for price projection, e.g. value of 0.95 will result in a price range such that over the specified projection horizon, the prices observed in the market should be in that range 95% of the time.",
"Probability level used in <0>Expected Shortfall</0> calculation when obtaining Risk Factor Long and Risk Factor Short": "Probability level used in <0>Expected Shortfall</0> calculation when obtaining Risk Factor Long and Risk Factor Short",
"Projection horizon measured as a year fraction used in <0>Expected Shortfall</0> calculation when obtaining Risk Factor Long and Risk Factor Short": "Projection horizon measured as a year fraction used in <0>Expected Shortfall</0> calculation when obtaining Risk Factor Long and Risk Factor Short",
"proofsOfOwnership": "{{count}} proofs of ownership",
"proofsOfOwnership_one": "{{count}} proof of ownership",
"proofsOfOwnership_other": "{{count}} proofs of ownership",
"Proposal": "Proposal",
@@ -131,6 +135,7 @@
"Updated": "Updated",
"Used to calculate the penalty to liquidity providers when they cannot support their open position with the assets in their margin and general accounts. This is a network parameter.": "Used to calculate the penalty to liquidity providers when they cannot support their open position with the assets in their margin and general accounts. This is a network parameter.",
"Verified since {{lastVerified}}": "Verified since {{lastVerified}}",
"verifyProofs": "Verify {{count}} proofs of ownership",
"verifyProofs_one": "Verify {{count}} proof of ownership",
"verifyProofs_other": "Verify {{count}} proofs of ownership",
"View governance proposal": "View governance proposal",
+3 -1
View File
@@ -1,7 +1,6 @@
{
"[This is {{network}} transaction only]": "[This is {{network}} transaction only]",
"{{proposalChange}} proposal {{proposalState}}": "{{proposalChange}} proposal {{proposalState}}",
"<0>{{count}}</0> blocks": "<0>{{count}}</0> blocks",
"Awaiting network confirmation": "Awaiting network confirmation",
"blocks": "blocks",
"Changes have been proposed for this asset.": "Changes have been proposed for this asset.",
@@ -15,6 +14,9 @@
"Market": "Market",
"Network upgrade in {{countdown}}": "Network upgrade in {{countdown}}",
"No proposed markets": "No proposed markets",
"numberOfBlocks": "<0>{{count}}</0> blocks",
"numberOfBlocks_one": "<0>{{count}}</0> block",
"numberOfBlocks_other": "<0>{{count}}</0> blocks",
"Parent market": "Parent market",
"Please open your wallet application and confirm or reject the transaction": "Please open your wallet application and confirm or reject the transaction",
"Please wait for your transaction to be confirmed": "Please wait for your transaction to be confirmed",
+28 -12
View File
@@ -38,8 +38,6 @@
"Code must be 64 characters in length": "Code must be 64 characters in length",
"Code must be be valid hex": "Code must be be valid hex",
"Collateral": "Collateral",
"Combined running notional over the {{count}} epochs": "Combined running notional over the {{count}} epochs",
"Combined volume (last {{count}} epochs)": "Combined volume (last {{count}} epochs)",
"Conduct your own due diligence and consult your financial advisor before making any investment decisions.": "Conduct your own due diligence and consult your financial advisor before making any investment decisions.",
"Confirm in wallet...": "Confirm in wallet...",
"Connect": "Connect",
@@ -55,6 +53,9 @@
"Countdown": "Countdown",
"Create a referral code": "Create a referral code",
"Current tier": "Current tier",
"combinedVolume": "Combined volume (last {{count}} epochs)",
"combinedVolume_one": "Combined volume (last {{count}} epoch)",
"combinedVolume_other": "Combined volume (last {{count}} epochs)",
"Dark mode": "Dark mode",
"Date Joined": "Date Joined",
"Deposit": "Deposit",
@@ -143,11 +144,15 @@
"Menu": "Menu",
"Min. epochs": "Min. epochs",
"Min. trading volume": "Min. trading volume",
"Min. trading volume (last {{count}} epochs)": "Min. trading volume (last {{count}} epochs)",
"My current volume": "My current volume",
"My liquidity provision": "My liquidity provision",
"My trading fees": "My trading fees",
"My volume (last {{count}} epochs)": "My volume (last {{count}} epochs)",
"minTradingVolume": "Min. trading volume (last {{count}} epochs)",
"minTradingVolume_one": "Min. trading volume (last {{count}} epoch)",
"minTradingVolume_other": "Min. trading volume (last {{count}} epochs)",
"myVolume": "My volume (last {{count}} epochs)",
"myVolume_one": "My volume (last {{count}} epoch)",
"myVolume_other": "My volume (last {{count}} epochs)",
"Name": "Name",
"No closed orders": "No closed orders",
"No data": "No data",
@@ -183,7 +188,6 @@
"Orders": "Orders",
"Page not found": "Page not found",
"Parent of a market": "Parent of a market",
"Past {{count}} epochs": "Past {{count}} epochs",
"Perpetuals": "Perpetuals",
"Please choose another market from the <0>market list</0>": "Please choose another market from the <0>market list</0>",
"Please connect Vega wallet": "Please connect Vega wallet",
@@ -197,17 +201,17 @@
"Proposed markets": "Proposed markets",
"Providing liquidity": "Providing liquidity",
"Purpose built proof of stake blockchain": "Purpose built proof of stake blockchain",
"pastEpochs": "Past {{count}} epochs",
"pastEpochs_one": "Past {{count}} epoch",
"pastEpochs_other": "Past {{count}} epochs",
"qUSD": "qUSD",
"qUSD provides a rough USD equivalent of balances across all assets using the value of \"Quantum\" for that asset": "qUSD provides a rough USD equivalent of balances across all assets using the value of \"Quantum\" for that asset",
"Read the terms": "Read the terms",
"Ready to trade": "Ready to trade",
"Ready to trade with real funds? <0>Switch to Mainnet</0>": "Ready to trade with real funds? <0>Switch to Mainnet</0>",
"Redeem rewards": "Redeem rewards",
"referralApplyPreviewMessage": "You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epoch.",
"referralApplyPreviewMessage_plural": "You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.",
"Referral benefits": "Referral benefits",
"Referral discount": "Referral discount",
"referral-statistics-commission": "Commission earned in <0>qUSD</0> (last {{count}} epochs)",
"Referrals": "Referrals",
"Referrer commission": "Referrer commission",
"Referrer trading discount": "Referrer trading discount",
@@ -220,6 +224,12 @@
"Rewards": "Rewards",
"Rewards history": "Rewards history",
"Rewards multipliers": "Rewards multipliers",
"referralStatisticsCommission": "Commission earned in <0>qUSD</0> (last {{count}} epochs)",
"referralStatisticsCommission_one": "Commission earned in <0>qUSD</0> (last {{count}} epoch)",
"referralStatisticsCommission_other": "Commission earned in <0>qUSD</0> (last {{count}} epochs)",
"runningNotionalOverEpochs": "Combined running notional over the {{count}} epochs",
"runningNotionalOverEpochs_one": "Combined running notional over the {{count}} epoch",
"runningNotionalOverEpochs_other": "Combined running notional over the {{count}} epochs",
"SCCR": "SCCR",
"Search": "Search",
"See all markets": "See all markets",
@@ -269,7 +279,6 @@
"This timestamp is user curated metadata and does not drive any on-chain functionality.": "This timestamp is user curated metadata and does not drive any on-chain functionality.",
"Tier": "Tier",
"Toast location": "Toast location",
"Total commission (last {{count}}} epochs)": "Total commission (last {{count}}} epochs)",
"Total discount": "Total discount",
"Total distributed": "Total distributed",
"Total fee after discount": "Total fee after discount",
@@ -283,6 +292,9 @@
"Trading on Market {{name}} may stop. There are open proposals to close this market": "Trading on Market {{name}} may stop. There are open proposals to close this market",
"Trading on Market {{name}} will stop on {{date}}": "Trading on Market {{name}} will stop on {{date}}",
"Transfer": "Transfer",
"totalCommission": "Total commission (last {{count}}} epochs)",
"totalCommission_one": "Total commission (last {{count}}} epoch)",
"totalCommission_other": "Total commission (last {{count}}} epochs)",
"Unknown": "Unknown",
"Unknown settlement date": "Unknown settlement date",
"Vega Reward pot": "Vega Reward pot",
@@ -299,14 +311,15 @@
"View successor market": "View successor market",
"Volume": "Volume",
"Volume (24h)": "Volume (24h)",
"Volume (last {{count}} epochs)": "Volume (last {{count}} epochs)",
"Volume discount": "Volume discount",
"Volume to next tier": "Volume to next tier",
"volumeLastEpochs": "Volume (last {{count}} epochs)",
"volumeLastEpochs_one": "Volume (last {{count}} epoch)",
"volumeLastEpochs_other": "Volume (last {{count}} epochs)",
"Wallet": "Wallet",
"We're sorry but we don't have an active referral programme currently running. You can propose a new programme <0>here</0>.": "We're sorry but we don't have an active referral programme currently running. You can propose a new programme <0>here</0>.",
"Welcome to Vega trading!": "Welcome to Vega trading!",
"Withdraw": "Withdraw",
"You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.": "You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.",
"You can opt out any time via settings": "You can opt out any time via settings",
"You may encounter bugs, loss of functionality or loss of assets.": "You may encounter bugs, loss of functionality or loss of assets.",
"You must be connected to the Vega wallet.": "You must be connected to the Vega wallet.",
@@ -316,5 +329,8 @@
"Your code has been rejected": "Your code has been rejected",
"Your identity is always anonymous on Vega": "Your identity is always anonymous on Vega",
"Your referral code": "Your referral code",
"Your tier": "Your tier"
"Your tier": "Your tier",
"youAreJoiningTheGroup": "You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.",
"youAreJoiningTheGroup_one": "You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epoch.",
"youAreJoiningTheGroup_other": "You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs."
}
+1 -1
View File
@@ -6,7 +6,7 @@
"Approved": "Approved",
"Await Ethereum transaction": "Await Ethereum transaction",
"Awaiting confirmation": "Awaiting confirmation",
"Awaiting confirmations {{confirmations}}/{[requiredConfirmations}}": "Awaiting confirmations {{confirmations}}/{[requiredConfirmations}}",
"Awaiting confirmations {{confirmations}}/{{requiredConfirmations}}": "Awaiting confirmations {{confirmations}}/{{requiredConfirmations}}",
"Awaiting Ethereum transaction {{confirmations}}/{{requiredConfirmations}} confirmations...": "Awaiting Ethereum transaction {{confirmations}}/{{requiredConfirmations}} confirmations...",
"Batch market instruction": "Batch market instruction",
"Cancel all orders": "Cancel all orders",
+3 -1
View File
@@ -5,7 +5,9 @@
"Available to withdraw in {{availableTimestamp}}": "Available to withdraw in {{availableTimestamp}}",
"Balance available": "Balance available",
"Complete the withdrawal to release your funds": "Complete the withdrawal to release your funds",
"Complete these {{count}} withdrawals to release your funds": "Complete these {{count}} withdrawals to release your funds",
"completeWithdrawals": "Complete these {{count}} withdrawals to release your funds",
"completeWithdrawals_one": "Complete these {{count}} withdrawal to release your funds",
"completeWithdrawals_other": "Complete these {{count}} withdrawals to release your funds",
"Complete withdrawal": "Complete withdrawal",
"Completed": "Completed",
"Connect Ethereum wallet to complete": "Connect Ethereum wallet to complete",
@@ -278,18 +278,4 @@ describe('createDownloadUrl', () => {
)}&dateRange.endTimestamp=${toNanoSeconds(dateTo)}`
);
});
it('should throw if invalid args are provided', () => {
// invalid url
expect(() => {
// @ts-ignore override z.infer type
createDownloadUrl({ ...args, protohost: 'foo' });
}).toThrow();
// invalid partyId
expect(() => {
// @ts-ignore override z.infer type
createDownloadUrl({ ...args, partyId: 'z'.repeat(64) });
}).toThrow();
});
});
+197 -140
View File
@@ -1,18 +1,18 @@
import { useRef, useState } from 'react';
import { format, subDays } from 'date-fns';
import { useRef, useCallback } from 'react';
import { subDays } from 'date-fns';
import { Controller, useForm } from 'react-hook-form';
import {
InputError,
Intent,
Loader,
TradingButton,
TradingFormGroup,
TradingInput,
TradingSelect,
} from '@vegaprotocol/ui-toolkit';
import { z } from 'zod';
import {
formatForInput,
getDateTimeFormat,
toNanoSeconds,
VEGA_ID_REGEX,
} from '@vegaprotocol/utils';
import { localLoggerFactory } from '@vegaprotocol/logger';
import { useLedgerDownloadFile } from './ledger-download-store';
@@ -35,18 +35,18 @@ const getProtoHost = (vegaurl: string) => {
return `${loc.protocol}//${loc.host}`;
};
const downloadSchema = z.object({
protohost: z.string().url().nonempty(),
partyId: z.string().regex(VEGA_ID_REGEX).nonempty(),
assetId: z.string().regex(VEGA_ID_REGEX).nonempty(),
dateFrom: z.string().nonempty(),
dateTo: z.string().optional(),
});
export const createDownloadUrl = (args: z.infer<typeof downloadSchema>) => {
// check args from form inputs
downloadSchema.parse(args);
type LedgerFormValues = {
assetId: string;
dateFrom: string;
dateTo?: string;
};
export const createDownloadUrl = (
args: LedgerFormValues & {
partyId: string;
protohost: string;
}
) => {
const params = new URLSearchParams();
params.append('partyId', args.partyId);
params.append('assetId', args.assetId);
@@ -72,117 +72,106 @@ interface Props {
export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
const t = useT();
const now = useRef(new Date());
const [dateFrom, setDateFrom] = useState(() => {
return formatForInput(subDays(now.current, 7));
const { control, handleSubmit, watch } = useForm<LedgerFormValues>({
defaultValues: {
dateFrom: formatForInput(subDays(now.current, 7)),
dateTo: '',
assetId: Object.keys(assets)[0],
},
});
const [dateTo, setDateTo] = useState('');
const dateTo = watch('dateTo');
const maxFromDate = formatForInput(new Date(dateTo || now.current));
const maxToDate = formatForInput(now.current);
const [assetId, setAssetId] = useState(Object.keys(assets)[0]);
const protohost = getProtoHost(vegaUrl);
const disabled = Boolean(!assetId);
const hasItem = useLedgerDownloadFile((store) => store.hasItem);
const updateDownloadQueue = useLedgerDownloadFile(
(store) => store.updateQueue
);
const assetDropDown = (
<TradingSelect
id="select-ledger-asset"
value={assetId}
onChange={(e) => {
setAssetId(e.target.value);
}}
className="w-full"
data-testid="select-ledger-asset"
>
{Object.keys(assets).map((assetKey) => (
<option key={assetKey} value={assetKey}>
{assets[assetKey]}
</option>
))}
</TradingSelect>
);
const link = createDownloadUrl({
protohost,
partyId,
assetId,
dateFrom,
dateTo,
});
const startDownload = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const title = t(
'Downloading for {{asset}} from {{startDate}} till {{endDate}}',
{
asset: assets[assetId],
startDate: format(new Date(dateFrom), 'dd MMMM yyyy HH:mm'),
endDate: format(new Date(dateTo || Date.now()), 'dd MMMM yyyy HH:mm'),
}
);
const downloadStoreItem = {
title,
link,
isChanged: true,
};
if (hasItem(link)) {
updateDownloadQueue(downloadStoreItem);
return;
}
const ts = setTimeout(() => {
updateDownloadQueue({
...downloadStoreItem,
intent: Intent.Warning,
isDelayed: true,
isChanged: true,
const startDownload = useCallback(
async (formValues: LedgerFormValues) => {
const link = createDownloadUrl({
protohost,
partyId,
...formValues,
});
}, 1000 * 30);
try {
updateDownloadQueue(downloadStoreItem);
const resp = await fetch(link);
if (!resp?.ok) {
if (resp?.status === 429) {
throw new Error('Too many requests. Try again later.');
const dateTimeFormatter = getDateTimeFormat();
const title = t(
'Downloading for {{asset}} from {{startDate}} till {{endDate}}',
{
asset: assets[formValues.assetId],
startDate: dateTimeFormatter.format(new Date(formValues.dateFrom)),
endDate: dateTimeFormatter.format(
new Date(formValues.dateTo || Date.now())
),
}
throw new Error('Download of ledger entries failed');
);
const downloadStoreItem = {
title,
link,
isChanged: true,
};
if (hasItem(link)) {
updateDownloadQueue(downloadStoreItem);
return;
}
const { headers } = resp;
const nameHeader = headers.get('content-disposition');
const filename = nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME;
updateDownloadQueue({
...downloadStoreItem,
filename,
});
const blob = await resp.blob();
if (blob) {
const ts = setTimeout(() => {
updateDownloadQueue({
...downloadStoreItem,
blob,
isDownloaded: true,
intent: Intent.Warning,
isDelayed: true,
isChanged: true,
intent: Intent.Success,
});
}, 1000 * 30);
try {
updateDownloadQueue(downloadStoreItem);
const resp = await fetch(link);
if (!resp?.ok) {
if (resp?.status === 429) {
throw new Error('Too many requests. Try again later.');
}
throw new Error('Download of ledger entries failed');
}
const { headers } = resp;
const nameHeader = headers.get('content-disposition');
const filename =
nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME;
updateDownloadQueue({
...downloadStoreItem,
filename,
});
const blob = await resp.blob();
if (blob) {
updateDownloadQueue({
...downloadStoreItem,
blob,
isDownloaded: true,
isChanged: true,
intent: Intent.Success,
});
}
} catch (err) {
localLoggerFactory({ application: 'ledger' }).error(
'Download file',
err
);
updateDownloadQueue({
...downloadStoreItem,
intent: Intent.Danger,
isError: true,
isChanged: true,
errorMessage: (err as Error).message || undefined,
});
} finally {
clearTimeout(ts);
}
} catch (err) {
localLoggerFactory({ application: 'ledger' }).error('Download file', err);
updateDownloadQueue({
...downloadStoreItem,
intent: Intent.Danger,
isError: true,
isChanged: true,
errorMessage: (err as Error).message || undefined,
});
} finally {
clearTimeout(ts);
}
};
},
[assets, hasItem, partyId, protohost, t, updateDownloadQueue]
);
if (!protohost || Object.keys(assets).length === 0) {
return null;
@@ -191,49 +180,117 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
const offset = new Date().getTimezoneOffset();
return (
<form onSubmit={startDownload} className="p-4 w-[350px]">
<form
onSubmit={handleSubmit(startDownload)}
className="p-4 w-[350px]"
noValidate
>
<h2 className="mb-4">{t('Export ledger entries')}</h2>
<TradingFormGroup label={t('Select asset')} labelFor="asset">
{assetDropDown}
</TradingFormGroup>
<TradingFormGroup label={t('Date from')} labelFor="date-from">
<TradingInput
type="datetime-local"
data-testid="date-from"
id="date-from"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
max={maxFromDate}
/>
</TradingFormGroup>
<TradingFormGroup label={t('Date to')} labelFor="date-to">
<TradingInput
type="datetime-local"
data-testid="date-to"
id="date-to"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
max={maxToDate}
/>
</TradingFormGroup>
<Controller
name="assetId"
control={control}
rules={{
required: t('You need to select an asset'),
}}
render={({ field, fieldState }) => (
<div className="mb-2">
<TradingFormGroup
label={t('Select asset')}
labelFor="asset"
compact
>
<TradingSelect
{...field}
id="select-ledger-asset"
className="w-full"
data-testid="select-ledger-asset"
>
{Object.keys(assets).map((assetKey) => (
<option key={assetKey} value={assetKey}>
{assets[assetKey]}
</option>
))}
</TradingSelect>
</TradingFormGroup>
{fieldState.error && (
<InputError>{fieldState.error.message}</InputError>
)}
</div>
)}
/>
<Controller
name="dateFrom"
control={control}
rules={{
required: t('You need to provide a date from'),
max: {
value: maxFromDate,
message: dateTo
? t('Date from cannot be greater than date to')
: t('Date from cannot be in the future'),
},
deps: ['dateTo'],
}}
render={({ field, fieldState }) => (
<div className="mb-2">
<TradingFormGroup
label={t('Date from')}
labelFor="date-from"
compact
>
<TradingInput
{...field}
type="datetime-local"
data-testid="date-from"
id="date-from"
max={maxFromDate}
/>
</TradingFormGroup>
{fieldState.error && (
<InputError>{fieldState.error.message}</InputError>
)}
</div>
)}
/>
<Controller
name="dateTo"
control={control}
rules={{
max: {
value: maxToDate,
message: t('Date to cannot be in the future'),
},
}}
render={({ field, fieldState }) => (
<div className="mb-2">
<TradingFormGroup label={t('Date to')} labelFor="date-to" compact>
<TradingInput
{...field}
type="datetime-local"
data-testid="date-to"
id="date-to"
max={maxToDate}
/>
</TradingFormGroup>
{fieldState.error && (
<InputError>{fieldState.error.message}</InputError>
)}
</div>
)}
/>
<div className="relative text-sm" title={t('Download all to .csv file')}>
<TradingButton
fill
disabled={disabled}
type="submit"
data-testid="ledger-download-button"
>
<TradingButton fill type="submit" data-testid="ledger-download-button">
{t('Download')}
</TradingButton>
</div>
{offset && (
{offset ? (
<p className="text-xs text-neutral-400 mt-1">
{t(
'The downloaded file uses the UTC time zone for all listed times. Your time zone is UTC{{offset}}.',
{ offset: toHoursAndMinutes(offset) }
)}
</p>
)}
) : null}
</form>
);
};
+4 -12
View File
@@ -107,9 +107,6 @@ export const LiquidityTable = ({
const feesAccruedTooltip = ({ value, data }: ITooltipParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
let lessThanFull = false,
lessThanMinimum = false;
if (data.sla) {
@@ -154,7 +151,7 @@ export const LiquidityTable = ({
}
);
}
return addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0);
return addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0);
};
const stakeToCcyVolumeQuantumFormatter = ({
@@ -416,14 +413,9 @@ export const LiquidityTable = ({
},
'text-red-500': ({ data }: { data: LiquidityProvisionData }) => {
if (!data.sla) return false;
return (
new BigNumber(
data.sla.currentEpochFractionOfTimeOnBook
).isLessThan(data.commitmentMinTimeFraction) &&
new BigNumber(
data.sla.currentEpochFractionOfTimeOnBook
).isGreaterThan(0)
);
return new BigNumber(
data.sla.currentEpochFractionOfTimeOnBook
).isLessThan(data.commitmentMinTimeFraction);
},
},
},
@@ -7,7 +7,11 @@ import {
} from '@vegaprotocol/data-provider';
import { type Market } from '@vegaprotocol/markets';
import { marketsMapProvider } from '@vegaprotocol/markets';
import { Cursor, type PageInfo, type Edge } from '@vegaprotocol/data-provider';
import {
type Cursor,
type PageInfo,
type Edge,
} from '@vegaprotocol/data-provider';
import { OrderStatus } from '@vegaprotocol/types';
import {
OrdersDocument,
@@ -1,6 +1,6 @@
import { memo, forwardRef, useMemo, type ForwardedRef } from 'react';
import {
MAXGOINT64,
HALFMAXGOINT64,
addDecimalsFormatNumber,
getDateTimeFormat,
isNumeric,
@@ -151,7 +151,7 @@ export const OrderListTable = memo<
: '';
if (
data.size === MAXGOINT64 &&
data.size >= HALFMAXGOINT64 &&
data.timeInForce ===
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC &&
data.reduceOnly
@@ -3,7 +3,7 @@ import { PositionsManager } from './positions-manager';
import { positionsMarketsProvider } from './positions-data-providers';
import { singleRow } from './positions.mock';
import { MockedProvider } from '@apollo/client/testing';
import { MAXGOINT64 } from '@vegaprotocol/utils';
import { HALFMAXGOINT64 } from '@vegaprotocol/utils';
const mockCreate = jest.fn();
@@ -31,9 +31,7 @@ jest.mock('@vegaprotocol/data-provider', () => ({
}));
describe('PositionsManager', () => {
// TODO: Close position temporarily disabled in https://github.com/vegaprotocol/frontend-monorepo/pull/5350
// eslint-disable-next-line jest/no-disabled-tests
it.skip('should close position with max uint64', async () => {
it('should close position with half of max uint64', async () => {
render(<PositionsManager partyIds={['partyId']} isReadOnly={false} />, {
wrapper: MockedProvider,
});
@@ -43,6 +41,6 @@ describe('PositionsManager', () => {
expect(
mockCreate.mock.lastCall[0].batchMarketInstructions.submissions[0].size
).toEqual(MAXGOINT64);
).toEqual(HALFMAXGOINT64);
});
});
+34 -39
View File
@@ -7,13 +7,11 @@ import {
} from './positions-data-providers';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { useT } from '../use-t';
// TODO: Close position temporarily disabled in https://github.com/vegaprotocol/frontend-monorepo/pull/5350
//
// import { useCallback } from 'react';
// import * as Schema from '@vegaprotocol/types';
// import { useVegaTransactionStore } from '@vegaprotocol/web3';
// import { MAXGOINT64 } from '@vegaprotocol/utils';
import { useCallback } from 'react';
import * as Schema from '@vegaprotocol/types';
import { useVegaTransactionStore } from '@vegaprotocol/web3';
import { HALFMAXGOINT64 } from '@vegaprotocol/utils';
import { FLAGS } from '@vegaprotocol/environment';
interface PositionsManagerProps {
partyIds: string[];
@@ -32,37 +30,35 @@ export const PositionsManager = ({
}: PositionsManagerProps) => {
const t = useT();
const { pubKeys, pubKey } = useVegaWallet();
const create = useVegaTransactionStore((store) => store.create);
const disableClosePositionsButton = FLAGS.DISABLE_CLOSE_POSITION;
// TODO: Close position temporarily disabled in https://github.com/vegaprotocol/frontend-monorepo/pull/5350
//
// const create = useVegaTransactionStore((store) => store.create);
//
// const onClose = useCallback(
// ({ marketId, openVolume }: { marketId: string; openVolume: string }) =>
// create({
// batchMarketInstructions: {
// cancellations: [
// {
// marketId,
// orderId: '', // omit order id to cancel all active orders
// },
// ],
// submissions: [
// {
// marketId: marketId,
// type: Schema.OrderType.TYPE_MARKET as const,
// timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC as const,
// side: openVolume.startsWith('-')
// ? Schema.Side.SIDE_BUY
// : Schema.Side.SIDE_SELL,
// size: MAXGOINT64, // improvement for avoiding leftovers filled in the meantime when close request has been sent
// reduceOnly: true,
// },
// ],
// },
// }),
// [create]
// );
const onClose = useCallback(
({ marketId, openVolume }: { marketId: string; openVolume: string }) =>
create({
batchMarketInstructions: {
cancellations: [
{
marketId,
orderId: '', // omit order id to cancel all active orders
},
],
submissions: [
{
marketId: marketId,
type: Schema.OrderType.TYPE_MARKET as const,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC as const,
side: openVolume.startsWith('-')
? Schema.Side.SIDE_BUY
: Schema.Side.SIDE_SELL,
size: HALFMAXGOINT64, // improvement for avoiding leftovers filled in the meantime when close request has been sent
reduceOnly: true,
},
],
},
}),
[create]
);
const { data: marketIds } = useDataProvider({
dataProvider: positionsMarketsProvider,
@@ -81,8 +77,7 @@ export const PositionsManager = ({
pubKeys={pubKeys}
rowData={data}
onMarketClick={onMarketClick}
// TODO: temporarily disable close position
// onClose={onClose}
onClose={disableClosePositionsButton ? undefined : onClose}
isReadOnly={isReadOnly}
multipleKeys={partyIds.length > 1}
overlayNoRowsTemplate={error ? error.message : t('No positions')}
+3 -10
View File
@@ -220,10 +220,7 @@ export const PositionsTable = ({
<p className="mb-2">{secondaryTooltip}</p>
<p className="mb-2">
{t('Status: {{status}}', {
nsSeparator: '*',
replace: {
status: PositionStatusMapping[args.data.status],
},
status: PositionStatusMapping[args.data.status],
})}
</p>
{POSITION_RESOLUTION_LINK && (
@@ -390,18 +387,14 @@ export const PositionsTable = ({
<>
<p className="mb-2">
{t('Realised PNL: {{value}}', {
nsSeparator: '*',
replace: { value: args.value },
value: args.value,
})}
</p>
<p className="mb-2">
{t(
'Lifetime loss socialisation deductions: {{losses}}',
{
nsSeparator: '*',
replace: {
losses: lossesFormatted,
},
losses: lossesFormatted,
}
)}
</p>
@@ -30,7 +30,7 @@ export const MarketProposalNotification = ({
</div>
);
return (
<div className="border-default min-w-min whitespace-nowrap border-l pb-1 pl-1 pr-1">
<div className="border-default min-w-min border-l pb-1 pl-1 pr-1">
<Notification
intent={Intent.Warning}
message={message}
@@ -48,6 +48,7 @@ export const ProtocolUpgradeCountdown = ({
case ProtocolUpgradeCountdownMode.IN_BLOCKS:
countdown = (
<Trans
i18nKey="numberOfBlocks"
defaults="<0>{{count}}</0> blocks"
components={[<span className={emphasis}>count</span>]}
values={{
@@ -43,6 +43,7 @@ export const ProtocolUpgradeProposalNotification = ({
case ProtocolUpgradeCountdownMode.IN_BLOCKS:
countdown = (
<Trans
i18nKey="numberOfBlocks"
defaults="<0>{{count}}</0> blocks"
components={[<span className="text-vega-orange-500">count</span>]}
values={{
+1 -1
View File
@@ -98,7 +98,7 @@ export const tradesProvider = makeDataProvider<
pagination: {
getPageInfo,
append,
first: MAX_TRADES,
last: MAX_TRADES,
},
fetchPolicy: 'no-cache',
getSubscriptionVariables: ({ marketId }) => ({ marketId }),
@@ -1,7 +1,26 @@
export const IconGlobe = ({ size = 24 }: { size: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 24 24">
<path d="M11.9946 3C10.2165 3.00106 8.47842 3.52883 6.99987 4.51677C5.51983 5.5057 4.36628 6.91131 3.68509 8.55585C3.0039 10.2004 2.82567 12.01 3.17294 13.7558C3.5202 15.5016 4.37737 17.1053 5.63604 18.364C6.89471 19.6226 8.49836 20.4798 10.2442 20.8271C11.99 21.1743 13.7996 20.9961 15.4442 20.3149C17.0887 19.6337 18.4943 18.4802 19.4832 17.0001C20.4722 15.5201 21 13.78 21 12C21 9.61305 20.0518 7.32386 18.364 5.63604C16.6761 3.94821 14.3869 3 12 3C12.0001 3 11.9999 3 12 3M11.9959 3.936C11.9972 3.936 11.9985 3.936 11.9999 3.936C13.2976 3.93617 14.3772 5.83592 14.9515 8.22998H9.04712C9.62068 5.83664 10.6991 3.93971 11.9959 3.936ZM9.8073 4.24157C9.05208 5.16925 8.44481 6.56185 8.07534 8.22998H4.87438C5.24741 7.52551 5.72602 6.87397 6.3 6.3C7.28288 5.31711 8.49319 4.61388 9.8073 4.24157ZM4.42885 9.22998C4.10667 10.1091 3.93685 11.0458 3.936 12C3.936 12.9499 4.10378 13.8872 4.42669 14.77H7.88922C7.75324 13.8973 7.67969 12.9663 7.67969 12C7.67969 11.0336 7.7527 10.1027 7.8879 9.22998H4.42885ZM4.87153 15.77C5.00006 16.013 5.14133 16.2501 5.29503 16.4801C6.18112 17.8062 7.44054 18.8398 8.91404 19.4502C9.20977 19.5727 9.51146 19.677 9.81744 19.763C9.06048 18.8354 8.44956 17.4409 8.07765 15.77H4.87153ZM14.1834 19.7628C15.5101 19.3896 16.7227 18.6815 17.7021 17.7021C18.2744 17.1298 18.7541 16.4778 19.1285 15.77H15.9224C15.5508 17.4416 14.9402 18.8355 14.1834 19.7628ZM19.5733 14.77C19.7153 14.3819 19.8278 13.9819 19.9091 13.5732C20.1981 12.12 20.0808 10.6174 19.5733 9.22998H16.1106C16.2463 10.1024 16.3197 11.0333 16.3197 12C16.3197 12.9667 16.2463 13.8976 16.1106 14.77H19.5733ZM19.1285 8.22998C18.5047 7.05058 17.596 6.04063 16.4801 5.29503C15.7711 4.82129 14.9955 4.46564 14.1834 4.23723C14.9402 5.16453 15.5508 6.55844 15.9224 8.22998H19.1285ZM8.60129 12C8.60129 11.0806 8.68603 10.1352 8.84194 9.22998H15.1569C15.3132 10.1358 15.3981 11.0814 15.3981 12C15.3981 12.9186 15.314 13.8642 15.1588 14.77H8.84003C8.68519 13.8648 8.60129 12.9194 8.60129 12ZM11.9997 20.064C10.6916 20.064 9.61486 18.1657 9.04394 15.77H14.9547C14.3836 18.1642 13.3072 20.064 11.9997 20.064Z" />
<path
d="M12 1.248C14.1265 1.248 16.2053 1.87859 17.9735 3.06004C19.7417 4.24148 21.1198 5.92072 21.9336 7.88539C22.7474 9.85006 22.9603 12.0119 22.5454 14.0976C22.1305 16.1833 21.1065 18.0991 19.6028 19.6028C18.0991 21.1065 16.1833 22.1305 14.0976 22.5454C12.0119 22.9603 9.85006 22.7473 7.88539 21.9336C5.92072 21.1198 4.24149 19.7416 3.06004 17.9735C1.8786 16.2053 1.248 14.1265 1.248 12C1.25055 9.14917 2.38416 6.41584 4.4 4.39999C6.41584 2.38415 9.14918 1.25054 12 1.248ZM12 0C9.62663 0 7.30655 0.703786 5.33316 2.02236C3.35977 3.34094 1.8217 5.21508 0.91345 7.4078C0.00519871 9.60051 -0.23244 12.0133 0.230582 14.3411C0.693605 16.6689 1.83649 18.807 3.51472 20.4853C5.19295 22.1635 7.33115 23.3064 9.65892 23.7694C11.9867 24.2324 14.3995 23.9948 16.5922 23.0866C18.7849 22.1783 20.6591 20.6402 21.9776 18.6668C23.2962 16.6935 24 14.3734 24 12C24 8.8174 22.7357 5.76515 20.4853 3.51472C18.2349 1.26428 15.1826 0 12 0Z"
fill="currentColor"
/>
<path
d="M12 1.248C14.592 1.248 16.5312 6.9312 16.5312 12C16.5312 17.0688 14.6112 22.752 12 22.752C9.38879 22.752 7.46879 17.0784 7.46879 12C7.46879 6.9216 9.40799 1.248 12 1.248ZM12 0C8.81279 0 6.23999 5.376 6.23999 12C6.23999 18.624 8.83199 24 12 24C15.168 24 17.76 18.6336 17.76 12C17.76 5.3664 15.168 0 12 0Z"
fill="currentColor"
/>
<path
d="M1.229 7.64001H22.7714"
stroke="currentColor"
strokeWidth="1.3"
strokeMiterlimit="10"
/>
<path
d="M1.229 16.36H22.7714"
stroke="currentColor"
strokeWidth="1.3"
strokeMiterlimit="10"
/>
</svg>
);
};
@@ -1,9 +1,8 @@
export const IconMoon = ({ size = 16 }: { size: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 16 16">
<path d="M8 1C4.13 1 1 4.13 1 8C1 11.87 4.13 15 8 15C11.87 15 15 11.87 15 8C15 4.13 11.87 1 8 1ZM8.66 12.44H7.32V11.1H8.66V12.44ZM10.38 6.78C10.29 7.01 10.18 7.2 10.05 7.36C9.92 7.52 9.75 7.7 9.53 7.91C9.3 8.14 9.11 8.34 8.98 8.51C8.85 8.68 8.73 8.89 8.64 9.14C8.55 9.37 8.51 9.65 8.51 9.96V10.04V10.13H7.3V10.04C7.3 9.61 7.36 9.24 7.47 8.92C7.58 8.61 7.71 8.34 7.87 8.13C8.03 7.92 8.22 7.69 8.47 7.43C8.75 7.13 8.96 6.87 9.09 6.66C9.22 6.46 9.28 6.2 9.28 5.88C9.28 5.47 9.16 5.16 8.93 4.93C8.7 4.7 8.38 4.58 7.96 4.58C7.6 4.58 7.28 4.68 7.01 4.89C6.75 5.09 6.56 5.44 6.45 5.96C6.34 6.48 6.43 6.06 6.43 6.06L5.26 5.62L5.28 5.54C5.47 4.87 5.81 4.35 6.29 4.02C6.77 3.69 7.34 3.53 8 3.53C8.75 3.53 9.37 3.75 9.82 4.18C10.28 4.62 10.5 5.22 10.5 5.97C10.5 6.27 10.46 6.53 10.37 6.76L10.38 6.78Z" />
<circle cx="8" cy="8" r="7" />
<path d="M6.15393 5.69232C6.15393 5.10304 6.24054 4.5075 6.46161 4C4.99179 4.63982 4 6.14089 4 7.84607C4 10.1402 5.85982 12 8.15393 12C9.85911 12 11.3602 11.0082 12 9.53839C11.4925 9.75946 10.8964 9.84607 10.3077 9.84607C8.01357 9.84607 6.15393 7.98643 6.15393 5.69232Z" />
// TODO: we need to rescale the icon in an svg editor so the view box is the default 0 0 16 16
<svg width={size} height={size} viewBox="0 0 45 45">
<path d="M28.75 11.69A12.39 12.39 0 0 0 22.5 10a12.5 12.5 0 1 0 0 25c2.196 0 4.353-.583 6.25-1.69A12.46 12.46 0 0 0 35 22.5a12.46 12.46 0 0 0-6.25-10.81Zm-6.25 22a11.21 11.21 0 0 1-11.2-11.2 11.21 11.21 0 0 1 11.2-11.2c1.246 0 2.484.209 3.66.62a13.861 13.861 0 0 0-5 10.58 13.861 13.861 0 0 0 5 10.58 11.078 11.078 0 0 1-3.66.63v-.01Z" />
</svg>
);
};
@@ -0,0 +1,17 @@
export const IconSun = ({ size = 16 }: { size: number }) => {
return (
// TODO: we need to rescale the icon in an svg editor so the view box is the default 0 0 16 16
<svg width={size} height={size} viewBox="0 0 45 45">
<path
d="M22.5 27.79a5.29 5.29 0 1 0 0-10.58 5.29 5.29 0 0 0 0 10.58Z"
fill="currentColor"
/>
<path
d="M15.01 22.5H10M35 22.5h-5.01M22.5 29.99V35M22.5 10v5.01M17.21 27.79l-3.55 3.55M31.34 13.66l-3.55 3.55M27.79 27.79l3.55 3.55M13.66 13.66l3.55 3.55"
stroke="currentColor"
strokeWidth="1.3"
strokeMiterlimit="10"
/>
</svg>
);
};
@@ -31,6 +31,7 @@ import { IconPlus } from './svg-icons/icon-plus';
import { IconQuestionMark } from './svg-icons/icon-question-mark';
import { IconSearch } from './svg-icons/icon-search';
import { IconStar } from './svg-icons/icon-star';
import { IconSun } from './svg-icons/icon-sun';
import { IconTick } from './svg-icons/icon-tick';
import { IconTicket } from './svg-icons/icon-ticket';
import { IconTransfer } from './svg-icons/icon-transfer';
@@ -75,6 +76,7 @@ export enum VegaIconNames {
QUESTION_MARK = 'question-mark',
SEARCH = 'search',
STAR = 'star',
SUN = 'sun',
TICK = 'tick',
TICKET = 'ticket',
TRANSFER = 'transfer',
@@ -125,6 +127,7 @@ export const VegaIconNameMap: Record<
plus: IconPlus,
search: IconSearch,
star: IconStar,
sun: IconSun,
tick: IconTick,
ticket: IconTicket,
transfer: IconTransfer,
@@ -5,7 +5,7 @@ import { VegaIconNameMap } from './vega-icon-record';
export interface VegaIconProps {
name: VegaIconNames;
size?: 8 | 10 | 12 | 13 | 14 | 16 | 18 | 20 | 24 | 32;
size?: 8 | 10 | 12 | 13 | 14 | 16 | 18 | 20 | 24 | 28 | 32;
}
export const VegaIcon = ({ size = 16, name }: VegaIconProps) => {
+1
View File
@@ -19,6 +19,7 @@ export * from './indicator';
export * from './input';
export * from './input-error';
export * from './key-value-table';
export * from './language-selector';
export * from './link';
export * from './loader';
export * from './lozenge';

Some files were not shown because too many files have changed in this diff Show More