Compare commits

..
Author SHA1 Message Date
Madalina Raicu ec87858b22 fix: lint issue 2023-12-01 12:32:30 +00:00
Madalina Raicu ee98dfd5fd Merge branch 'chore/add-close-pos-back' of github.com:vegaprotocol/frontend-monorepo into chore/add-close-pos-back 2023-12-01 12:31:16 +00:00
m.ray 7c30ad9998 Update libs/utils/src/lib/constants.ts 2023-12-01 11:41:23 +00:00
Madalina Raicu 2bbd64b757 chore: update test name and add comment 2023-12-01 11:30:49 +00:00
Madalina Raicu aaad3cf402 fix: add environment variable and use half of maxgoint64 everywhere 2023-12-01 10:14:33 +00:00
Madalina Raicu bb69f5c05f fix: use half of MAXGOINT64 2023-11-30 19:36:25 +00:00
Madalina Raicu e1aa355c97 chore(trading): add close position button back to console 2023-11-30 19:26:45 +00: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
Art 73a118978f feat(trading): referral code eligibility (#5325) 2023-11-28 16:08:21 +01:00
m.ray eac26c1966 fix(trading): empty connect wallet dialog (#5356) 2023-11-28 14:43:31 +00:00
Matthew Russell d615587564 Merge pull request #5361 from vegaprotocol/chore/backmerge-main
chore(trading): back merge main
2023-11-27 22:33:19 -08:00
Matthew Russell ba4ce1ce88 fix: botched merge in gitignore 2023-11-27 22:06:46 -08:00
Matthew Russell 3bbacc1aa0 fix: fix broken test after removal of formatting 2023-11-27 21:22:30 -08:00
Matthew Russell de5371435d Merge branch 'main' into chore/backmerge-main 2023-11-27 20:08:08 -08:00
m.rayandMatthew Russell 12cb5e10b6 chore(trading): merge main in develop (#5352)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-11-27 15:49:26 -08:00
Art ee2909cc84 fix(wallet): empty connect dialog (#5354) 2023-11-27 16:15:18 +00:00
Bartłomiej Głownia 068d6abf1b feat(trading): show percentage sign on y axis in funding rate chart (#5342) 2023-11-27 13:39:20 +00:00
m.ray e5d4d2b0b8 chore(trading): temporarily disable close position (#5350) 2023-11-27 10:48:29 +00:00
m.ray d4e801cfc6 feat(trading): reset console to default (#5344) 2023-11-27 10:42:47 +00:00
Bartłomiej Głownia 964deb2f23 feat(trading): update title on rewards and fees pages (#5347) 2023-11-27 08:18:59 +00:00
Art 6669125dd3 feat(trading): copy apply referral link from code tile (#5338) 2023-11-24 10:20:37 +00:00
m.ray 129b6c4e89 feat(trading): referral initial state when no sets are available (#5294) 2023-11-23 17:08:26 +00:00
Ben ca418cabfe chore(trading): migrate python tests to jest (#5339) 2023-11-23 15:56:29 +00:00
Ben 192af844c4 chore(trading): fix change keys edge case (#5337) 2023-11-23 09:12:45 +00:00
Ben 6a841f226a chore(ci): reduce number of runners for python tests (#5333) 2023-11-22 16:00:29 +00:00
Ben 76426baa2a chore(ci): create traces dir (#5327) 2023-11-22 15:52:59 +00:00
Ben 6fdac2419c chore(trading): move python stop order tests to jest (#5329) 2023-11-22 15:10:19 +00:00
Matthew Russell d9dc43b359 chore(deal-ticket): re-enable margin estimates for perps (#5317) 2023-11-20 13:57:18 -08:00
m.rayandMatthew Russell 06a6fe6d67 fix(accounts): remove account balance formatting in transfer form (#5315)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-11-20 13:07:17 -08:00
125 changed files with 2451 additions and 1426 deletions
+2 -2
View File
@@ -205,7 +205,7 @@ jobs:
# run tests
#----------------------------------------------
- name: Run tests
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v -s --numprocesses 6 --dist loadfile --durations=15
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v -s --numprocesses 4 --dist loadfile --durations=15
working-directory: apps/trading/e2e
#----------------------------------------------
# upload traces
@@ -215,7 +215,7 @@ jobs:
if: always()
with:
name: playwright-trace
path: ./traces/
path: apps/trading/e2e/traces/
retention-days: 15
#----------------------------------------------
# ----- upload logs -----
+1 -2
View File
@@ -59,5 +59,4 @@ apps/trading/e2e/logs/
apps/trading/e2e/.pytest_cache/
apps/trading/e2e/traces/
.nx/cache
.nx/
@@ -12,7 +12,8 @@ import { type VegaICellRendererParams } from '@vegaprotocol/datagrid';
import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { useNavigate } from 'react-router-dom';
import { type RowClickedEvent, ColDef } from 'ag-grid-community';
import { 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 { 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
+11 -1
View File
@@ -1,11 +1,21 @@
import { FeesContainer } from '../../components/fees-container';
import { useT } from '../../lib/use-t';
import { usePageTitleStore } from '../../stores';
import { titlefy } from '@vegaprotocol/utils';
import { useEffect } from 'react';
export const Fees = () => {
const t = useT();
const title = t('Fees');
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([title]));
}, [updateTitle, title]);
return (
<div className="container p-4 mx-auto">
<h1 className="px-4 pb-4 text-2xl">{t('Fees')}</h1>
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
<FeesContainer />
</div>
);
@@ -1,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 (
@@ -10,7 +10,7 @@ import { useForm } from 'react-hook-form';
import classNames from 'classnames';
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { RainbowButton } from './buttons';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { useReferral } from './hooks/use-referral';
@@ -32,6 +32,19 @@ const validateCode = (value: string, t: ReturnType<typeof useT>) => {
return true;
};
export const ApplyCodeFormContainer = () => {
const { pubKey } = useVegaWallet();
const { data: referee } = useReferral({ pubKey, role: 'referee' });
const { data: referrer } = useReferral({ pubKey, role: 'referrer' });
// go to main page if the current pubkey is already a referrer or referee
if (referee || referrer) {
return <Navigate to={Routes.REFERRALS} />;
}
return <ApplyCodeForm />;
};
export const ApplyCodeForm = () => {
const t = useT();
const program = useReferralProgram();
@@ -55,14 +68,29 @@ export const ApplyCodeForm = () => {
} = useForm();
const [params] = useSearchParams();
const { data: referee } = useReferral({ pubKey, role: 'referee' });
const { data: referrer } = useReferral({ pubKey, role: 'referrer' });
const codeField = watch('code');
const { data: previewData, loading: previewLoading } = useReferral({
code: validateCode(codeField, t) ? codeField : undefined,
});
/**
* Validates the set a user tries to apply to.
*/
const validateSet = useCallback(() => {
if (
codeField &&
!previewLoading &&
previewData &&
!previewData.isEligible
) {
return t('The code is no longer valid.');
}
if (codeField && !previewLoading && !previewData) {
return t('The code is invalid');
}
return true;
}, [codeField, previewData, previewLoading, t]);
useEffect(() => {
const code = params.get('code');
if (code) setValue('code', code);
@@ -144,11 +172,6 @@ export const ApplyCodeForm = () => {
}
}, [navigate, status]);
// go to main page if the current pubkey is already a referrer or referee
if (referee || referrer) {
return <Navigate to={Routes.REFERRALS} />;
}
// show "code applied" message when successfully applied
if (status === 'successful') {
return (
@@ -205,7 +228,10 @@ export const ApplyCodeForm = () => {
return (
<>
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto w-2/3 max-w-md rounded-lg p-8">
<div
data-testid="referral-apply-code-form"
className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto w-2/3 max-w-md rounded-lg p-8"
>
<h3 className="calt mb-4 text-center text-2xl">
{t('Apply a referral code')}
</h3>
@@ -224,7 +250,11 @@ export const ApplyCodeForm = () => {
hasError={Boolean(errors.code)}
{...register('code', {
required: t('You have to provide a code to apply it.'),
validate: (value) => validateCode(value, t),
validate: (value) => {
const err = validateCode(value, t);
if (err !== true) return err;
return validateSet();
},
})}
placeholder="Enter a code"
className="bg-vega-clight-900 dark:bg-vega-cdark-700 mb-2"
@@ -238,15 +268,17 @@ export const ApplyCodeForm = () => {
</InputError>
)}
</div>
{previewLoading && !previewData ? (
{validateCode(codeField, t) === true && previewLoading && !previewData ? (
<div className="mt-10">
<Loader />
</div>
) : null}
{previewData ? (
{/* TODO: Re-check plural forms once i18n is updated */}
{previewData && previewData.isEligible ? (
<div className="mt-10">
<h2 className="mb-5 text-2xl">
{t(
'youAreJoiningTheGroup',
'You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.',
{ count: nextBenefitTierEpochsValue }
)}
@@ -39,7 +39,10 @@ export const CreateCodeForm = () => {
const { pubKey, isReadOnly } = useVegaWallet();
return (
<div className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg">
<div
data-testid="referral-create-code-form"
className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg"
>
<h3 className="mb-4 text-2xl text-center calt">
{t('Create a referral code')}
</h3>
@@ -95,6 +98,11 @@ const CreateCodeDialog = ({
const { stakeAvailable: currentStakeAvailable, requiredStake } =
useStakeAvailable();
const { data: referralSets } = useReferral({
pubKey,
role: 'referrer',
});
const onSubmit = () => {
if (isReadOnly || !pubKey) {
setErr('Not connected');
@@ -193,6 +201,68 @@ const CreateCodeDialog = ({
);
}
if (!referralSets) {
return (
<div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && (
<>
{
<p>
{t(
'There is currently no referral program active, are you sure you want to create a code?'
)}
</p>
}
</>
)}
{status === 'success' && code && (
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700">
<p className="overflow-hidden whitespace-nowrap text-ellipsis">
{code}
</p>
</div>
<CopyWithTooltip text={code}>
<TradingButton
className="text-sm no-underline"
icon={<VegaIcon name={VegaIconNames.COPY} />}
>
<span>{t('Copy')}</span>
</TradingButton>
</CopyWithTooltip>
</div>
)}
<TradingButton
fill={true}
intent={Intent.Primary}
onClick={() => onSubmit()}
{...getButtonProps()}
></TradingButton>
{status === 'idle' && (
<TradingButton
fill={true}
intent={Intent.Primary}
onClick={() => {
refetch();
setDialogOpen(false);
}}
>
{t('No')}
</TradingButton>
)}
{err && <InputError>{err}</InputError>}
<div className="flex justify-center pt-5 mt-2 text-sm border-t gap-4 text-default border-default">
<ExternalLink href={ABOUT_REFERRAL_DOCS_LINK}>
{t('About the referral program')}
</ExternalLink>
<ExternalLink href={DISCLAIMER_REFERRAL_DOCS_LINK}>
{t('Disclaimer')}
</ExternalLink>
</div>
</div>
);
}
return (
<div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && (
@@ -11,6 +11,7 @@ query ReferralSetStats($code: ID!, $epoch: Int) {
rewardsMultiplier
rewardsFactorMultiplier
referrerTakerVolume
wasEligible
}
}
}
@@ -0,0 +1,10 @@
query StakeAvailable($partyId: ID!) {
party(id: $partyId) {
stakingSummary {
currentStakeAvailable
}
}
networkParameter(key: "referralProgram.minStakedVegaTokens") {
value
}
}
@@ -9,7 +9,7 @@ export type ReferralSetStatsQueryVariables = Types.Exact<{
}>;
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string, referrerTakerVolume: string } } | null> } };
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string, referrerTakerVolume: string, wasEligible: boolean } } | null> } };
export const ReferralSetStatsDocument = gql`
@@ -26,6 +26,7 @@ export const ReferralSetStatsDocument = gql`
rewardsMultiplier
rewardsFactorMultiplier
referrerTakerVolume
wasEligible
}
}
}
@@ -0,0 +1,53 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type StakeAvailableQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type StakeAvailableQuery = { __typename?: 'Query', party?: { __typename?: 'Party', stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string } } | null, networkParameter?: { __typename?: 'NetworkParameter', value: string } | null };
export const StakeAvailableDocument = gql`
query StakeAvailable($partyId: ID!) {
party(id: $partyId) {
stakingSummary {
currentStakeAvailable
}
}
networkParameter(key: "referralProgram.minStakedVegaTokens") {
value
}
}
`;
/**
* __useStakeAvailableQuery__
*
* To run a query within a React component, call `useStakeAvailableQuery` and pass it any options that fit your needs.
* When your component renders, `useStakeAvailableQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useStakeAvailableQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function useStakeAvailableQuery(baseOptions: Apollo.QueryHookOptions<StakeAvailableQuery, StakeAvailableQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<StakeAvailableQuery, StakeAvailableQueryVariables>(StakeAvailableDocument, options);
}
export function useStakeAvailableLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<StakeAvailableQuery, StakeAvailableQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<StakeAvailableQuery, StakeAvailableQueryVariables>(StakeAvailableDocument, options);
}
export type StakeAvailableQueryHookResult = ReturnType<typeof useStakeAvailableQuery>;
export type StakeAvailableLazyQueryHookResult = ReturnType<typeof useStakeAvailableLazyQuery>;
export type StakeAvailableQueryResult = Apollo.QueryResult<StakeAvailableQuery, StakeAvailableQueryVariables>;
@@ -75,22 +75,20 @@ export const useReferralProgram = () => {
const benefitTiers = sortBy(data.currentReferralProgram.benefitTiers, (t) =>
Number(t.referralRewardFactor)
)
.reverse()
.map((t, i) => {
return {
tier: i + 1,
rewardFactor: Number(t.referralRewardFactor),
commission: Number(t.referralRewardFactor) * 100 + '%',
discountFactor: Number(t.referralDiscountFactor),
discount: Number(t.referralDiscountFactor) * 100 + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: getNumberFormat(0).format(
Number(t.minimumRunningNotionalTakerVolume)
),
epochs: Number(t.minimumEpochs),
};
});
).map((t, i) => {
return {
tier: i + 1, // sorted in asc order, hence first is the lowest tier
rewardFactor: Number(t.referralRewardFactor),
commission: Number(t.referralRewardFactor) * 100 + '%',
discountFactor: Number(t.referralDiscountFactor),
discount: Number(t.referralDiscountFactor) * 100 + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: getNumberFormat(0).format(
Number(t.minimumRunningNotionalTakerVolume)
),
epochs: Number(t.minimumEpochs),
};
});
const stakingTiers = sortBy(
data.currentReferralProgram.stakingTiers,
@@ -0,0 +1,111 @@
import {
Intent,
type Toast,
useToasts,
ToastHeading,
Button,
} from '@vegaprotocol/ui-toolkit';
import { useReferral } from './use-referral';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useEffect } from 'react';
import { useT } from '../../../lib/use-t';
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
import { Routes } from '../../../lib/links';
import { useCurrentEpochInfoQuery } from './__generated__/Epoch';
const REFETCH_INTERVAL = 60 * 60 * 1000; // 1h
const NON_ELIGIBLE_REFERRAL_SET_TOAST_ID = 'non-eligible-referral-set';
const useNonEligibleReferralSet = () => {
const { pubKey } = useVegaWallet();
const { data, loading, refetch } = useReferral({ pubKey, role: 'referee' });
const {
data: epochData,
loading: epochLoading,
refetch: epochRefetch,
} = useCurrentEpochInfoQuery();
useEffect(() => {
const interval = setInterval(() => {
refetch();
epochRefetch();
}, REFETCH_INTERVAL);
return () => {
clearInterval(interval);
};
}, [epochRefetch, refetch]);
return { data, epoch: epochData?.epoch.id, loading: loading || epochLoading };
};
export const useReferralToasts = () => {
const navigate = useNavigate();
const { pathname } = useLocation();
const t = useT();
const [setToast, hasToast, updateToast] = useToasts((store) => [
store.setToast,
store.hasToast,
store.update,
]);
const { data, epoch, loading } = useNonEligibleReferralSet();
useEffect(() => {
if (
data &&
epoch &&
!loading &&
!data.isEligible &&
!hasToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch)
) {
const nonEligibleReferralToast: Toast = {
id: NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch,
intent: Intent.Warning,
content: (
<>
<ToastHeading>{t('Referral code no longer valid')}</ToastHeading>
<p>
{t(
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements.'
)}
</p>
<p className="mt-2">
<Button
data-testid="toast-apply-code"
size="xs"
onClick={() => {
const matched = matchPath(
Routes.REFERRALS_APPLY_CODE,
pathname
);
if (!matched) navigate(Routes.REFERRALS_APPLY_CODE);
updateToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch, {
hidden: true,
});
}}
>
{t('Apply a new code')}
</Button>
</p>
</>
),
onClose: () =>
updateToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch, {
hidden: true,
}),
};
setToast(nonEligibleReferralToast);
}
}, [
data,
epoch,
hasToast,
loading,
navigate,
pathname,
setToast,
t,
updateToast,
]);
};
@@ -4,6 +4,7 @@ import { useRefereesQuery } from './__generated__/Referees';
import compact from 'lodash/compact';
import type { ReferralSetsQueryVariables } from './__generated__/ReferralSets';
import { useReferralSetsQuery } from './__generated__/ReferralSets';
import { useStakeAvailable } from './use-stake-available';
export const DEFAULT_AGGREGATION_DAYS = 30;
@@ -62,6 +63,8 @@ export const useReferral = (args: UseReferralArgs) => {
? referralData.referralSets.edges[0]?.node
: undefined;
const { isEligible } = useStakeAvailable(referralSet?.referrer);
const {
data: refereesData,
loading: refereesLoading,
@@ -103,6 +106,7 @@ export const useReferral = (args: UseReferralArgs) => {
referee: referee,
referrerId: referralSet.referrer,
createdAt: referralSet.createdAt,
isEligible,
referees,
}
: undefined;
@@ -1,34 +1,35 @@
import { gql, useQuery } from '@apollo/client';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useStakeAvailableQuery } from './__generated__/StakeAvailable';
const STAKE_QUERY = gql`
query CreateCode($partyId: ID!) {
party(id: $partyId) {
stakingSummary {
currentStakeAvailable
}
}
networkParameter(key: "referralProgram.minStakedVegaTokens") {
value
}
}
`;
export const useStakeAvailable = () => {
const { pubKey } = useVegaWallet();
const { data } = useQuery(STAKE_QUERY, {
variables: { partyId: pubKey || '' },
skip: !pubKey,
/**
* Gets the current stake available for given public key and required stake for
* the referral program.
*
* (Uses currently connected public key if left empty)
*/
export const useStakeAvailable = (pubKey?: string) => {
const { pubKey: currentPubKey } = useVegaWallet();
const partyId = pubKey || currentPubKey;
const { data } = useStakeAvailableQuery({
variables: { partyId: partyId || '' },
skip: !partyId,
// TODO: remove when network params available
errorPolicy: 'ignore',
});
const stakeAvailable = data
? BigInt(data.party?.stakingSummary.currentStakeAvailable || '0')
: undefined;
const requiredStake = data
? BigInt(data.networkParameter?.value || '0')
: undefined;
return {
stakeAvailable: data
? BigInt(data.party?.stakingSummary.currentStakeAvailable || '0')
: undefined,
requiredStake: data
? BigInt(data.networkParameter?.value || '0')
: undefined,
stakeAvailable,
requiredStake,
isEligible:
stakeAvailable != null &&
requiredStake != null &&
stakeAvailable >= requiredStake,
};
};
@@ -0,0 +1,374 @@
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
import { render, waitFor } from '@testing-library/react';
import { type VegaWalletContextShape } from '@vegaprotocol/wallet';
import { ReferralStatistics } from './referral-statistics';
import {
ReferralProgramDocument,
type ReferralProgramQuery,
} from './hooks/__generated__/CurrentReferralProgram';
import {
ReferralSetsDocument,
type ReferralSetsQueryVariables,
type ReferralSetsQuery,
} from './hooks/__generated__/ReferralSets';
import {
StakeAvailableDocument,
type StakeAvailableQueryVariables,
type StakeAvailableQuery,
} from './hooks/__generated__/StakeAvailable';
import {
RefereesDocument,
type RefereesQueryVariables,
type RefereesQuery,
} from './hooks/__generated__/Referees';
import { MemoryRouter } from 'react-router-dom';
const MOCK_PUBKEY =
'1234567890123456789012345678901234567890123456789012345678901234';
const MOCK_STAKE_AVAILABLE: StakeAvailableQuery = {
networkParameter: {
__typename: 'NetworkParameter',
value: '1',
},
party: {
__typename: 'Party',
stakingSummary: {
__typename: 'StakingSummary',
currentStakeAvailable: '1',
},
},
};
const MOCK_NON_ELIGIBILE_STAKE_AVAILABLE: StakeAvailableQuery = {
networkParameter: {
__typename: 'NetworkParameter',
value: '1',
},
party: {
__typename: 'Party',
stakingSummary: {
__typename: 'StakingSummary',
currentStakeAvailable: '0',
},
},
};
const MOCK_REFERRAL_PROGRAM: ReferralProgramQuery = {
currentReferralProgram: {
__typename: 'CurrentReferralProgram',
benefitTiers: [
{
__typename: 'BenefitTier',
minimumEpochs: 1,
minimumRunningNotionalTakerVolume: '0',
referralDiscountFactor: '0.01',
referralRewardFactor: '0.01',
},
{
__typename: 'BenefitTier',
minimumEpochs: 2,
minimumRunningNotionalTakerVolume: '10',
referralDiscountFactor: '0.02',
referralRewardFactor: '0.02',
},
],
endOfProgramTimestamp: '202411012023-11-26T05:58:24.045158Z',
id: '123',
stakingTiers: [
{
__typename: 'StakingTier',
minimumStakedTokens: '100',
referralRewardMultiplier: '1',
},
{
__typename: 'StakingTier',
minimumStakedTokens: '1000',
referralRewardMultiplier: '2',
},
],
version: 2,
windowLength: 3,
endedAt: null,
},
};
const MOCK_REFERRER_SET: ReferralSetsQuery = {
referralSets: {
__typename: 'ReferralSetConnection',
edges: [
{
__typename: 'ReferralSetEdge',
node: {
__typename: 'ReferralSet',
createdAt: '2023-11-26T05:58:24.045158Z',
id: '3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
referrer: MOCK_PUBKEY,
updatedAt: '2023-11-26T05:58:24.045158Z',
},
},
],
},
};
const MOCK_REFERREE_SET: ReferralSetsQuery = {
referralSets: {
__typename: 'ReferralSetConnection',
edges: [
{
__typename: 'ReferralSetEdge',
node: {
__typename: 'ReferralSet',
createdAt: '2023-11-26T05:58:24.045158Z',
id: '3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
referrer:
'1111111111111111111111111111111111111111111111111111111111111111',
updatedAt: '2023-11-26T05:58:24.045158Z',
},
},
],
},
};
const MOCK_REFEREES: RefereesQuery = {
referralSetReferees: {
__typename: 'ReferralSetRefereeConnection',
edges: [
{
node: {
atEpoch: 1,
joinedAt: '2023-11-21T14:17:09.257235Z',
refereeId:
'0987654321098765432109876543210987654321098765432109876543219876',
referralSetId:
'3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
totalRefereeGeneratedRewards: '1234',
totalRefereeNotionalTakerVolume: '5678',
__typename: 'ReferralSetReferee',
},
},
],
},
};
const programMock: MockedResponse<ReferralProgramQuery> = {
request: {
query: ReferralProgramDocument,
},
result: { data: MOCK_REFERRAL_PROGRAM },
};
const referralSetAsReferrerMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referrer: MOCK_PUBKEY,
},
},
result: {
data: MOCK_REFERRER_SET,
},
};
const noReferralSetAsReferrerMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referrer: MOCK_PUBKEY,
},
},
result: {
data: { referralSets: { edges: [] } },
},
};
const referralSetAsRefereeMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referee: MOCK_PUBKEY,
},
},
result: {
data: MOCK_REFERREE_SET,
},
};
const noReferralSetAsRefereeMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referee: MOCK_PUBKEY,
},
},
result: {
data: { referralSets: { edges: [] } },
},
};
const stakeAvailableMock: MockedResponse<
StakeAvailableQuery,
StakeAvailableQueryVariables
> = {
request: {
query: StakeAvailableDocument,
variables: {
partyId: MOCK_PUBKEY,
},
},
result: {
data: MOCK_STAKE_AVAILABLE,
},
};
const nonEligibleStakeAvailableMock: MockedResponse<
StakeAvailableQuery,
StakeAvailableQueryVariables
> = {
request: {
query: StakeAvailableDocument,
variables: {
partyId: MOCK_PUBKEY,
},
},
result: {
data: MOCK_NON_ELIGIBILE_STAKE_AVAILABLE,
},
};
const refereesMock: MockedResponse<RefereesQuery, RefereesQueryVariables> = {
request: {
query: RefereesDocument,
variables: {
code: MOCK_REFERRER_SET.referralSets.edges[0]?.node.id as string,
aggregationEpochs:
MOCK_REFERRAL_PROGRAM.currentReferralProgram?.windowLength,
},
},
result: {
data: MOCK_REFEREES,
},
};
jest.mock('@vegaprotocol/wallet', () => {
return {
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: () => {
const ctx: Partial<VegaWalletContextShape> = {
pubKey: MOCK_PUBKEY,
};
return ctx;
},
};
});
describe('ReferralStatistics', () => {
it('displays create code when no data has been found for given pubkey', () => {
const { queryByTestId } = render(
<MockedProvider mocks={[]} showWarnings={false}>
<ReferralStatistics />
</MockedProvider>
);
expect(queryByTestId('referral-create-code-form')).toBeInTheDocument();
});
it('displays referrer stats when given pubkey is a referrer', async () => {
const { queryByTestId } = render(
<MockedProvider
mocks={[
programMock,
referralSetAsReferrerMock,
noReferralSetAsRefereeMock,
stakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
);
await waitFor(() => {
expect(
queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referrer'
);
});
});
it('displays referee stats when given pubkey is a referee', async () => {
const { queryByTestId } = render(
<MemoryRouter>
<MockedProvider
mocks={[
programMock,
noReferralSetAsReferrerMock,
referralSetAsRefereeMock,
stakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
await waitFor(() => {
expect(
queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee'
);
});
});
it('displays eligibility warning when the set is no longer valid due to the referrers stake', async () => {
const { queryByTestId } = render(
<MemoryRouter>
<MockedProvider
mocks={[
programMock,
noReferralSetAsReferrerMock,
referralSetAsRefereeMock,
nonEligibleStakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
await waitFor(() => {
expect(
queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee'
);
expect(queryByTestId('referral-eligibility-warning')).toBeInTheDocument();
expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument();
});
});
});
@@ -1,3 +1,4 @@
import minBy from 'lodash/minBy';
import { CodeTile, StatTile } from './tile';
import {
VegaIcon,
@@ -28,10 +29,10 @@ import sortBy from 'lodash/sortBy';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
import BigNumber from 'bignumber.js';
import maxBy from 'lodash/maxBy';
import { DocsLinks } from '@vegaprotocol/environment';
import { useT, ns } from '../../lib/use-t';
import { Trans } from 'react-i18next';
import { ApplyCodeForm } from './apply-code-form';
export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet();
@@ -50,11 +51,21 @@ export const ReferralStatistics = () => {
});
if (referee?.code) {
return <Statistics data={referee} program={program} as="referee" />;
return (
<>
<Statistics data={referee} program={program} as="referee" />;
{!referee.isEligible && <ApplyCodeForm />}
</>
);
}
if (referrer?.code) {
return <Statistics data={referrer} program={program} as="referrer" />;
return (
<>
<Statistics data={referrer} program={program} as="referrer" />;
<RefereesTable data={referrer} program={program} />
</>
);
}
return <CreateCodeContainer />;
@@ -117,7 +128,7 @@ export const useStats = ({
);
const nextBenefitTierValue = currentBenefitTierValue
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1)
: maxBy(benefitTiers, (bt) => bt.tier); // max tier number is lowest tier
: minBy(benefitTiers, (bt) => bt.tier); // min tier number is lowest tier
const epochsValue =
!isNaN(currentEpoch) && refereeInfo?.atEpoch
? currentEpoch - refereeInfo?.atEpoch
@@ -174,7 +185,7 @@ export const Statistics = ({
const { benefitTiers } = useReferralProgram();
const { stakeAvailable } = useStakeAvailable();
const { stakeAvailable, isEligible } = useStakeAvailable();
const { details } = program;
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
@@ -200,12 +211,24 @@ export const Statistics = ({
{baseCommissionValue * 100}%
</StatTile>
);
const stakingMultiplierTile = (
<StatTile
title={t('Staking multiplier')}
description={t('{{amount}} $VEGA staked', {
amount: addDecimalsFormatNumber(stakeAvailable?.toString() || 0, 18),
})}
description={
<span
className={classNames({
'text-vega-red': !isEligible,
})}
>
{t('{{amount}} $VEGA staked', {
amount: addDecimalsFormatNumber(
stakeAvailable?.toString() || 0,
18
),
})}
</span>
}
>
{multiplier || t('None')}
</StatTile>
@@ -238,7 +261,7 @@ export const Statistics = ({
const referrerVolumeTile = (
<StatTile
title={t('My volume (last {{count}} epochs)', {
title={t('myVolume', 'My volume (last {{count}} epochs)', {
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
})}
>
@@ -251,7 +274,7 @@ export const Statistics = ({
.reduce((all, r) => all.plus(r), new BigNumber(0));
const totalCommissionTile = (
<StatTile
title={t('Total commission (last {{count}}} epochs)', {
title={t('totalCommission', 'Total commission (last {{count}}} epochs)', {
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
})}
description={<QUSDTooltip />}
@@ -294,9 +317,13 @@ export const Statistics = ({
);
const runningVolumeTile = (
<StatTile
title={t('Combined volume (last {{count}} epochs)', {
count: details?.windowLength,
})}
title={t(
'runningNotionalOverEpochs',
'Combined volume (last {{count}} epochs)',
{
count: details?.windowLength,
}
)}
>
{compactNumFormat.format(runningVolumeValue)}
</StatTile>
@@ -333,28 +360,60 @@ export const Statistics = ({
</>
);
const [collapsed, setCollapsed] = useState(false);
const tableRef = useRef<HTMLTableElement>(null);
useLayoutEffect(() => {
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
setCollapsed(true);
}
}, []);
const eligibilityWarning = as === 'referee' && !isEligible && (
<div
data-testid="referral-eligibility-warning"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
>
<h2 className="text-2xl mb-2">{t('Referral code no longer valid')}</h2>
<p>
{t(
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
)}
</p>
</div>
);
return (
<>
{/* Stats tiles */}
<div
data-testid="referral-statistics"
data-as={as}
className="relative mx-auto mb-20"
>
<div
className={classNames(
'grid grid-cols-1 grid-rows-1 gap-5 mx-auto mb-20'
)}
className={classNames('grid grid-cols-1 grid-rows-1 gap-5', {
'opacity-20 pointer-events-none': as === 'referee' && !isEligible,
})}
>
{as === 'referrer' && referrerTiles}
{as === 'referee' && refereeTiles}
</div>
{eligibilityWarning}
</div>
);
};
export const RefereesTable = ({
data,
program,
}: {
data: NonNullable<ReturnType<typeof useReferral>['data']>;
program: ReturnType<typeof useReferralProgram>;
}) => {
const t = useT();
const [collapsed, setCollapsed] = useState(false);
const tableRef = useRef<HTMLTableElement>(null);
const { details } = program;
useLayoutEffect(() => {
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
setCollapsed(true);
}
}, []);
return (
<>
{/* Referees (only for referrer view) */}
{as === 'referrer' && data.referees.length > 0 && (
{data.referees.length > 0 && (
<div className="mt-20 mb-20">
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
<div
@@ -384,15 +443,19 @@ export const Statistics = ({
{ name: 'joined', displayName: t('Date Joined') },
{
name: 'volume',
displayName: t('Volume (last {{count}} epochs)', {
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
}),
displayName: t(
'volumeLastEpochs',
'Volume (last {{count}} epochs)',
{
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
}
),
},
{
name: 'commission',
displayName: (
<Trans
i18nKey="referral-statistics-commission"
i18nKey="referralStatisticsCommission"
defaults="Commission earned in <0>qUSD</0> (last {{count}} epochs)"
values={{
count:
+27 -9
View File
@@ -6,7 +6,12 @@ import { BORDER_COLOR, GRADIENT } from './constants';
import { Tag } from './tag';
import type { ComponentProps, ReactNode } from 'react';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { DApp, TOKEN_PROPOSALS, useLinks } from '@vegaprotocol/environment';
import {
DApp,
DocsLinks,
TOKEN_PROPOSALS,
useLinks,
} from '@vegaprotocol/environment';
import { useT, ns } from '../../lib/use-t';
import { Trans } from 'react-i18next';
@@ -88,10 +93,19 @@ export const TiersContainer = () => {
return (
<div className="text-base px-5 py-10 text-center">
<Trans
defaults="We're sorry but we don't have an active referral programme currently running. You can propose a new programme <0>here</0>."
defaults="There are currently no active referral programs. Check the <0>Governance App</0> to see if there are any proposals in progress and vote."
components={[
<ExternalLink href={governanceLink(TOKEN_PROPOSALS)} key="link">
{t('here')}
{t('Governance App')}
</ExternalLink>,
]}
ns={ns}
/>
<Trans
defaults="You can propose a new program via the <0>Docs</0>."
components={[
<ExternalLink href={DocsLinks?.REFERRALS} key="link">
{t('Docs')}
</ExternalLink>,
]}
ns={ns}
@@ -194,9 +208,13 @@ const TiersTable = ({
{ name: 'discount', displayName: t('Referrer trading discount') },
{
name: 'volume',
displayName: t('Min. trading volume (last {{count}} epochs)', {
count: windowLength,
}),
displayName: t(
'minTradingVolume',
'Min. trading volume (last {{count}} epochs)',
{
count: windowLength,
}
),
},
{ name: 'epochs', displayName: t('Min. epochs') },
]}
@@ -204,13 +222,13 @@ const TiersTable = ({
...d,
className: classNames({
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
d.tier === 1,
d.tier >= 3,
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
d.tier === 2,
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
d.tier === 3,
d.tier === 1,
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
d.tier > 3,
d.tier == 0,
}),
}))}
/>
+11 -1
View File
@@ -1,11 +1,21 @@
import { useT } from '../../lib/use-t';
import { RewardsContainer } from '../../components/rewards-container';
import { usePageTitleStore } from '../../stores';
import { titlefy } from '@vegaprotocol/utils';
import { useEffect } from 'react';
export const Rewards = () => {
const t = useT();
const title = t('Rewards');
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([title]));
}, [updateTitle, title]);
return (
<div className="container mx-auto p-4">
<h1 className="px-4 pb-4 text-2xl">{t('Rewards')}</h1>
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
<RewardsContainer />
</div>
);
@@ -36,7 +36,7 @@ export const FeesContainer = () => {
const { data: markets, loading: marketsLoading } = useMarketList();
const { data: programData, loading: programLoading } =
useDiscountProgramsQuery();
useDiscountProgramsQuery({ errorPolicy: 'ignore' });
const volumeDiscountWindowLength =
programData?.currentVolumeDiscountProgram?.windowLength || 1;
@@ -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 = tiers.length - 1 - tierIndex === i;
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>
{formatPercentage(Number(tier.volumeDiscountFactor))}%
</Td>
<Td>
{formatNumber(tier.minimumRunningNotionalTakerVolume)}
</Td>
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
<Td>{isUserTier ? <YourTier /> : null}</Td>
</Tr>
);
})}
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 = tiers.length - 1 - 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>
@@ -65,7 +65,7 @@ export const FundingContainer = ({ marketId }: { marketId: string }) => {
if (edge.node.endTime) {
acc?.push({
endTime: fromNanoSeconds(edge.node.endTime),
fundingRate: Number(edge.node.fundingRate) * 100,
fundingRate: Number(edge.node.fundingRate),
});
}
return acc;
@@ -82,7 +82,8 @@ export const FundingContainer = ({ marketId }: { marketId: string }) => {
<LineChart
data={values}
theme={theme}
priceFormat={(fundingRate) => `${fundingRate.toFixed(4)}%`}
priceFormat={(fundingRate) => `${(fundingRate * 100).toFixed(4)}%`}
yAxisTickFormat="%"
/>
);
};
@@ -16,15 +16,12 @@ export const LedgerContainer = () => {
});
const assets = (data?.party?.accountsConnection?.edges ?? [])
.map<PartyAssetFieldsFragment>(
(item) => item?.node?.asset ?? ({} as PartyAssetFieldsFragment)
)
.reduce((aggr, item) => {
if ('id' in item && 'symbol' in item) {
aggr[item.id as string] = item.symbol as string;
}
return aggr;
}, {} as Record<string, string>);
.map((item) => item?.node?.asset)
.filter((asset): asset is PartyAssetFieldsFragment => !!asset?.id)
.reduce(
(aggr, item) => Object.assign(aggr, { [item.id]: item.symbol }),
{} as Record<string, string>
);
if (!pubKey) {
return (
@@ -172,4 +172,20 @@ describe('Navbar', () => {
expect(mockDisconnect).toHaveBeenCalled();
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
});
it('does not render the language selector until we have more languages', () => {
renderComponent();
expect(screen.queryByTestId('icon-globe')).not.toBeInTheDocument();
});
it('renders the theme switcher', async () => {
renderComponent();
await userEvent.click(screen.getByTestId('icon-moon'));
expect(screen.queryByTestId('icon-moon')).not.toBeInTheDocument();
expect(screen.getByTestId('icon-sun')).toBeInTheDocument();
await userEvent.click(screen.getByTestId('icon-sun'));
expect(screen.queryByTestId('icon-sun')).not.toBeInTheDocument();
expect(screen.getByTestId('icon-moon')).toBeInTheDocument();
});
});
+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) {
@@ -0,0 +1,12 @@
import { Settings } from './settings';
import { render, screen } from '@testing-library/react';
describe('Settings', () => {
it('should the settings component with all the options', () => {
render(<Settings />);
expect(screen.getByText('Dark mode')).toBeInTheDocument();
expect(screen.getByText('Share usage data')).toBeInTheDocument();
expect(screen.getByText('Toast location')).toBeInTheDocument();
expect(screen.getByText('Reset to default')).toBeInTheDocument();
});
});
+55 -2
View File
@@ -1,7 +1,13 @@
import { Switch, ToastPositionSetter } from '@vegaprotocol/ui-toolkit';
import {
Dialog,
Intent,
Switch,
ToastPositionSetter,
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
import type { ReactNode } from 'react';
import { useState, type ReactNode } from 'react';
import classNames from 'classnames';
import { useT } from '../../lib/use-t';
@@ -9,6 +15,7 @@ export const Settings = () => {
const t = useT();
const { theme, setTheme } = useThemeSwitcher();
const [isApproved, setIsApproved] = useTelemetryApproval();
const [open, setOpen] = useState(false);
return (
<div>
<SettingsGroup label={t('Dark mode')}>
@@ -33,6 +40,52 @@ export const Settings = () => {
<SettingsGroup label={t('Toast location')}>
<ToastPositionSetter />
</SettingsGroup>
<SettingsGroup label={t('Reset to default')}>
<TradingButton
name="reset-to-defaults"
size="small"
intent={Intent.None}
onClick={() => {
setOpen(true);
}}
>
{t('Reset')}
</TradingButton>
<Dialog open={open} title={t('Reset')}>
<div className="mb-4">
<p>
{t(
'You will lose all persisted settings and you will be logged out.'
)}
</p>
<p>
{t('Are you sure you want to reset all settings to default?')}
</p>
</div>
<div className="flex flex-col gap-4">
<TradingButton
name="reset-to-defaults-cancel"
intent={Intent.Primary}
onClick={() => {
localStorage.clear();
window.location.reload();
}}
>
{t('Yes, clear cache and refresh')}
</TradingButton>
<TradingButton
name="reset-to-defaults-cancel"
intent={Intent.None}
onClick={() => {
setOpen(false);
}}
>
{t('No, keep settings')}
</TradingButton>
</div>
</Dialog>
</SettingsGroup>
<SettingsGroup inline={false} label={t('App information')}>
<dl className="text-sm grid grid-cols-2 gap-1">
{process.env.GIT_TAG && (
+1 -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
View File
@@ -44,4 +44,5 @@ def truncate_middle(market_id, start=6, end=4):
def change_keys(page: Page, vega:VegaServiceNull, key_name):
page.get_by_test_id("manage-vega-wallet").click()
page.get_by_test_id("key-" + vega.wallet.public_key(key_name)).click()
page.click(f'data-testid=key-{vega.wallet.public_key(key_name)} >> .inline-flex')
page.reload()
+5 -2
View File
@@ -136,8 +136,11 @@ def init_page(vega: VegaServiceNull, browser: Browser, request: pytest.FixtureRe
page.add_init_script(script=window_env)
yield page
finally:
if not os.path.exists("traces"):
os.makedirs("traces")
try:
if not os.path.exists("apps/trading/e2e/traces"):
os.makedirs("apps/trading/e2e/traces")
except OSError as e:
print(f"Failed to create directory '{'apps/trading/e2e/traces'}': {e}")
# Check whether this test failed or passed
outcome = request.config.cache.get(request.node.nodeid, None)
@@ -1,12 +0,0 @@
import pytest
from playwright.sync_api import expect, Page
@pytest.mark.usefixtures("page", "continuous_market", "risk_accepted")
def test_see_market_depth_chart(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
# Click on the 'Depth' tab
page.get_by_test_id("Depth").click()
# Check if the 'Depth' tab and the depth chart are visible
# 6006-DEPC-001
expect(page.get_by_test_id("tab-depth")).to_be_visible()
expect(page.locator('[class^="depth-chart-module_canvas__"]').first).to_be_visible()
@@ -29,11 +29,12 @@ def continuous_market(vega):
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_limit_buy_order_GTT(continuous_market, vega: VegaService, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(tif).select_option("Good 'til Time (GTT)")
page.get_by_test_id(order_size).fill("10")
page.get_by_test_id(order_price).fill("120")
page.get_by_test_id(tif).select_option("Good 'til Time (GTT)")
expires_at = datetime.now() + timedelta(days=1)
expires_at_input_value = expires_at.strftime("%Y-%m-%dT%H:%M:%S")
page.get_by_test_id("date-picker-field").clear()
page.get_by_test_id("date-picker-field").fill(expires_at_input_value)
# 7002-SORD-011
expect(page.get_by_test_id("place-order").locator("span").first).to_have_text(
@@ -2,10 +2,8 @@ import pytest
from playwright.sync_api import Page, expect
from vega_sim.service import VegaService
from actions.vega import submit_order
from conftest import init_vega
from fixtures.market import setup_continuous_market
from actions.utils import wait_for_toast_confirmation
from wallet_config import MM_WALLET, MM_WALLET2, TERMINATE_WALLET, wallets
stop_order_btn = "order-type-Stop"
stop_limit_order_btn = "order-type-StopLimit"
@@ -328,98 +326,4 @@ def test_submit_stop_oco_limit_order_cancel(
page.locator(".ag-center-cols-container").locator('[col-id="status"]').last
).to_have_text("CancelledOCO")
class TestStopOcoValidation:
@pytest.fixture(scope="class")
def vega(self, request):
with init_vega(request) as vega:
yield vega
@pytest.fixture(scope="class")
def continuous_market(self, vega):
return setup_continuous_market(vega)
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_stop_market_order_oco_form_validation(self, continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(stop_order_btn).click()
page.get_by_test_id(stop_market_order_btn).is_visible()
page.get_by_test_id(stop_market_order_btn).click()
page.get_by_test_id(oco).click()
expect(
page.get_by_test_id("sidebar-content").get_by_text("Trigger").last
).to_be_visible()
# 7002-SORD-084
expect(page.locator('[for="triggerDirection-risesAbove-oco"]')).to_have_text(
"Rises above"
)
# 7002-SORD-085
expect(page.locator('[for="triggerDirection-fallsBelow-oco"]')).to_have_text(
"Falls below"
)
# 7002-SORD-087
expect(page.locator('[for="triggerType-price-oco"]')).to_have_text("Price")
expect(page.locator('[for="triggerType-price"]')).to_be_checked
# 7002-SORD-088
expect(
page.locator('[for="triggerType-trailingPercentOffset-oco"]')
).to_have_text("Trailing Percent Offset")
expect(page.locator('[for="order-size-oco"]')).to_have_text("Size")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_stop_limit_order_oco_form_validation(self, continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(stop_order_btn).click()
page.get_by_test_id(stop_market_order_btn).is_visible()
page.get_by_test_id(stop_limit_order_btn).click()
page.get_by_test_id(oco).click()
expect(
page.get_by_test_id("sidebar-content").get_by_text("Trigger").last
).to_be_visible()
# 7002-SORD-099
expect(page.locator('[for="triggerDirection-risesAbove-oco"]')).to_have_text(
"Rises above"
)
# 7002-SORD-091
expect(page.locator('[for="triggerDirection-fallsBelow-oco"]')).to_have_text(
"Falls below"
)
# 7002-SORD-095
expect(page.locator('[for="triggerType-price-oco"]')).to_have_text("Price")
expect(page.locator('[for="triggerType-price"]')).to_be_checked
# 7002-SORD-095
expect(
page.locator('[for="triggerType-trailingPercentOffset-oco"]')
).to_have_text("Trailing Percent Offset")
expect(page.locator('[for="order-size-oco"]')).to_have_text("Size")
expect(page.locator('[for="order-price-oco"]')).to_have_text("Price")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_maximum_number_of_active_stop_orders_oco(
self, continuous_market, vega: VegaService, page: Page
):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(stop_order_btn).click()
page.get_by_test_id(stop_limit_order_btn).is_visible()
page.get_by_test_id(stop_limit_order_btn).click()
page.get_by_test_id(order_side_sell).click()
page.locator("label").filter(has_text="Falls below").click()
page.get_by_test_id(trigger_price).fill("102")
page.get_by_test_id(order_size).fill("3")
page.get_by_test_id(order_price).fill("103")
page.get_by_test_id(oco).click()
page.get_by_test_id(trigger_price_oco).fill("120")
page.get_by_test_id(order_size_oco).fill("2")
page.get_by_test_id(order_limit_price_oco).fill("99")
for i in range(2):
page.get_by_test_id(submit_stop_order).click()
wait_for_toast_confirmation(page)
vega.wait_fn(1)
vega.forward("20s")
vega.wait_for_total_catchup()
if page.get_by_test_id(close_toast).is_visible():
page.get_by_test_id(close_toast).click()
# 7002-SORD-011
expect(page.get_by_test_id("stop-order-warning-limit")).to_have_text(
"There is a limit of 4 active stop orders per market. Orders submitted above the limit will be immediately rejected."
)
@@ -47,48 +47,6 @@ class TestIcebergOrdersValidations:
(page.get_by_role("row").locator('[col-id="type"]')).nth(1)
).to_have_text("Limit (Iceberg)")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_iceberg_tooltips(self, continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id("iceberg").hover()
expect(page.get_by_role("tooltip")).to_be_visible()
page.get_by_test_id("iceberg").click()
hover_and_assert_tooltip(page, "Peak size")
hover_and_assert_tooltip(page, "Minimum size")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_iceberg_validations(self, continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id("iceberg").click()
page.get_by_test_id("place-order").click()
expect(page.get_by_test_id("deal-ticket-peak-error-message")).to_be_visible()
expect(page.get_by_test_id("deal-ticket-peak-error-message")).to_have_text(
"You need to provide a peak size"
)
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_be_visible()
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_have_text(
"You need to provide a minimum visible size"
)
page.get_by_test_id("order-peak-size").clear()
page.get_by_test_id("order-peak-size").type("1")
page.get_by_test_id("order-minimum-size").clear()
page.get_by_test_id("order-minimum-size").type("2")
expect(page.get_by_test_id("deal-ticket-peak-error-message")).to_be_visible()
expect(page.get_by_test_id("deal-ticket-peak-error-message")).to_have_text(
"Peak size cannot be greater than the size (0)"
)
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_be_visible()
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_have_text(
"Minimum visible size cannot be greater than the peak size (1)"
)
page.get_by_test_id("order-minimum-size").clear()
page.get_by_test_id("order-minimum-size").type("0.1")
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_be_visible()
expect(page.get_by_test_id("deal-ticket-minimum-error-message")).to_have_text(
"Minimum visible size cannot be lower than 1"
)
@pytest.mark.usefixtures("vega", "page", "continuous_market", "auth", "risk_accepted")
def test_iceberg_open_order(continuous_market, vega: VegaService, page: Page):
page.goto(f"/#/markets/{continuous_market}")
@@ -1,4 +1,5 @@
import pytest
import re
import vega_sim.api.governance as governance
from playwright.sync_api import Page, expect
from vega_sim.service import VegaService, PeggedOrder
@@ -10,6 +11,8 @@ from wallet_config import MM_WALLET, MM_WALLET2, GOVERNANCE_WALLET
@pytest.mark.usefixtures("vega", "page", "proposed_market", "risk_accepted")
def test_market_lifecycle(proposed_market, vega: VegaService, page: Page):
# 7002-SORD-001
# 7002-SORD-002
trading_mode = page.get_by_test_id("market-trading-mode").get_by_test_id(
"item-value"
)
@@ -18,12 +21,32 @@ def test_market_lifecycle(proposed_market, vega: VegaService, page: Page):
# setup market in proposed step, without liquidity provided
market_id = proposed_market
page.goto(f"/#/markets/{market_id}")
# 6002-MDET-001
expect(page.get_by_test_id("header-title")).to_have_text("BTC:DAI_2023Futr")
# 6002-MDET-002
expect(page.get_by_test_id("market-expiry")).to_have_text("ExpiryNot time-based")
page.get_by_test_id("market-expiry").hover()
expect(page.get_by_test_id("expiry-tooltip").first).to_have_text("This market expires when triggered by its oracle, not on a set date.View oracle specification")
expect(page.get_by_test_id("expiry-tooltip").first.get_by_test_id("link")).to_have_attribute("href", re.compile('.*'))
# 6002-MDET-003
expect(page.get_by_test_id("market-price")).to_have_text("Mark Price0.00")
# 6002-MDET-004
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
# 6002-MDET-005
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
# 6002-MDET-008
expect(page.get_by_test_id("market-settlement-asset")).to_have_text("Settlement assettDAI")
expect(page.get_by_test_id("liquidity-supplied")).to_have_text("Liquidity supplied 0.00 (0.00%)")
page.get_by_test_id("liquidity-supplied").hover()
expect(page.get_by_test_id("liquidity-supplied-tooltip").first).to_have_text("Supplied stake0.00Target stake0.00View liquidity provision tableLearn about providing liquidity")
expect(page.get_by_test_id("liquidity-supplied-tooltip").first.get_by_test_id("link").first).to_have_text("View liquidity provision table")
# check that market is in proposed state
# 6002-MDET-006
# 6002-MDET-007
# 7002-SORD-061
expect(trading_mode).to_have_text("No trading")
trading_mode.hover()
expect(page.get_by_test_id("trading-mode-tooltip").first).to_have_text("No trading enabled for this market.")
expect(market_state).to_have_text("Proposed")
# approve market
@@ -1,4 +1,4 @@
import pytest
import pytest
import re
from playwright.sync_api import Page, expect
from vega_sim.service import VegaService
@@ -20,23 +20,23 @@ def test_transfer_submit(continuous_market, vega: VegaService, page: Page):
# 1003-TRAN-010
# 1003-TRAN-023
page.goto('/#/portfolio')
expect(page.get_by_test_id('transfer-form')).to_be_visible
page.get_by_test_id('select-asset').click()
expect(page.get_by_test_id('rich-select-option')).to_have_count(1)
page.get_by_test_id('rich-select-option').click()
page.select_option('[data-testid=transfer-form] [name="toVegaKey"]', index=2)
page.select_option('[data-testid=transfer-form] [name="fromAccount"]', index=1)
expected_asset_text = re.compile(r"tDAI tDAI999,991.49731 tDAI.{6}….{4}")
expected_asset_text = re.compile(r"tDAI tDAI999991.49731 tDAI.{6}….{4}")
actual_asset_text = page.get_by_test_id('select-asset').text_content().strip()
assert expected_asset_text.search(actual_asset_text), f"Expected pattern not found in {actual_asset_text}"
page.locator('[data-testid=transfer-form] input[name="amount"]').fill('1')
expect(page.locator('[data-testid=transfer-form] input[name="amount"]')).not_to_be_empty()
page.locator('[data-testid=transfer-form] [type="submit"]').click()
wait_for_toast_confirmation(page)
vega.forward("10s")
@@ -45,15 +45,15 @@ def test_transfer_submit(continuous_market, vega: VegaService, page: Page):
expected_confirmation_text = re.compile(r"Transfer completeYour transaction has been confirmedView in block explorerTransferTo .{6}….{6}1\.00 tDAI")
actual_confirmation_text = page.get_by_test_id('toast-content').text_content()
assert expected_confirmation_text.search(actual_confirmation_text), f"Expected pattern not found in {actual_confirmation_text}"
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_transfer_vesting_below_minimum(continuous_market, vega: VegaService, page: Page):
vega.update_network_parameter(
"market_maker", parameter="transfer.minTransferQuantumMultiple", new_value="100000"
"market_maker", parameter="transfer.minTransferQuantumMultiple", new_value="100000"
)
vega.wait_for_total_catchup()
create_and_faucet_wallet(vega=vega, wallet=PARTY_A, amount=1e3)
create_and_faucet_wallet(vega=vega, wallet=PARTY_B, amount=1e5)
create_and_faucet_wallet(vega=vega, wallet=PARTY_C, amount=1e5)
@@ -96,11 +96,11 @@ def test_transfer_vesting_below_minimum(continuous_market, vega: VegaService, pa
next_epoch(vega=vega)
page.goto('/#/portfolio')
expect(page.get_by_test_id('transfer-form')).to_be_visible
change_keys(page, vega, "party_b")
page.get_by_test_id('select-asset').click()
page.get_by_test_id('rich-select-option').click()
option_value = page.locator('[data-testid="transfer-form"] [name="fromAccount"] option[value^="ACCOUNT_TYPE_VESTED_REWARDS"]').first.get_attribute("value")
page.select_option('[data-testid="transfer-form"] [name="fromAccount"]', option_value)
@@ -120,7 +120,7 @@ def test_transfer_vesting_below_minimum(continuous_market, vega: VegaService, pa
vega.forward("10s")
vega.wait_fn(10)
vega.wait_for_total_catchup()
page.get_by_text("Use max").first.click()
page.locator('[data-testid=transfer-form] [type="submit"]').click()
wait_for_toast_confirmation(page)
@@ -129,4 +129,4 @@ def test_transfer_vesting_below_minimum(continuous_market, vega: VegaService, pa
vega.wait_for_total_catchup()
expected_confirmation_text = re.compile(r"Transfer completeYour transaction has been confirmedView in block explorerTransferTo .{6}….{6}0\.00001 tDAI")
actual_confirmation_text = page.get_by_test_id('toast-content').text_content()
assert expected_confirmation_text.search(actual_confirmation_text), f"Expected pattern not found in {actual_confirmation_text}"
assert expected_confirmation_text.search(actual_confirmation_text), f"Expected pattern not found in {actual_confirmation_text}"
@@ -0,0 +1,116 @@
import pytest
import re
import json
from playwright.sync_api import Page, expect, Route
from vega_sim.service import VegaService
from conftest import init_vega
from fixtures.market import setup_continuous_market
order_size = "order-size"
order_price = "order-price"
place_order = "place-order"
order_side_sell = "order-side-SIDE_SELL"
market_order = "order-type-Market"
tif = "order-tif"
expire = "expire"
api_request_match = r"http://localhost:\d+/api/v2/requests"
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega:
yield vega
@pytest.fixture(scope="module")
def continuous_market(vega):
return setup_continuous_market(vega)
def handle_route_connection_lost(route: Route, request):
if request.method == "POST" and re.match(api_request_match, request.url):
route.fulfill(
status=200,
headers={"Content-Type": "application/json"},
body='{"jsonrpc": "2.0", "id": "1"}'
)
else:
route.continue_()
def handle_route_connection_rejected(route: Route, request):
if request.method == "POST" and re.match(api_request_match, request.url):
custom_response = {
"jsonrpc": "2.0",
"error": {
"code": 3001,
"data": "the user rejected the wallet connection",
"message": "User error"
},
"id": "0"
}
route.fulfill(
status=400,
headers={"Content-Type": "application/json"},
body=json.dumps(custom_response)
)
else:
route.continue_()
def assert_connection_approve(route: Route, request, page:Page):
if request.method == "POST" and re.match(api_request_match, request.url):
expect(page.get_by_test_id("toast-content")).to_have_text("Please go to your Vega wallet application and approve or reject the transaction.")
else:
route.continue_()
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_wallet_connection_error(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.route("**/*", handle_route_connection_lost)
page.get_by_test_id("connect-vega-wallet").click()
page.get_by_test_id("connector-jsonRpc").click()
expect(page.get_by_test_id("wallet-dialog-title")).to_have_text("Something went wrong")
@pytest.mark.usefixtures("page", "risk_accepted")
def test_wallet_connection_rejected(continuous_market, page: Page):
# 0002-WCON-002
# 0002-WCON-005
# 0002-WCON-007
# 0002-WCON-015
page.goto(f"/#/markets/{continuous_market}")
page.route("**/*", handle_route_connection_rejected)
page.get_by_test_id("connect-vega-wallet").click()
page.get_by_test_id("connector-jsonRpc").click()
expect(page.get_by_test_id("dialog-content").nth(1)).to_have_text("User errorthe user rejected the wallet connectionTry againAbout the Vega wallet | Supported browsers ")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_wallet_connection_error_transaction(continuous_market, vega: VegaService, page: Page):
# 0003-WTXN-009
# 0003-WTXN-011
# 0002-WCON-016
# 0003-WTXN-008
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(order_size).fill("10")
page.get_by_test_id(order_price).fill("120")
page.route("**/*", handle_route_connection_lost)
page.get_by_test_id(place_order).click()
expect(page.get_by_test_id("toast-content")).to_have_text("Wallet disconnectedThe connection to your Vega Wallet has been lost.Connect vega wallet")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_wallet_transaction_rejected(continuous_market, vega: VegaService, page: Page):
# 0003-WTXN-007
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(order_size).fill("10")
page.get_by_test_id(order_price).fill("120")
page.route("**/*", handle_route_connection_rejected)
page.get_by_test_id(place_order).click()
expect(page.get_by_test_id("toast-content")).to_have_text("Error occurredthe user rejected the wallet connection")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_wallet_connection_approve(continuous_market, vega: VegaService, page: Page):
# 0002-WCON-005
# 0002-WCON-007
# 0002-WCON-009
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(order_size).fill("10")
page.get_by_test_id(order_price).fill("120")
page.route("**/*", assert_connection_approve)
page.get_by_test_id(place_order).click()
+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 = () => {
+2 -2
View File
@@ -18,7 +18,7 @@ import { Routes as AppRoutes } from '../lib/links';
import { LayoutWithSky } from '../client-pages/referrals/layout';
import { Referrals } from '../client-pages/referrals/referrals';
import { ReferralStatistics } from '../client-pages/referrals/referral-statistics';
import { ApplyCodeForm } from '../client-pages/referrals/apply-code-form';
import { ApplyCodeFormContainer } from '../client-pages/referrals/apply-code-form';
import { CreateCodeContainer } from '../client-pages/referrals/create-code-form';
import { NotFound as ReferralNotFound } from '../client-pages/referrals/error-boundary';
import { compact } from 'lodash';
@@ -79,7 +79,7 @@ export const routerConfig: RouteObject[] = compact([
},
{
path: AppRoutes.REFERRALS_APPLY_CODE,
element: <ApplyCodeForm />,
element: <ApplyCodeFormContainer />,
},
],
},
+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;
+2
View File
@@ -5,6 +5,7 @@ import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
import { Links } from '../lib/links';
import { useReferralToasts } from '../client-pages/referrals/hooks/use-referral-toasts';
export const ToastsManager = () => {
useProposalToasts();
@@ -14,6 +15,7 @@ export const ToastsManager = () => {
useReadyToWithdrawalToasts({
withdrawalsLink: Links.PORTFOLIO(),
});
useReferralToasts();
const toasts = useToasts((store) => store.toasts);
return <ToastsContainer order="desc" toasts={toasts} />;
+28 -40
View File
@@ -1,10 +1,23 @@
import { act, render, screen } from '@testing-library/react';
import { act, render, screen, within } from '@testing-library/react';
import * as Types from '@vegaprotocol/types';
import type { AccountFields } from './accounts-data-provider';
import { getAccountData } from './accounts-data-provider';
import { AccountTable } from './accounts-table';
import userEvent from '@testing-library/user-event';
const asset1 = {
__typename: 'Asset',
id: 'asset-1',
symbol: 'tBTC',
decimals: 5,
name: 'T BTC',
};
const asset2 = {
__typename: 'Asset',
id: 'asset-2',
symbol: 'aBTC',
decimals: 5,
name: 'A BTC',
};
const singleRow = {
__typename: 'AccountBalance',
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
@@ -13,12 +26,7 @@ const singleRow = {
__typename: 'Market',
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
},
asset: {
__typename: 'Asset',
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
decimals: 5,
},
asset: asset1,
available: '125600000',
used: '125600000',
total: '251200000',
@@ -33,12 +41,7 @@ const secondRow = {
__typename: 'Market',
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
},
asset: {
__typename: 'Asset',
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'aBTC',
decimals: 5,
},
asset: asset2,
available: '125600001',
used: '125600001',
total: '251200002',
@@ -134,7 +137,7 @@ describe('AccountsTable', () => {
it('should sort assets', async () => {
// 7001-COLL-010
const { container } = render(
render(
<AccountTable
rowData={multiRowData}
onClickAsset={() => null}
@@ -142,13 +145,13 @@ describe('AccountsTable', () => {
/>
);
const headerCell = screen.getByText('Asset');
await userEvent.click(headerCell);
const rows = container.querySelectorAll(
'.ag-center-cols-container .ag-row'
);
expect(rows[0].textContent).toContain('aBTC');
expect(rows[1].textContent).toContain('tBTC');
const headerCell = screen
.getAllByRole('columnheader')
.find((h) => h?.getAttribute('col-id') === 'asset.symbol') as HTMLElement;
await userEvent.click(within(headerCell).getByText(/asset/i));
expect(headerCell).toHaveAttribute('aria-sort', 'ascending');
});
it('should apply correct formatting in view as user mode', async () => {
@@ -176,12 +179,7 @@ describe('AccountsTable', () => {
rowData={singleRowData}
onClickAsset={() => null}
isReadOnly={false}
pinnedAsset={{
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
name: 'tBTC',
}}
pinnedAsset={asset1}
/>
);
await screen.findAllByRole('rowgroup');
@@ -213,23 +211,13 @@ describe('AccountsTable', () => {
const result = getAccountData([singleRow]);
const expected = [
{
asset: {
__typename: 'Asset',
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
},
asset: asset1,
available: '0',
balance: '0',
breakdown: [
{
__typename: 'AccountBalance',
asset: {
__typename: 'Asset',
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
},
asset: asset1,
available: '0',
balance: '125600000',
total: '125600000',
+94 -10
View File
@@ -1,4 +1,10 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import BigNumber from 'bignumber.js';
import {
@@ -7,7 +13,7 @@ import {
TransferForm,
type TransferFormProps,
} from './transfer-form';
import { AccountType } from '@vegaprotocol/types';
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
import { removeDecimal } from '@vegaprotocol/utils';
describe('TransferForm', () => {
@@ -39,8 +45,7 @@ describe('TransferForm', () => {
};
const amount = '100';
const pubKey =
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
const pubKey = '1'.repeat(64);
const asset = {
id: 'eur',
symbol: '€',
@@ -50,10 +55,7 @@ describe('TransferForm', () => {
};
const props = {
pubKey,
pubKeys: [
pubKey,
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
],
pubKeys: [pubKey, '2'.repeat(64)],
feeFactor: '0.001',
submitTransfer: jest.fn(),
accounts: [
@@ -177,7 +179,7 @@ describe('TransferForm', () => {
// Test use max button
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
expect(amountInput).toHaveValue('1000');
expect(amountInput).toHaveValue('1000.00');
// Test amount validation
await userEvent.clear(amountInput);
@@ -262,7 +264,7 @@ describe('TransferForm', () => {
// Test use max button
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
expect(amountInput).toHaveValue('100');
expect(amountInput).toHaveValue('100.00');
// If transfering from a vested account 'include fees' checkbox should
// be disabled and fees should be 0
@@ -291,6 +293,88 @@ describe('TransferForm', () => {
});
});
it('handles lots of decimal places', async () => {
const balance = '904195168829277777';
const expectedBalance = '0.904195168829277777';
const longDecimalAsset = {
id: 'assetId',
symbol: 'VEGA',
name: 'VEGA',
decimals: 18,
quantum: '1',
};
const account = {
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
asset: longDecimalAsset,
balance,
};
const mockSubmit = jest.fn();
renderComponent({
...props,
accounts: [account],
submitTransfer: mockSubmit,
minQuantumMultiple: '100000',
});
// Select a pubkey
await userEvent.selectOptions(
screen.getByLabelText('To Vega key'),
props.pubKeys[1] // Use not current pubkey so we can check it switches to current pubkey later
);
// Select asset
await selectAsset(longDecimalAsset);
const accountSelect = screen.getByLabelText('From account');
const option = within(accountSelect)
.getAllByRole('option')
.find(
(o) => o.getAttribute('value') === `${account.type}-${account.asset.id}`
);
// plus one for disabled 'please select' option
expect(option).toHaveTextContent(
`${AccountTypeMapping[account.type]} (${expectedBalance} ${
account.asset.symbol
})`
);
await userEvent.selectOptions(
accountSelect,
`${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${longDecimalAsset.id}`
);
expect(accountSelect).toHaveValue(
`${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${longDecimalAsset.id}`
);
// Check switch back to connected key
const amountInput = screen.getByLabelText('Amount');
// Test use max button
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
expect(amountInput).toHaveValue(expectedBalance);
await submit();
await waitFor(() => {
// 1003-TRAN-023
expect(mockSubmit).toHaveBeenCalledTimes(1);
expect(mockSubmit).toHaveBeenCalledWith({
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: props.pubKey,
asset: longDecimalAsset.id,
amount: balance,
oneOff: {},
});
});
});
describe('IncludeFeesCheckbox', () => {
it('validates fields and submits when checkbox is checked', async () => {
const mockSubmit = jest.fn();
+4 -10
View File
@@ -5,7 +5,6 @@ import {
useVegaPublicKey,
addDecimal,
formatNumber,
addDecimalsFormatNumber,
toBigNum,
} from '@vegaprotocol/utils';
import { useT } from './use-t';
@@ -218,12 +217,7 @@ export const TransferForm = ({
<AssetOption
key={a.key}
asset={a}
balance={
<Balance
balance={formatNumber(a.balance, a.decimals)}
symbol={a.symbol}
/>
}
balance={<Balance balance={a.balance} symbol={a.symbol} />}
/>
))}
</TradingRichSelect>
@@ -290,8 +284,8 @@ export const TransferForm = ({
return (
<option value={id} key={id}>
{AccountTypeMapping[a.type]} (
{addDecimalsFormatNumber(a.balance, a.asset.decimals)}{' '}
{a.asset.symbol})
{addDecimal(a.balance, a.asset.decimals)} {a.asset.symbol}
)
</option>
);
})}
@@ -420,7 +414,7 @@ export const TransferForm = ({
type="button"
className="absolute right-0 top-0 ml-auto text-xs underline"
onClick={() =>
setValue('amount', parseFloat(accountBalance).toString(), {
setValue('amount', accountBalance, {
shouldValidate: true,
})
}
-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;
@@ -1,5 +1,5 @@
import { useCallback, useState } from 'react';
import { getAsset, getProductType, getQuoteName } from '@vegaprotocol/markets';
import { getAsset, getQuoteName } from '@vegaprotocol/markets';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
@@ -77,12 +77,10 @@ export const DealTicketFeeDetails = ({
label={
<>
{t('Fees')}
{totalDiscountFactor ? (
{totalDiscountFactor !== '0' ? (
<Pill size="xxs" intent={Intent.Info} className="ml-1">
-
{formatNumberPercentage(
new BigNumber(totalDiscountFactor).multipliedBy(100),
2
new BigNumber(totalDiscountFactor).multipliedBy(100)
)}
</Pill>
) : null}
@@ -105,10 +103,7 @@ export const DealTicketFeeDetails = ({
)}
</p>
<FeesBreakdown
totalFeeAmount={feeEstimate?.totalFeeAmount}
referralDiscountFactor={feeEstimate?.referralDiscountFactor}
volumeDiscountFactor={feeEstimate?.volumeDiscountFactor}
fees={feeEstimate?.fees}
feeEstimate={feeEstimate}
feeFactors={market.fees.factors}
symbol={assetSymbol}
decimals={assetDecimals}
@@ -297,134 +292,120 @@ export const DealTicketMarginDetails = ({
);
const quoteName = getQuoteName(market);
const productType = getProductType(market);
return (
<div className="flex flex-col w-full gap-2 pt-2">
{/*
TODO: remove this conditional check once the following PRs are deployed
and the estimatePosition query is working for perps
- https://github.com/vegaprotocol/vega/pull/10119
- https://github.com/vegaprotocol/vega/pull/10122
*/}
{productType === 'Future' && (
<>
<Accordion>
<AccordionPanel
itemId="margin"
trigger={
<AccordionPrimitive.Trigger
data-testid="accordion-toggle"
className={classNames(
'w-full',
'flex items-center gap-2 text-xs',
'group'
)}
>
<div
data-testid={`deal-ticket-fee-margin-required`}
key={'value-dropdown'}
className="flex items-center justify-between w-full gap-2"
>
<div className="flex items-center text-left gap-1">
<Tooltip
description={t(
'MARGIN_DIFF_TOOLTIP_TEXT',
MARGIN_DIFF_TOOLTIP_TEXT,
{ assetSymbol }
)}
>
<span className="text-muted">
{t('Margin required')}
</span>
</Tooltip>
<AccordionChevron size={10} />
</div>
<Tooltip
description={
formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
) ?? '-'
}
>
<div className="font-mono text-right">
{formatValue(
marginRequiredWorstCase,
assetDecimals,
quantum
)}{' '}
{assetSymbol || ''}
</div>
</Tooltip>
</div>
</AccordionPrimitive.Trigger>
}
<Accordion>
<AccordionPanel
itemId="margin"
trigger={
<AccordionPrimitive.Trigger
data-testid="accordion-toggle"
className={classNames(
'w-full',
'flex items-center gap-2 text-xs',
'group'
)}
>
<div className="flex flex-col w-full gap-2">
<KeyValue
label={t('Total margin available')}
indent
value={formatValue(totalMarginAvailable, assetDecimals)}
formattedValue={formatValue(
totalMarginAvailable,
<div
data-testid={`deal-ticket-fee-margin-required`}
key={'value-dropdown'}
className="flex items-center justify-between w-full gap-2"
>
<div className="flex items-center text-left gap-1">
<Tooltip
description={t(
'MARGIN_DIFF_TOOLTIP_TEXT',
MARGIN_DIFF_TOOLTIP_TEXT,
{ assetSymbol }
)}
>
<span className="text-muted">{t('Margin required')}</span>
</Tooltip>
<AccordionChevron size={10} />
</div>
<Tooltip
description={
formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
) ?? '-'
}
>
<div className="font-mono text-right">
{formatValue(
marginRequiredWorstCase,
assetDecimals,
quantum
)}{' '}
{assetSymbol || ''}
</div>
</Tooltip>
</div>
</AccordionPrimitive.Trigger>
}
>
<div className="flex flex-col w-full gap-2">
<KeyValue
label={t('Total margin available')}
indent
value={formatValue(totalMarginAvailable, assetDecimals)}
formattedValue={formatValue(
totalMarginAvailable,
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={t(
'TOTAL_MARGIN_AVAILABLE',
TOTAL_MARGIN_AVAILABLE,
{
generalAccountBalance: formatValue(
generalAccountBalance,
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={t(
'TOTAL_MARGIN_AVAILABLE',
TOTAL_MARGIN_AVAILABLE,
{
generalAccountBalance: formatValue(
generalAccountBalance,
assetDecimals,
quantum
),
marginAccountBalance: formatValue(
marginAccountBalance,
assetDecimals,
quantum
),
marginMaintenance: formatValue(
currentMargins?.maintenanceLevel,
assetDecimals,
quantum
),
assetSymbol,
}
)}
/>
{deductionFromCollateral}
<KeyValue
label={t('Current margin allocation')}
indent
onClick={
generalAccountBalance
? () => setBreakdownDialog(true)
: undefined
}
value={formatValue(marginAccountBalance, assetDecimals)}
symbol={assetSymbol}
labelDescription={t(
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
MARGIN_ACCOUNT_TOOLTIP_TEXT
)}
formattedValue={formatValue(
),
marginAccountBalance: formatValue(
marginAccountBalance,
assetDecimals,
quantum
)}
/>
</div>
</AccordionPanel>
</Accordion>
{projectedMargin}
</>
)}
),
marginMaintenance: formatValue(
currentMargins?.maintenanceLevel,
assetDecimals,
quantum
),
assetSymbol,
}
)}
/>
{deductionFromCollateral}
<KeyValue
label={t('Current margin allocation')}
indent
onClick={
generalAccountBalance
? () => setBreakdownDialog(true)
: undefined
}
value={formatValue(marginAccountBalance, assetDecimals)}
symbol={assetSymbol}
labelDescription={t(
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
MARGIN_ACCOUNT_TOOLTIP_TEXT
)}
formattedValue={formatValue(
marginAccountBalance,
assetDecimals,
quantum
)}
/>
</div>
</AccordionPanel>
</Accordion>
{projectedMargin}
<KeyValue
label={t('Liquidation')}
value={liquidationPriceEstimateRange}
@@ -101,7 +101,7 @@ describe('StopOrder', () => {
jest.clearAllMocks();
});
it('should display ticket defaults', async () => {
it('should display ticket defaults limit order', async () => {
render(generateJsx());
// place order button should always be enabled
expect(screen.getByTestId(submitButton)).toBeEnabled();
@@ -131,6 +131,47 @@ describe('StopOrder', () => {
});
});
it('should display ticket defaults market order', async () => {
render(generateJsx());
// place order button should always be enabled
expect(screen.getByTestId(submitButton)).toBeEnabled();
// Assert defaults are used
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeMarket));
await userEvent.click(screen.getByTestId(orderTypeTrigger));
expect(screen.getByTestId(orderTypeLimit).dataset.state).toEqual(
'unchecked'
);
expect(screen.getByTestId(orderTypeMarket).dataset.state).toEqual(
'checked'
);
await userEvent.click(screen.getByTestId(orderTypeMarket));
expect(screen.getByTestId(orderSideBuy).dataset.state).toEqual('checked');
expect(screen.getByTestId(sizeInput)).toHaveDisplayValue('0');
expect(screen.getByTestId(timeInForce)).toHaveValue(
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
);
// 7002-SORD-084
expect(
screen.getByTestId(triggerDirectionRisesAbove).dataset.state
).toEqual('checked');
// 7002-SORD-085
expect(
screen.getByTestId(triggerDirectionFallsBelow).dataset.state
).toEqual('unchecked');
expect(screen.getByTestId(triggerTypePrice).dataset.state).toEqual(
'checked'
);
expect(screen.getByTestId(expire).dataset.state).toEqual('unchecked');
expect(screen.getByTestId(oco).dataset.state).toEqual('unchecked');
await userEvent.click(screen.getByTestId(expire));
await waitFor(() => {
expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual(
'checked'
);
});
});
it('calculate notional for market limit', async () => {
render(generateJsx());
await userEvent.type(screen.getByTestId(sizeInput), '10');
@@ -239,33 +280,43 @@ describe('StopOrder', () => {
it.each([
{ fieldName: 'size', ocoValue: false },
{ fieldName: 'ocoSize', ocoValue: true },
])('validates $fieldName field', async ({ ocoValue }) => {
render(generateJsx());
if (ocoValue) {
await userEvent.click(screen.getByTestId(oco));
}
await userEvent.click(screen.getByTestId(submitButton));
const getByTestId = (id: string) =>
screen.getByTestId(ocoPostfix(id, ocoValue));
const queryByTestId = (id: string) =>
screen.queryByTestId(ocoPostfix(id, ocoValue));
// default value should be invalid
expect(getByTestId(sizeErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(getByTestId(sizeInput), '0.01');
expect(getByTestId(sizeErrorMessage)).toBeInTheDocument();
{ fieldName: 'size', ocoValue: false, orderTypeMarketValue: true },
{ fieldName: 'ocoSize', ocoValue: true, orderTypeMarketValue: true },
])(
'validates $fieldName field',
async ({ ocoValue, orderTypeMarketValue }) => {
render(generateJsx());
if (orderTypeMarketValue) {
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeMarket));
}
if (ocoValue) {
await userEvent.click(screen.getByTestId(oco));
}
await userEvent.click(screen.getByTestId(submitButton));
const getByTestId = (id: string) =>
screen.getByTestId(ocoPostfix(id, ocoValue));
const queryByTestId = (id: string) =>
screen.queryByTestId(ocoPostfix(id, ocoValue));
// default value should be invalid
expect(getByTestId(sizeErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(getByTestId(sizeInput), '0.01');
expect(getByTestId(sizeErrorMessage)).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(getByTestId(sizeInput));
await userEvent.type(getByTestId(sizeInput), '0.1');
expect(queryByTestId(sizeErrorMessage)).toBeNull();
});
// clear and fill using valid value
await userEvent.clear(getByTestId(sizeInput));
await userEvent.type(getByTestId(sizeInput), '0.1');
expect(queryByTestId(sizeErrorMessage)).toBeNull();
}
);
it.each([
{ fieldName: 'price', ocoValue: false },
{ fieldName: 'ocoPrice', ocoValue: true },
])('validates $fieldName field', async ({ ocoValue }) => {
render(generateJsx());
if (ocoValue) {
await userEvent.click(screen.getByTestId(oco));
}
@@ -275,7 +326,7 @@ describe('StopOrder', () => {
screen.getByTestId(ocoPostfix(id, ocoValue));
const queryByTestId = (id: string) =>
screen.queryByTestId(ocoPostfix(id, ocoValue));
// 7002-SORD-095
expect(getByTestId(priceErrorMessage)).toBeInTheDocument();
await userEvent.type(getByTestId(priceInput), '0.001');
expect(getByTestId(priceErrorMessage)).toBeInTheDocument();
@@ -305,48 +356,77 @@ describe('StopOrder', () => {
it.each([
{ fieldName: 'triggerPrice', ocoValue: false },
{ fieldName: 'ocoTriggerPrice', ocoValue: true },
])('validates $fieldName field', async ({ ocoValue }) => {
render(generateJsx());
{ fieldName: 'triggerPrice', ocoValue: false, orderTypeMarketValue: true },
{
fieldName: 'ocoTriggerPrice',
ocoValue: true,
orderTypeMarketValue: true,
},
])(
'validates $fieldName field',
async ({ ocoValue, orderTypeMarketValue }) => {
render(generateJsx());
if (orderTypeMarketValue) {
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeMarket));
}
if (ocoValue) {
await userEvent.click(screen.getByTestId(oco));
await userEvent.click(screen.getByTestId(triggerDirectionFallsBelow));
}
await userEvent.click(screen.getByTestId(submitButton));
const getByTestId = (id: string) =>
screen.getByTestId(ocoPostfix(id, ocoValue));
const queryByTestId = (id: string) =>
screen.queryByTestId(ocoPostfix(id, ocoValue));
// 7002-SORD-095
// 7002-SORD-087
if (ocoValue) {
await userEvent.click(screen.getByTestId(oco));
await userEvent.click(screen.getByTestId(triggerDirectionFallsBelow));
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// switch to trailing percentage offset trigger type
await userEvent.click(getByTestId(triggerTypeTrailingPercentOffset));
expect(queryByTestId(triggerPriceErrorMessage)).toBeNull();
// switch back to price trigger type
await userEvent.click(getByTestId(triggerTypePrice));
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(getByTestId(triggerPriceInput), '0.001');
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// clear and fill using value causing immediate trigger
await userEvent.clear(getByTestId(triggerPriceInput));
await userEvent.type(getByTestId(triggerPriceInput), '0.01');
expect(queryByTestId(triggerPriceErrorMessage)).toBeNull();
expect(queryByTestId(triggerPriceWarningMessage)).toBeInTheDocument();
// change to correct value
await userEvent.type(getByTestId(triggerPriceInput), '2');
expect(queryByTestId(triggerPriceWarningMessage)).toBeNull();
}
await userEvent.click(screen.getByTestId(submitButton));
const getByTestId = (id: string) =>
screen.getByTestId(ocoPostfix(id, ocoValue));
const queryByTestId = (id: string) =>
screen.queryByTestId(ocoPostfix(id, ocoValue));
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// switch to trailing percentage offset trigger type
await userEvent.click(getByTestId(triggerTypeTrailingPercentOffset));
expect(queryByTestId(triggerPriceErrorMessage)).toBeNull();
// switch back to price trigger type
await userEvent.click(getByTestId(triggerTypePrice));
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(getByTestId(triggerPriceInput), '0.001');
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// clear and fill using value causing immediate trigger
await userEvent.clear(getByTestId(triggerPriceInput));
await userEvent.type(getByTestId(triggerPriceInput), '0.01');
expect(queryByTestId(triggerPriceErrorMessage)).toBeNull();
expect(queryByTestId(triggerPriceWarningMessage)).toBeInTheDocument();
// change to correct value
await userEvent.type(getByTestId(triggerPriceInput), '2');
expect(queryByTestId(triggerPriceWarningMessage)).toBeNull();
});
);
it.each([
{ fieldName: 'trailingPercentageOffset', ocoValue: false },
{ fieldName: 'ocoTrailingPercentageOffset', ocoValue: true },
{
fieldName: 'trailingPercentageOffset',
ocoValue: false,
orderTypeMarket: true,
},
{
fieldName: 'ocoTrailingPercentageOffset',
ocoValue: true,
orderTypeMarket: true,
},
])('validates $fieldName field', async ({ ocoValue }) => {
render(generateJsx());
if (orderTypeMarket) {
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeMarket));
}
if (ocoValue) {
await userEvent.click(screen.getByTestId(oco));
}
@@ -401,9 +481,11 @@ describe('StopOrder', () => {
it('sync oco trigger', async () => {
render(generateJsx());
await userEvent.click(screen.getByTestId(oco));
// 7002-SORD-099
expect(
screen.getByTestId(triggerDirectionRisesAbove).dataset.state
).toEqual('checked');
// 7002-SORD-091
expect(
screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow)).dataset.state
).toEqual('checked');
@@ -481,6 +563,7 @@ describe('StopOrder', () => {
expect(mockDataProvider.mock.lastCall?.[0].skip).toBe(true);
await userEvent.type(screen.getByTestId(sizeInput), '0.01');
expect(mockDataProvider.mock.lastCall?.[0].skip).toBe(false);
// 7002-SORD-011
expect(screen.getByTestId(numberOfActiveOrdersLimit)).toBeInTheDocument();
});
@@ -367,6 +367,29 @@ describe('DealTicket', () => {
});
});
it('should see an explanation of peak size', async () => {
render(generateJsx());
await userEvent.click(screen.getByTestId('iceberg'));
await userEvent.hover(screen.getByText('Peak size'));
await waitFor(() => {
const tooltips = screen.getAllByTestId('tooltip-content');
expect(tooltips[0]).toHaveTextContent(
`The maximum volume that can be traded at once. Must be less than the total size of the order.`
);
});
});
it('should see an explanation of minimum size', async () => {
render(generateJsx());
await userEvent.click(screen.getByTestId('iceberg'));
await userEvent.hover(screen.getByText('Minimum size'));
await waitFor(() => {
const tooltips = screen.getAllByTestId('tooltip-content');
expect(tooltips[0]).toHaveTextContent(
`When the order trades and its size falls below this threshold, it will be reset to the peak size and moved to the back of the priority order. Must be less than or equal to peak size, and greater than 0.`
);
});
});
it('should see an explanation of reduce only', async () => {
render(generateJsx());
userEvent.hover(screen.getByText('Reduce only'));
@@ -59,9 +59,7 @@ export const TimeInForceSelector = ({
components={[
<Tooltip
description={
<SimpleGrid
grid={compileGridData(t, market, marketData, t)}
/>
<SimpleGrid grid={compileGridData(t, market, marketData)} />
}
>
sufficient liquidity
@@ -83,9 +81,7 @@ export const TimeInForceSelector = ({
components={[
<Tooltip
description={
<SimpleGrid
grid={compileGridData(t, market, marketData, t)}
/>
<SimpleGrid grid={compileGridData(t, market, marketData)} />
}
>
high price volatility
@@ -6,16 +6,19 @@ describe('getDiscountedFee', () => {
discountedFee: '100',
volumeDiscount: '0',
referralDiscount: '0',
totalDiscount: '0',
});
expect(getDiscountedFee('100', undefined, '0.1')).toEqual({
discountedFee: '90',
volumeDiscount: '10',
referralDiscount: '0',
totalDiscount: '10',
});
expect(getDiscountedFee('100', '0.1', undefined)).toEqual({
discountedFee: '90',
volumeDiscount: '0',
referralDiscount: '10',
totalDiscount: '10',
});
});
@@ -24,6 +27,7 @@ describe('getDiscountedFee', () => {
discountedFee: '',
volumeDiscount: '0',
referralDiscount: '0',
totalDiscount: '0',
});
});
});
@@ -35,7 +39,7 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0',
referralDiscountFactor: '0',
})
).toEqual(0);
).toEqual('0');
});
it('returns volumeDiscountFactor if referralDiscountFactor is 0', () => {
@@ -44,7 +48,7 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0.1',
referralDiscountFactor: '0',
})
).toEqual(0.1);
).toEqual('-0.1');
});
it('returns referralDiscountFactor if volumeDiscountFactor is 0', () => {
expect(
@@ -52,7 +56,7 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0',
referralDiscountFactor: '0.1',
})
).toEqual(0.1);
).toEqual('-0.1');
});
it('calculates discount using referralDiscountFactor and volumeDiscountFactor', () => {
@@ -61,6 +65,6 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0.2',
referralDiscountFactor: '0.1',
})
).toBeCloseTo(0.28);
).toBe('-0.28');
});
});
+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;
},
});
@@ -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',
+1
View File
@@ -85,6 +85,7 @@ export const DocsLinks = VEGA_DOCS_URL
ICEBERG_ORDERS: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#iceberg-order`,
POST_REDUCE_ONLY: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#conditional-order-parameters`,
QUANTUM: `${VEGA_DOCS_URL}/concepts/assets/asset-framework#quantum`,
REFERRALS: `${VEGA_DOCS_URL}/tutorials/proposals/referral-program-proposal`,
}
: undefined;
@@ -70,10 +70,7 @@ export const useNodeHealth = () => {
);
intent = Intent.Danger;
} else if (blockDiff >= BLOCK_THRESHOLD) {
text = t('blocksBehind', {
defaultValue: '{{count}} Blocks behind',
replace: { count: blockDiff },
});
text = t('blocksBehind', '{{count}} Blocks behind', { count: blockDiff });
intent = Intent.Warning;
} else if (blockUpdateMsLatency > WARNING_LATENCY) {
text = t(
+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 = {
+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 -10
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,6 +201,9 @@
"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",
@@ -205,7 +212,6 @@
"Redeem rewards": "Redeem rewards",
"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",
@@ -218,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",
@@ -267,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",
@@ -281,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",
@@ -297,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.",
@@ -314,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>
);
};
+1 -4
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 = ({
@@ -9,6 +9,7 @@ describe('DepthChart', () => {
<DepthChartContainer marketId={'market-id'} />
</MockedProvider>
);
// 6006-DEPC-001
expect(baseElement).toBeTruthy();
});
});
@@ -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,17 +3,20 @@ 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();
jest.mock('@vegaprotocol/wallet', () => ({
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: jest.fn(() => ({ pubKey: 'partyId' })),
}));
jest.mock('@vegaprotocol/web3', () => ({
...jest.requireActual('@vegaprotocol/web3'),
useVegaTransactionStore: jest.fn(() => mockCreate),
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockUseDataProvider = (args: any) => {
if (args.dataProvider === positionsMarketsProvider) {
@@ -21,22 +24,23 @@ const mockUseDataProvider = (args: any) => {
}
return { data: [singleRow] };
};
jest.mock('@vegaprotocol/data-provider', () => ({
...jest.requireActual('@vegaprotocol/data-provider'),
useDataProvider: jest.fn((args) => mockUseDataProvider(args)),
}));
describe('PositionsManager', () => {
it('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,
});
expect(await screen.getByTestId('close-position')).toBeInTheDocument();
expect(await screen.findByTestId('close-position')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('close-position'));
expect(
mockCreate.mock.lastCall[0].batchMarketInstructions.submissions[0].size
).toEqual(MAXGOINT64);
).toEqual(HALFMAXGOINT64);
});
});
+9 -6
View File
@@ -1,7 +1,4 @@
import { useCallback } from 'react';
import { PositionsTable } from './positions-table';
import * as Schema from '@vegaprotocol/types';
import { useVegaTransactionStore } from '@vegaprotocol/web3';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useDataProvider } from '@vegaprotocol/data-provider';
import {
@@ -9,8 +6,12 @@ import {
positionsMarketsProvider,
} from './positions-data-providers';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { MAXGOINT64 } from '@vegaprotocol/utils';
import { useT } from '../use-t';
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[];
@@ -30,6 +31,8 @@ export const PositionsManager = ({
const t = useT();
const { pubKeys, pubKey } = useVegaWallet();
const create = useVegaTransactionStore((store) => store.create);
const disableClosePositionsButton = FLAGS.DISABLE_CLOSE_POSITION;
const onClose = useCallback(
({ marketId, openVolume }: { marketId: string; openVolume: string }) =>
create({
@@ -48,7 +51,7 @@ export const PositionsManager = ({
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
size: HALFMAXGOINT64, // improvement for avoiding leftovers filled in the meantime when close request has been sent
reduceOnly: true,
},
],
@@ -74,7 +77,7 @@ export const PositionsManager = ({
pubKeys={pubKeys}
rowData={data}
onMarketClick={onMarketClick}
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>

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