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
102 changed files with 1773 additions and 1035 deletions
@@ -12,7 +12,8 @@ import { type VegaICellRendererParams } from '@vegaprotocol/datagrid';
import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { useNavigate } from 'react-router-dom';
import { type RowClickedEvent, ColDef } from 'ag-grid-community';
import { 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
@@ -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>
@@ -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:
+10 -6
View File
@@ -208,9 +208,13 @@ const TiersTable = ({
{ name: 'discount', displayName: t('Referrer trading discount') },
{
name: 'volume',
displayName: t('Min. trading volume (last {{count}} epochs)', {
count: windowLength,
}),
displayName: t(
'minTradingVolume',
'Min. trading volume (last {{count}} epochs)',
{
count: windowLength,
}
),
},
{ name: 'epochs', displayName: t('Min. epochs') },
]}
@@ -218,13 +222,13 @@ const TiersTable = ({
...d,
className: classNames({
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
d.tier === 1,
d.tier >= 3,
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
d.tier === 2,
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
d.tier === 3,
d.tier === 1,
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
d.tier > 3,
d.tier == 0,
}),
}))}
/>
@@ -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>
@@ -16,15 +16,12 @@ export const LedgerContainer = () => {
});
const assets = (data?.party?.accountsConnection?.edges ?? [])
.map<PartyAssetFieldsFragment>(
(item) => item?.node?.asset ?? ({} as PartyAssetFieldsFragment)
)
.reduce((aggr, item) => {
if ('id' in item && 'symbol' in item) {
aggr[item.id as string] = item.symbol as string;
}
return aggr;
}, {} as Record<string, string>);
.map((item) => item?.node?.asset)
.filter((asset): asset is PartyAssetFieldsFragment => !!asset?.id)
.reduce(
(aggr, item) => Object.assign(aggr, { [item.id]: item.symbol }),
{} as Record<string, string>
);
if (!pubKey) {
return (
@@ -172,4 +172,20 @@ describe('Navbar', () => {
expect(mockDisconnect).toHaveBeenCalled();
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
});
it('does not render the language selector until we have more languages', () => {
renderComponent();
expect(screen.queryByTestId('icon-globe')).not.toBeInTheDocument();
});
it('renders the theme switcher', async () => {
renderComponent();
await userEvent.click(screen.getByTestId('icon-moon'));
expect(screen.queryByTestId('icon-moon')).not.toBeInTheDocument();
expect(screen.getByTestId('icon-sun')).toBeInTheDocument();
await userEvent.click(screen.getByTestId('icon-sun'));
expect(screen.queryByTestId('icon-sun')).not.toBeInTheDocument();
expect(screen.getByTestId('icon-moon')).toBeInTheDocument();
});
});
+19 -2
View File
@@ -11,7 +11,13 @@ import {
} from '@vegaprotocol/environment';
import { useGlobalStore } from '../../stores';
import { VegaWalletConnectButton } from '../vega-wallet-connect-button';
import { VegaIconNames, VegaIcon, VLogo } from '@vegaprotocol/ui-toolkit';
import {
VegaIconNames,
VegaIcon,
VLogo,
LanguageSelector,
ThemeSwitcher,
} from '@vegaprotocol/ui-toolkit';
import * as N from '@radix-ui/react-navigation-menu';
import * as D from '@radix-ui/react-dialog';
import { NavLink } from 'react-router-dom';
@@ -22,7 +28,8 @@ import { VegaWalletMenu } from '../vega-wallet';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { WalletIcon } from '../icons/wallet';
import { ProtocolUpgradeCountdown } from '@vegaprotocol/proposals';
import { useT } from '../../lib/use-t';
import { useT, useI18n } from '../../lib/use-t';
import { supportedLngs } from '../../lib/i18n';
type MenuState = 'wallet' | 'nav' | null;
type Theme = 'system' | 'yellow';
@@ -34,6 +41,7 @@ export const Navbar = ({
children?: ReactNode;
theme?: Theme;
}) => {
const i18n = useI18n();
const t = useT();
// menu state for small screens
const [menu, setMenu] = useState<MenuState>(null);
@@ -77,6 +85,15 @@ export const Navbar = ({
{/* Right section */}
<div className="ml-auto flex items-center justify-end gap-2">
<ProtocolUpgradeCountdown />
<div className="flex">
<ThemeSwitcher />
{supportedLngs.length > 1 ? (
<LanguageSelector
languages={supportedLngs}
onSelect={(language) => i18n.changeLanguage(language)}
/>
) : null}
</div>
<NavbarMobileButton
onClick={() => {
if (isConnected) {
+1 -1
View File
@@ -1,2 +1,2 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
VEGA_VERSION=v0.73.5
VEGA_VERSION=v0.73.6
+1 -1
View File
@@ -1,2 +1,2 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:develop
VEGA_VERSION=v0.73.5
VEGA_VERSION=v0.73.6
+1 -1
View File
@@ -1,2 +1,2 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:main
VEGA_VERSION=v0.73.5
VEGA_VERSION=v0.73.6
@@ -1,4 +1,5 @@
import pytest
import re
import vega_sim.api.governance as governance
from playwright.sync_api import Page, expect
from vega_sim.service import VegaService, PeggedOrder
@@ -10,6 +11,8 @@ from wallet_config import MM_WALLET, MM_WALLET2, GOVERNANCE_WALLET
@pytest.mark.usefixtures("vega", "page", "proposed_market", "risk_accepted")
def test_market_lifecycle(proposed_market, vega: VegaService, page: Page):
# 7002-SORD-001
# 7002-SORD-002
trading_mode = page.get_by_test_id("market-trading-mode").get_by_test_id(
"item-value"
)
@@ -18,12 +21,32 @@ def test_market_lifecycle(proposed_market, vega: VegaService, page: Page):
# setup market in proposed step, without liquidity provided
market_id = proposed_market
page.goto(f"/#/markets/{market_id}")
# 6002-MDET-001
expect(page.get_by_test_id("header-title")).to_have_text("BTC:DAI_2023Futr")
# 6002-MDET-002
expect(page.get_by_test_id("market-expiry")).to_have_text("ExpiryNot time-based")
page.get_by_test_id("market-expiry").hover()
expect(page.get_by_test_id("expiry-tooltip").first).to_have_text("This market expires when triggered by its oracle, not on a set date.View oracle specification")
expect(page.get_by_test_id("expiry-tooltip").first.get_by_test_id("link")).to_have_attribute("href", re.compile('.*'))
# 6002-MDET-003
expect(page.get_by_test_id("market-price")).to_have_text("Mark Price0.00")
# 6002-MDET-004
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
# 6002-MDET-005
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
# 6002-MDET-008
expect(page.get_by_test_id("market-settlement-asset")).to_have_text("Settlement assettDAI")
expect(page.get_by_test_id("liquidity-supplied")).to_have_text("Liquidity supplied 0.00 (0.00%)")
page.get_by_test_id("liquidity-supplied").hover()
expect(page.get_by_test_id("liquidity-supplied-tooltip").first).to_have_text("Supplied stake0.00Target stake0.00View liquidity provision tableLearn about providing liquidity")
expect(page.get_by_test_id("liquidity-supplied-tooltip").first.get_by_test_id("link").first).to_have_text("View liquidity provision table")
# check that market is in proposed state
# 6002-MDET-006
# 6002-MDET-007
# 7002-SORD-061
expect(trading_mode).to_have_text("No trading")
trading_mode.hover()
expect(page.get_by_test_id("trading-mode-tooltip").first).to_have_text("No trading enabled for this market.")
expect(market_state).to_have_text("Proposed")
# approve market
@@ -0,0 +1,116 @@
import pytest
import re
import json
from playwright.sync_api import Page, expect, Route
from vega_sim.service import VegaService
from conftest import init_vega
from fixtures.market import setup_continuous_market
order_size = "order-size"
order_price = "order-price"
place_order = "place-order"
order_side_sell = "order-side-SIDE_SELL"
market_order = "order-type-Market"
tif = "order-tif"
expire = "expire"
api_request_match = r"http://localhost:\d+/api/v2/requests"
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega:
yield vega
@pytest.fixture(scope="module")
def continuous_market(vega):
return setup_continuous_market(vega)
def handle_route_connection_lost(route: Route, request):
if request.method == "POST" and re.match(api_request_match, request.url):
route.fulfill(
status=200,
headers={"Content-Type": "application/json"},
body='{"jsonrpc": "2.0", "id": "1"}'
)
else:
route.continue_()
def handle_route_connection_rejected(route: Route, request):
if request.method == "POST" and re.match(api_request_match, request.url):
custom_response = {
"jsonrpc": "2.0",
"error": {
"code": 3001,
"data": "the user rejected the wallet connection",
"message": "User error"
},
"id": "0"
}
route.fulfill(
status=400,
headers={"Content-Type": "application/json"},
body=json.dumps(custom_response)
)
else:
route.continue_()
def assert_connection_approve(route: Route, request, page:Page):
if request.method == "POST" and re.match(api_request_match, request.url):
expect(page.get_by_test_id("toast-content")).to_have_text("Please go to your Vega wallet application and approve or reject the transaction.")
else:
route.continue_()
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_wallet_connection_error(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.route("**/*", handle_route_connection_lost)
page.get_by_test_id("connect-vega-wallet").click()
page.get_by_test_id("connector-jsonRpc").click()
expect(page.get_by_test_id("wallet-dialog-title")).to_have_text("Something went wrong")
@pytest.mark.usefixtures("page", "risk_accepted")
def test_wallet_connection_rejected(continuous_market, page: Page):
# 0002-WCON-002
# 0002-WCON-005
# 0002-WCON-007
# 0002-WCON-015
page.goto(f"/#/markets/{continuous_market}")
page.route("**/*", handle_route_connection_rejected)
page.get_by_test_id("connect-vega-wallet").click()
page.get_by_test_id("connector-jsonRpc").click()
expect(page.get_by_test_id("dialog-content").nth(1)).to_have_text("User errorthe user rejected the wallet connectionTry againAbout the Vega wallet | Supported browsers ")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_wallet_connection_error_transaction(continuous_market, vega: VegaService, page: Page):
# 0003-WTXN-009
# 0003-WTXN-011
# 0002-WCON-016
# 0003-WTXN-008
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(order_size).fill("10")
page.get_by_test_id(order_price).fill("120")
page.route("**/*", handle_route_connection_lost)
page.get_by_test_id(place_order).click()
expect(page.get_by_test_id("toast-content")).to_have_text("Wallet disconnectedThe connection to your Vega Wallet has been lost.Connect vega wallet")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_wallet_transaction_rejected(continuous_market, vega: VegaService, page: Page):
# 0003-WTXN-007
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(order_size).fill("10")
page.get_by_test_id(order_price).fill("120")
page.route("**/*", handle_route_connection_rejected)
page.get_by_test_id(place_order).click()
expect(page.get_by_test_id("toast-content")).to_have_text("Error occurredthe user rejected the wallet connection")
@pytest.mark.usefixtures("page", "auth", "risk_accepted")
def test_wallet_connection_approve(continuous_market, vega: VegaService, page: Page):
# 0002-WCON-005
# 0002-WCON-007
# 0002-WCON-009
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id(order_size).fill("10")
page.get_by_test_id(order_price).fill("120")
page.route("**/*", assert_connection_approve)
page.get_by_test_id(place_order).click()
+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} />;
-15
View File
@@ -1,15 +0,0 @@
export const useTranslation = () => ({
t: (label: string, replacements?: Record<string, string>) => {
const replace =
replacements?.['replace'] && typeof replacements === 'object'
? replacements?.['replace']
: replacements;
let translatedLabel = replacements?.['defaultValue'] || label;
if (typeof replace === 'object' && replace !== null) {
Object.keys(replace).forEach((key) => {
translatedLabel = translatedLabel.replace(`{{${key}}}`, replace[key]);
});
}
return translatedLabel;
},
});
@@ -36,10 +36,10 @@ export const AgGridThemed = ({
<div className={wrapperClasses}>
<AgGridReact
defaultColDef={defaultColDef}
ref={gridRef}
overlayLoadingTemplate={t('Loading...')}
overlayNoRowsTemplate={t('No data')}
suppressDragLeaveHidesColumns
ref={gridRef}
{...defaultProps}
{...props}
/>
+1 -1
View File
@@ -25,7 +25,7 @@ describe('Pagination', () => {
const mockOnLoad = jest.fn();
const count = 10;
render(<Pagination {...props} count={count} onLoad={mockOnLoad} />);
expect(screen.getByText(`${count} rows loaded`)).toBeInTheDocument();
expect(screen.getByText('10 rows loaded')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
expect(mockOnLoad).toHaveBeenCalled();
});
+5 -9
View File
@@ -18,19 +18,15 @@ export const Pagination = ({
let rowMessage = '';
if (count && !pageInfo?.hasNextPage) {
rowMessage = t('paginationAllLoaded', {
replace: { count },
defaultValue: 'All {{count}} rows loaded',
rowMessage = t('paginationAllLoaded', 'all {{count}} rows loaded', {
count,
});
} else {
rowMessage = t('paginationLoaded', {
replace: { count },
defaultValue: '{{count}} rows loaded',
});
rowMessage = t('paginationLoaded', '{{count}} rows loaded', { count });
}
return (
<div className="flex items-center justify-between p-1 border-t border-default">
<div className="border-default flex items-center justify-between border-t p-1">
<div className="text-xs">
{false}
{showRetentionMessage &&
@@ -47,7 +43,7 @@ export const Pagination = ({
) : null}
</div>
{count && hasDisplayedRows === false ? (
<div className="absolute text-xs top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform text-xs">
{t('No rows matching selected filters')}
</div>
) : null}
+14
View File
@@ -1,4 +1,18 @@
import '@testing-library/jest-dom';
import ResizeObserver from 'resize-observer-polyfill';
import { locales } from '@vegaprotocol/i18n';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
// Set up i18n instance so that components have the correct default
// en translations
i18n.use(initReactI18next).init({
// we init with resources
resources: locales,
fallbackLng: 'en',
ns: ['datagrid'],
defaultNS: 'datagrid',
});
global.ResizeObserver = ResizeObserver;
@@ -77,12 +77,10 @@ export const DealTicketFeeDetails = ({
label={
<>
{t('Fees')}
{totalDiscountFactor ? (
{totalDiscountFactor !== '0' ? (
<Pill size="xxs" intent={Intent.Info} className="ml-1">
-
{formatNumberPercentage(
new BigNumber(totalDiscountFactor).multipliedBy(100),
2
new BigNumber(totalDiscountFactor).multipliedBy(100)
)}
</Pill>
) : null}
@@ -105,10 +103,7 @@ export const DealTicketFeeDetails = ({
)}
</p>
<FeesBreakdown
totalFeeAmount={feeEstimate?.totalFeeAmount}
referralDiscountFactor={feeEstimate?.referralDiscountFactor}
volumeDiscountFactor={feeEstimate?.volumeDiscountFactor}
fees={feeEstimate?.fees}
feeEstimate={feeEstimate}
feeFactors={market.fees.factors}
symbol={assetSymbol}
decimals={assetDecimals}
@@ -59,9 +59,7 @@ export const TimeInForceSelector = ({
components={[
<Tooltip
description={
<SimpleGrid
grid={compileGridData(t, market, marketData, t)}
/>
<SimpleGrid grid={compileGridData(t, market, marketData)} />
}
>
sufficient liquidity
@@ -83,9 +81,7 @@ export const TimeInForceSelector = ({
components={[
<Tooltip
description={
<SimpleGrid
grid={compileGridData(t, market, marketData, t)}
/>
<SimpleGrid grid={compileGridData(t, market, marketData)} />
}
>
high price volatility
@@ -6,16 +6,19 @@ describe('getDiscountedFee', () => {
discountedFee: '100',
volumeDiscount: '0',
referralDiscount: '0',
totalDiscount: '0',
});
expect(getDiscountedFee('100', undefined, '0.1')).toEqual({
discountedFee: '90',
volumeDiscount: '10',
referralDiscount: '0',
totalDiscount: '10',
});
expect(getDiscountedFee('100', '0.1', undefined)).toEqual({
discountedFee: '90',
volumeDiscount: '0',
referralDiscount: '10',
totalDiscount: '10',
});
});
@@ -24,6 +27,7 @@ describe('getDiscountedFee', () => {
discountedFee: '',
volumeDiscount: '0',
referralDiscount: '0',
totalDiscount: '0',
});
});
});
@@ -35,7 +39,7 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0',
referralDiscountFactor: '0',
})
).toEqual(0);
).toEqual('0');
});
it('returns volumeDiscountFactor if referralDiscountFactor is 0', () => {
@@ -44,7 +48,7 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0.1',
referralDiscountFactor: '0',
})
).toEqual(0.1);
).toEqual('-0.1');
});
it('returns referralDiscountFactor if volumeDiscountFactor is 0', () => {
expect(
@@ -52,7 +56,7 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0',
referralDiscountFactor: '0.1',
})
).toEqual(0.1);
).toEqual('-0.1');
});
it('calculates discount using referralDiscountFactor and volumeDiscountFactor', () => {
@@ -61,6 +65,6 @@ describe('getTotalDiscountFactor', () => {
volumeDiscountFactor: '0.2',
referralDiscountFactor: '0.1',
})
).toBeCloseTo(0.28);
).toBe('-0.28');
});
});
+27 -12
View File
@@ -15,6 +15,7 @@ export const getDiscountedFee = (
discountedFee: feeAmount,
volumeDiscount: '0',
referralDiscount: '0',
totalDiscount: '0',
};
}
const referralDiscount = new BigNumber(referralDiscountFactor || '0')
@@ -23,12 +24,14 @@ export const getDiscountedFee = (
const volumeDiscount = new BigNumber(volumeDiscountFactor || '0')
.multipliedBy((BigInt(feeAmount) - BigInt(referralDiscount)).toString())
.toFixed(0, BigNumber.ROUND_FLOOR);
const totalDiscount = (
BigInt(referralDiscount) + BigInt(volumeDiscount)
).toString();
const discountedFee = (
BigInt(feeAmount || '0') -
BigInt(referralDiscount) -
BigInt(volumeDiscount)
BigInt(feeAmount || '0') - BigInt(totalDiscount)
).toString();
return {
totalDiscount,
referralDiscount,
volumeDiscount,
discountedFee,
@@ -39,16 +42,28 @@ export const getTotalDiscountFactor = (feeEstimate?: {
volumeDiscountFactor?: string;
referralDiscountFactor?: string;
}) => {
if (!feeEstimate) {
return 0;
if (
!feeEstimate ||
(feeEstimate.referralDiscountFactor === '0' &&
feeEstimate.volumeDiscountFactor === '0')
) {
return '0';
}
const volumeFactor = Number(feeEstimate?.volumeDiscountFactor) || 0;
const referralFactor = Number(feeEstimate?.referralDiscountFactor) || 0;
if (!volumeFactor) {
return referralFactor;
const volumeFactor = new BigNumber(
feeEstimate?.volumeDiscountFactor || 0
).minus(1);
const referralFactor = new BigNumber(
feeEstimate?.referralDiscountFactor || 0
).minus(1);
if (volumeFactor.isZero()) {
return feeEstimate.referralDiscountFactor
? `-${feeEstimate.referralDiscountFactor}`
: '0';
}
if (!referralFactor) {
return volumeFactor;
if (referralFactor.isZero()) {
return feeEstimate.volumeDiscountFactor
? `-${feeEstimate.volumeDiscountFactor}`
: '0';
}
return 1 - (1 - volumeFactor) * (1 - referralFactor);
return volumeFactor.multipliedBy(referralFactor).minus(1).toString();
};
@@ -14,13 +14,16 @@ describe('FeesBreakdown', () => {
liquidityFee: '100',
};
const props = {
totalFeeAmount: '100',
fees,
feeFactors,
symbol: 'USD',
decimals: 2,
referralDiscountFactor: '0.01',
volumeDiscountFactor: '0.01',
feeEstimate: {
referralDiscountFactor: '0.01',
volumeDiscountFactor: '0.01',
totalFeeAmount: '100',
fees,
},
};
render(<FeesBreakdown {...props} />);
expect(screen.getByText('Maker fee').nextElementSibling).toHaveTextContent(
@@ -1,12 +1,13 @@
import { sumFeesFactors } from '@vegaprotocol/markets';
import type { TradeFee, FeeFactors } from '@vegaprotocol/types';
import type { FeeFactors } from '@vegaprotocol/types';
import {
addDecimalsFormatNumber,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { getDiscountedFee } from '../discounts';
import { getDiscountedFee, getTotalDiscountFactor } from '../discounts';
import { useT } from '../../use-t';
import { type useEstimateFees } from '../../hooks/use-estimate-fees';
const formatValue = (
value: string | number | null | undefined,
@@ -24,18 +25,16 @@ const FeesBreakdownItem = ({
decimals,
}: {
label: string;
factor?: string;
factor?: string | number;
value: string;
symbol?: string;
decimals: number;
}) => (
<>
<dt className="col-span-2">{label}</dt>
{factor && (
<dd className="text-right col-span-1">
{formatNumberPercentage(new BigNumber(factor).times(100))}
</dd>
)}
<dd className="text-right col-span-1">
{factor ? formatNumberPercentage(new BigNumber(factor).times(100)) : ''}
</dd>
<dd className="text-right col-span-3">
{formatValue(value, decimals)} {symbol || ''}
</dd>
@@ -43,59 +42,35 @@ const FeesBreakdownItem = ({
);
export const FeesBreakdown = ({
totalFeeAmount,
fees,
feeEstimate,
feeFactors,
symbol,
decimals,
referralDiscountFactor,
volumeDiscountFactor,
}: {
totalFeeAmount?: string;
fees?: TradeFee;
feeEstimate: ReturnType<typeof useEstimateFees>;
feeFactors?: FeeFactors;
symbol?: string;
decimals: number;
referralDiscountFactor?: string;
volumeDiscountFactor?: string;
}) => {
const t = useT();
const { fees, totalFeeAmount, referralDiscountFactor, volumeDiscountFactor } =
feeEstimate || {};
if (!fees || !totalFeeAmount || totalFeeAmount === '0') return null;
const { discountedFee: discountedInfrastructureFee } = getDiscountedFee(
fees.infrastructureFee,
referralDiscountFactor,
volumeDiscountFactor
);
const { discountedFee: discountedLiquidityFee } = getDiscountedFee(
fees.liquidityFee,
referralDiscountFactor,
volumeDiscountFactor
);
const { discountedFee: discountedMakerFee } = getDiscountedFee(
fees.makerFee,
referralDiscountFactor,
volumeDiscountFactor
);
const {
discountedFee: discountedTotalFeeAmount,
volumeDiscount,
referralDiscount,
} = getDiscountedFee(
totalFeeAmount,
referralDiscountFactor,
volumeDiscountFactor
);
const totalDiscountFactor = getTotalDiscountFactor(feeEstimate);
const { discountedFee: discountedTotalFeeAmount, totalDiscount } =
getDiscountedFee(
totalFeeAmount,
referralDiscountFactor,
volumeDiscountFactor
);
return (
<dl className="grid grid-cols-6">
<FeesBreakdownItem
label={t('Infrastructure fee')}
factor={feeFactors?.infrastructureFee}
value={discountedInfrastructureFee}
value={fees.infrastructureFee}
symbol={symbol}
decimals={decimals}
/>
@@ -103,7 +78,7 @@ export const FeesBreakdown = ({
<FeesBreakdownItem
label={t('Liquidity fee')}
factor={feeFactors?.liquidityFee}
value={discountedLiquidityFee}
value={fees.liquidityFee}
symbol={symbol}
decimals={decimals}
/>
@@ -111,35 +86,50 @@ export const FeesBreakdown = ({
<FeesBreakdownItem
label={t('Maker fee')}
factor={feeFactors?.makerFee}
value={discountedMakerFee}
value={fees.makerFee}
symbol={symbol}
decimals={decimals}
/>
{volumeDiscountFactor && volumeDiscount !== '0' && (
<FeesBreakdownItem
label={t('Volume discount')}
factor={volumeDiscountFactor}
value={volumeDiscount}
symbol={symbol}
decimals={decimals}
/>
{totalDiscount && totalDiscount !== '0' ? (
<>
<FeesBreakdownItem
label={t('Subtotal')}
value={totalFeeAmount}
factor={
feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined
}
symbol={symbol}
decimals={decimals}
/>
<div className="col-span-6 mt-2"></div>
<FeesBreakdownItem
label={t('Discount')}
factor={totalDiscountFactor}
value={`-${totalDiscount}`}
symbol={symbol}
decimals={decimals}
/>
<FeesBreakdownItem
label={t('Total')}
value={discountedTotalFeeAmount}
symbol={symbol}
decimals={decimals}
/>
</>
) : (
<>
<div className="col-span-6 mt-2"></div>
<FeesBreakdownItem
label={t('Total')}
factor={
feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined
}
value={totalFeeAmount}
symbol={symbol}
decimals={decimals}
/>
</>
)}
{referralDiscountFactor && referralDiscount !== '0' && (
<FeesBreakdownItem
label={t('Referral discount')}
factor={referralDiscountFactor}
value={referralDiscount}
symbol={symbol}
decimals={decimals}
/>
)}
<FeesBreakdownItem
label={t('Total fees')}
factor={feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined}
value={discountedTotalFeeAmount}
symbol={symbol}
decimals={decimals}
/>
</dl>
);
};
@@ -32,15 +32,19 @@ export const mapFormValuesToOrderSubmission = (
? toNanoSeconds(order.expiresAt)
: undefined,
postOnly:
order.type === Schema.OrderType.TYPE_MARKET ? false : order.postOnly,
reduceOnly:
order.type === Schema.OrderType.TYPE_LIMIT &&
![
order.type === Schema.OrderType.TYPE_MARKET ||
[
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(order.timeInForce)
? false
: order.reduceOnly,
: order.postOnly,
reduceOnly: ![
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(order.timeInForce)
? false
: order.reduceOnly,
icebergOpts:
order.type === Schema.OrderType.TYPE_LIMIT &&
isPersistentOrder(order.timeInForce) &&
@@ -98,4 +98,91 @@ describe('mapFormValuesToOrderSubmission', () => {
).size
).toEqual('1000');
});
it.each([
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK, postOnly: false },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GFA, postOnly: true },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GFN, postOnly: true },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC, postOnly: true },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT, postOnly: true },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_IOC, postOnly: false },
])(
'sets postOnly correctly when TIF is $timeInForce',
({
timeInForce,
postOnly,
}: {
timeInForce: OrderTimeInForce;
postOnly: boolean;
}) => {
expect(
mapFormValuesToOrderSubmission(
{
type: OrderType.TYPE_LIMIT,
timeInForce,
postOnly: true,
} as OrderFormValues,
'marketId',
2,
2
).postOnly
).toEqual(postOnly);
// sets always false if type is market
expect(
mapFormValuesToOrderSubmission(
{
type: OrderType.TYPE_MARKET,
timeInForce,
postOnly: true,
} as OrderFormValues,
'marketId',
2,
2
).postOnly
).toEqual(false);
}
);
it.each([
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK, reduceOnly: true },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GFA, reduceOnly: false },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GFN, reduceOnly: false },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC, reduceOnly: false },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT, reduceOnly: false },
{ timeInForce: OrderTimeInForce.TIME_IN_FORCE_IOC, reduceOnly: true },
])(
'sets reduceOnly correctly when TIF is $timeInForce',
({
timeInForce,
reduceOnly,
}: {
timeInForce: OrderTimeInForce;
reduceOnly: boolean;
}) => {
expect(
mapFormValuesToOrderSubmission(
{
type: OrderType.TYPE_MARKET,
timeInForce,
reduceOnly: true,
} as OrderFormValues,
'marketId',
2,
2
).reduceOnly
).toEqual(reduceOnly);
expect(
mapFormValuesToOrderSubmission(
{
type: OrderType.TYPE_LIMIT,
timeInForce,
reduceOnly: true,
} as OrderFormValues,
'marketId',
2,
2
).reduceOnly
).toEqual(reduceOnly);
}
);
});
@@ -216,12 +216,10 @@ const ApprovalTxFeedback = ({
<p>
{t(
'You approved deposits of up to {{assetSymbol}} {{approvedAllowanceValue}}.',
[
{
assetSymbol: selectedAsset?.symbol,
approvedAllowanceValue,
},
]
{
assetSymbol: selectedAsset?.symbol,
approvedAllowanceValue,
}
)}
</p>
{txLink && <p>{txLink}</p>}
@@ -1,15 +0,0 @@
export const useTranslation = () => ({
t: (label: string, replacements?: Record<string, string>) => {
const replace =
replacements?.replace && typeof replacements === 'object'
? replacements?.replace
: replacements;
let translatedLabel = replacements?.defaultValue || label;
if (typeof replace === 'object' && replace !== null) {
Object.keys(replace).forEach((key) => {
translatedLabel = translatedLabel.replace(`{{${key}}}`, replace[key]);
});
}
return translatedLabel;
},
});
@@ -150,7 +150,7 @@ export const useEnvironment = create<EnvStore>()((set, get) => ({
* Initialize Vega app to dynamically select a node from the
* VEGA_CONFIG_URL
*
* This can be ommitted if you intend to only use a single node,
* This can be omitted if you intend to only use a single node,
* in those cases be sure to set NX_VEGA_URL
*/
export const useInitializeEnv = () => {
@@ -415,6 +415,12 @@ function compileFeatureFlags(): FeatureFlags {
REFERRALS: TRUTHY.includes(
windowOrDefault('NX_REFERRALS', process.env['NX_REFERRALS']) as string
),
DISABLE_CLOSE_POSITION: TRUTHY.includes(
windowOrDefault(
'NX_DISABLE_CLOSE_POSITION',
process.env['NX_DISABLE_CLOSE_POSITION']
) as string
),
UPDATE_MARKET_STATE: TRUTHY.includes(
windowOrDefault(
'NX_UPDATE_MARKET_STATE',
@@ -70,10 +70,7 @@ export const useNodeHealth = () => {
);
intent = Intent.Danger;
} else if (blockDiff >= BLOCK_THRESHOLD) {
text = t('blocksBehind', {
defaultValue: '{{count}} Blocks behind',
replace: { count: blockDiff },
});
text = t('blocksBehind', '{{count}} Blocks behind', { count: blockDiff });
intent = Intent.Warning;
} else if (blockUpdateMsLatency > WARNING_LATENCY) {
text = t(
+14
View File
@@ -5,6 +5,20 @@
import '@testing-library/jest-dom';
import ResizeObserver from 'resize-observer-polyfill';
import { locales } from '@vegaprotocol/i18n';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
// Set up i18n instance so that components have the correct default
// en translations
i18n.use(initReactI18next).init({
// we init with resources
resources: locales,
fallbackLng: 'en',
ns: ['environment'],
defaultNS: 'environment',
});
global.ResizeObserver = ResizeObserver;
// Required by radix-ui/react-dropdown-menu
+1
View File
@@ -27,6 +27,7 @@ export type CosmicElevatorFlags = Pick<
| 'UPDATE_MARKET_STATE'
| 'GOVERNANCE_TRANSFERS'
| 'VOLUME_DISCOUNTS'
| 'DISABLE_CLOSE_POSITION'
>;
export type Configuration = z.infer<typeof tomlConfigSchema>;
export const CUSTOM_NODE_KEY = 'custom' as const;
@@ -81,6 +81,7 @@ const COSMIC_ELEVATOR_FLAGS = {
UPDATE_MARKET_STATE: z.optional(z.boolean()),
GOVERNANCE_TRANSFERS: z.optional(z.boolean()),
VOLUME_DISCOUNTS: z.optional(z.boolean()),
DISABLE_CLOSE_POSITION: z.optional(z.boolean()),
};
const EXPLORER_FLAGS = {
+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 = ({
@@ -1,6 +1,6 @@
import { memo, forwardRef, useMemo, type ForwardedRef } from 'react';
import {
MAXGOINT64,
HALFMAXGOINT64,
addDecimalsFormatNumber,
getDateTimeFormat,
isNumeric,
@@ -151,7 +151,7 @@ export const OrderListTable = memo<
: '';
if (
data.size === MAXGOINT64 &&
data.size >= HALFMAXGOINT64 &&
data.timeInForce ===
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC &&
data.reduceOnly
@@ -3,7 +3,7 @@ import { PositionsManager } from './positions-manager';
import { positionsMarketsProvider } from './positions-data-providers';
import { singleRow } from './positions.mock';
import { MockedProvider } from '@apollo/client/testing';
import { MAXGOINT64 } from '@vegaprotocol/utils';
import { HALFMAXGOINT64 } from '@vegaprotocol/utils';
const mockCreate = jest.fn();
@@ -31,9 +31,7 @@ jest.mock('@vegaprotocol/data-provider', () => ({
}));
describe('PositionsManager', () => {
// TODO: Close position temporarily disabled in https://github.com/vegaprotocol/frontend-monorepo/pull/5350
// eslint-disable-next-line jest/no-disabled-tests
it.skip('should close position with max uint64', async () => {
it('should close position with half of max uint64', async () => {
render(<PositionsManager partyIds={['partyId']} isReadOnly={false} />, {
wrapper: MockedProvider,
});
@@ -43,6 +41,6 @@ describe('PositionsManager', () => {
expect(
mockCreate.mock.lastCall[0].batchMarketInstructions.submissions[0].size
).toEqual(MAXGOINT64);
).toEqual(HALFMAXGOINT64);
});
});
+34 -39
View File
@@ -7,13 +7,11 @@ import {
} from './positions-data-providers';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { useT } from '../use-t';
// TODO: Close position temporarily disabled in https://github.com/vegaprotocol/frontend-monorepo/pull/5350
//
// import { useCallback } from 'react';
// import * as Schema from '@vegaprotocol/types';
// import { useVegaTransactionStore } from '@vegaprotocol/web3';
// import { MAXGOINT64 } from '@vegaprotocol/utils';
import { useCallback } from 'react';
import * as Schema from '@vegaprotocol/types';
import { useVegaTransactionStore } from '@vegaprotocol/web3';
import { HALFMAXGOINT64 } from '@vegaprotocol/utils';
import { FLAGS } from '@vegaprotocol/environment';
interface PositionsManagerProps {
partyIds: string[];
@@ -32,37 +30,35 @@ export const PositionsManager = ({
}: PositionsManagerProps) => {
const t = useT();
const { pubKeys, pubKey } = useVegaWallet();
const create = useVegaTransactionStore((store) => store.create);
const disableClosePositionsButton = FLAGS.DISABLE_CLOSE_POSITION;
// TODO: Close position temporarily disabled in https://github.com/vegaprotocol/frontend-monorepo/pull/5350
//
// const create = useVegaTransactionStore((store) => store.create);
//
// const onClose = useCallback(
// ({ marketId, openVolume }: { marketId: string; openVolume: string }) =>
// create({
// batchMarketInstructions: {
// cancellations: [
// {
// marketId,
// orderId: '', // omit order id to cancel all active orders
// },
// ],
// submissions: [
// {
// marketId: marketId,
// type: Schema.OrderType.TYPE_MARKET as const,
// timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC as const,
// side: openVolume.startsWith('-')
// ? Schema.Side.SIDE_BUY
// : Schema.Side.SIDE_SELL,
// size: MAXGOINT64, // improvement for avoiding leftovers filled in the meantime when close request has been sent
// reduceOnly: true,
// },
// ],
// },
// }),
// [create]
// );
const onClose = useCallback(
({ marketId, openVolume }: { marketId: string; openVolume: string }) =>
create({
batchMarketInstructions: {
cancellations: [
{
marketId,
orderId: '', // omit order id to cancel all active orders
},
],
submissions: [
{
marketId: marketId,
type: Schema.OrderType.TYPE_MARKET as const,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC as const,
side: openVolume.startsWith('-')
? Schema.Side.SIDE_BUY
: Schema.Side.SIDE_SELL,
size: HALFMAXGOINT64, // improvement for avoiding leftovers filled in the meantime when close request has been sent
reduceOnly: true,
},
],
},
}),
[create]
);
const { data: marketIds } = useDataProvider({
dataProvider: positionsMarketsProvider,
@@ -81,8 +77,7 @@ export const PositionsManager = ({
pubKeys={pubKeys}
rowData={data}
onMarketClick={onMarketClick}
// TODO: temporarily disable close position
// onClose={onClose}
onClose={disableClosePositionsButton ? undefined : onClose}
isReadOnly={isReadOnly}
multipleKeys={partyIds.length > 1}
overlayNoRowsTemplate={error ? error.message : t('No positions')}
+3 -10
View File
@@ -220,10 +220,7 @@ export const PositionsTable = ({
<p className="mb-2">{secondaryTooltip}</p>
<p className="mb-2">
{t('Status: {{status}}', {
nsSeparator: '*',
replace: {
status: PositionStatusMapping[args.data.status],
},
status: PositionStatusMapping[args.data.status],
})}
</p>
{POSITION_RESOLUTION_LINK && (
@@ -390,18 +387,14 @@ export const PositionsTable = ({
<>
<p className="mb-2">
{t('Realised PNL: {{value}}', {
nsSeparator: '*',
replace: { value: args.value },
value: args.value,
})}
</p>
<p className="mb-2">
{t(
'Lifetime loss socialisation deductions: {{losses}}',
{
nsSeparator: '*',
replace: {
losses: lossesFormatted,
},
losses: lossesFormatted,
}
)}
</p>
@@ -30,7 +30,7 @@ export const MarketProposalNotification = ({
</div>
);
return (
<div className="border-default min-w-min whitespace-nowrap border-l pb-1 pl-1 pr-1">
<div className="border-default min-w-min border-l pb-1 pl-1 pr-1">
<Notification
intent={Intent.Warning}
message={message}
@@ -48,6 +48,7 @@ export const ProtocolUpgradeCountdown = ({
case ProtocolUpgradeCountdownMode.IN_BLOCKS:
countdown = (
<Trans
i18nKey="numberOfBlocks"
defaults="<0>{{count}}</0> blocks"
components={[<span className={emphasis}>count</span>]}
values={{
@@ -43,6 +43,7 @@ export const ProtocolUpgradeProposalNotification = ({
case ProtocolUpgradeCountdownMode.IN_BLOCKS:
countdown = (
<Trans
i18nKey="numberOfBlocks"
defaults="<0>{{count}}</0> blocks"
components={[<span className="text-vega-orange-500">count</span>]}
values={{
@@ -1,7 +1,26 @@
export const IconGlobe = ({ size = 24 }: { size: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 24 24">
<path d="M11.9946 3C10.2165 3.00106 8.47842 3.52883 6.99987 4.51677C5.51983 5.5057 4.36628 6.91131 3.68509 8.55585C3.0039 10.2004 2.82567 12.01 3.17294 13.7558C3.5202 15.5016 4.37737 17.1053 5.63604 18.364C6.89471 19.6226 8.49836 20.4798 10.2442 20.8271C11.99 21.1743 13.7996 20.9961 15.4442 20.3149C17.0887 19.6337 18.4943 18.4802 19.4832 17.0001C20.4722 15.5201 21 13.78 21 12C21 9.61305 20.0518 7.32386 18.364 5.63604C16.6761 3.94821 14.3869 3 12 3C12.0001 3 11.9999 3 12 3M11.9959 3.936C11.9972 3.936 11.9985 3.936 11.9999 3.936C13.2976 3.93617 14.3772 5.83592 14.9515 8.22998H9.04712C9.62068 5.83664 10.6991 3.93971 11.9959 3.936ZM9.8073 4.24157C9.05208 5.16925 8.44481 6.56185 8.07534 8.22998H4.87438C5.24741 7.52551 5.72602 6.87397 6.3 6.3C7.28288 5.31711 8.49319 4.61388 9.8073 4.24157ZM4.42885 9.22998C4.10667 10.1091 3.93685 11.0458 3.936 12C3.936 12.9499 4.10378 13.8872 4.42669 14.77H7.88922C7.75324 13.8973 7.67969 12.9663 7.67969 12C7.67969 11.0336 7.7527 10.1027 7.8879 9.22998H4.42885ZM4.87153 15.77C5.00006 16.013 5.14133 16.2501 5.29503 16.4801C6.18112 17.8062 7.44054 18.8398 8.91404 19.4502C9.20977 19.5727 9.51146 19.677 9.81744 19.763C9.06048 18.8354 8.44956 17.4409 8.07765 15.77H4.87153ZM14.1834 19.7628C15.5101 19.3896 16.7227 18.6815 17.7021 17.7021C18.2744 17.1298 18.7541 16.4778 19.1285 15.77H15.9224C15.5508 17.4416 14.9402 18.8355 14.1834 19.7628ZM19.5733 14.77C19.7153 14.3819 19.8278 13.9819 19.9091 13.5732C20.1981 12.12 20.0808 10.6174 19.5733 9.22998H16.1106C16.2463 10.1024 16.3197 11.0333 16.3197 12C16.3197 12.9667 16.2463 13.8976 16.1106 14.77H19.5733ZM19.1285 8.22998C18.5047 7.05058 17.596 6.04063 16.4801 5.29503C15.7711 4.82129 14.9955 4.46564 14.1834 4.23723C14.9402 5.16453 15.5508 6.55844 15.9224 8.22998H19.1285ZM8.60129 12C8.60129 11.0806 8.68603 10.1352 8.84194 9.22998H15.1569C15.3132 10.1358 15.3981 11.0814 15.3981 12C15.3981 12.9186 15.314 13.8642 15.1588 14.77H8.84003C8.68519 13.8648 8.60129 12.9194 8.60129 12ZM11.9997 20.064C10.6916 20.064 9.61486 18.1657 9.04394 15.77H14.9547C14.3836 18.1642 13.3072 20.064 11.9997 20.064Z" />
<path
d="M12 1.248C14.1265 1.248 16.2053 1.87859 17.9735 3.06004C19.7417 4.24148 21.1198 5.92072 21.9336 7.88539C22.7474 9.85006 22.9603 12.0119 22.5454 14.0976C22.1305 16.1833 21.1065 18.0991 19.6028 19.6028C18.0991 21.1065 16.1833 22.1305 14.0976 22.5454C12.0119 22.9603 9.85006 22.7473 7.88539 21.9336C5.92072 21.1198 4.24149 19.7416 3.06004 17.9735C1.8786 16.2053 1.248 14.1265 1.248 12C1.25055 9.14917 2.38416 6.41584 4.4 4.39999C6.41584 2.38415 9.14918 1.25054 12 1.248ZM12 0C9.62663 0 7.30655 0.703786 5.33316 2.02236C3.35977 3.34094 1.8217 5.21508 0.91345 7.4078C0.00519871 9.60051 -0.23244 12.0133 0.230582 14.3411C0.693605 16.6689 1.83649 18.807 3.51472 20.4853C5.19295 22.1635 7.33115 23.3064 9.65892 23.7694C11.9867 24.2324 14.3995 23.9948 16.5922 23.0866C18.7849 22.1783 20.6591 20.6402 21.9776 18.6668C23.2962 16.6935 24 14.3734 24 12C24 8.8174 22.7357 5.76515 20.4853 3.51472C18.2349 1.26428 15.1826 0 12 0Z"
fill="currentColor"
/>
<path
d="M12 1.248C14.592 1.248 16.5312 6.9312 16.5312 12C16.5312 17.0688 14.6112 22.752 12 22.752C9.38879 22.752 7.46879 17.0784 7.46879 12C7.46879 6.9216 9.40799 1.248 12 1.248ZM12 0C8.81279 0 6.23999 5.376 6.23999 12C6.23999 18.624 8.83199 24 12 24C15.168 24 17.76 18.6336 17.76 12C17.76 5.3664 15.168 0 12 0Z"
fill="currentColor"
/>
<path
d="M1.229 7.64001H22.7714"
stroke="currentColor"
strokeWidth="1.3"
strokeMiterlimit="10"
/>
<path
d="M1.229 16.36H22.7714"
stroke="currentColor"
strokeWidth="1.3"
strokeMiterlimit="10"
/>
</svg>
);
};
@@ -1,9 +1,8 @@
export const IconMoon = ({ size = 16 }: { size: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 16 16">
<path d="M8 1C4.13 1 1 4.13 1 8C1 11.87 4.13 15 8 15C11.87 15 15 11.87 15 8C15 4.13 11.87 1 8 1ZM8.66 12.44H7.32V11.1H8.66V12.44ZM10.38 6.78C10.29 7.01 10.18 7.2 10.05 7.36C9.92 7.52 9.75 7.7 9.53 7.91C9.3 8.14 9.11 8.34 8.98 8.51C8.85 8.68 8.73 8.89 8.64 9.14C8.55 9.37 8.51 9.65 8.51 9.96V10.04V10.13H7.3V10.04C7.3 9.61 7.36 9.24 7.47 8.92C7.58 8.61 7.71 8.34 7.87 8.13C8.03 7.92 8.22 7.69 8.47 7.43C8.75 7.13 8.96 6.87 9.09 6.66C9.22 6.46 9.28 6.2 9.28 5.88C9.28 5.47 9.16 5.16 8.93 4.93C8.7 4.7 8.38 4.58 7.96 4.58C7.6 4.58 7.28 4.68 7.01 4.89C6.75 5.09 6.56 5.44 6.45 5.96C6.34 6.48 6.43 6.06 6.43 6.06L5.26 5.62L5.28 5.54C5.47 4.87 5.81 4.35 6.29 4.02C6.77 3.69 7.34 3.53 8 3.53C8.75 3.53 9.37 3.75 9.82 4.18C10.28 4.62 10.5 5.22 10.5 5.97C10.5 6.27 10.46 6.53 10.37 6.76L10.38 6.78Z" />
<circle cx="8" cy="8" r="7" />
<path d="M6.15393 5.69232C6.15393 5.10304 6.24054 4.5075 6.46161 4C4.99179 4.63982 4 6.14089 4 7.84607C4 10.1402 5.85982 12 8.15393 12C9.85911 12 11.3602 11.0082 12 9.53839C11.4925 9.75946 10.8964 9.84607 10.3077 9.84607C8.01357 9.84607 6.15393 7.98643 6.15393 5.69232Z" />
// TODO: we need to rescale the icon in an svg editor so the view box is the default 0 0 16 16
<svg width={size} height={size} viewBox="0 0 45 45">
<path d="M28.75 11.69A12.39 12.39 0 0 0 22.5 10a12.5 12.5 0 1 0 0 25c2.196 0 4.353-.583 6.25-1.69A12.46 12.46 0 0 0 35 22.5a12.46 12.46 0 0 0-6.25-10.81Zm-6.25 22a11.21 11.21 0 0 1-11.2-11.2 11.21 11.21 0 0 1 11.2-11.2c1.246 0 2.484.209 3.66.62a13.861 13.861 0 0 0-5 10.58 13.861 13.861 0 0 0 5 10.58 11.078 11.078 0 0 1-3.66.63v-.01Z" />
</svg>
);
};
@@ -0,0 +1,17 @@
export const IconSun = ({ size = 16 }: { size: number }) => {
return (
// TODO: we need to rescale the icon in an svg editor so the view box is the default 0 0 16 16
<svg width={size} height={size} viewBox="0 0 45 45">
<path
d="M22.5 27.79a5.29 5.29 0 1 0 0-10.58 5.29 5.29 0 0 0 0 10.58Z"
fill="currentColor"
/>
<path
d="M15.01 22.5H10M35 22.5h-5.01M22.5 29.99V35M22.5 10v5.01M17.21 27.79l-3.55 3.55M31.34 13.66l-3.55 3.55M27.79 27.79l3.55 3.55M13.66 13.66l3.55 3.55"
stroke="currentColor"
strokeWidth="1.3"
strokeMiterlimit="10"
/>
</svg>
);
};
@@ -31,6 +31,7 @@ import { IconPlus } from './svg-icons/icon-plus';
import { IconQuestionMark } from './svg-icons/icon-question-mark';
import { IconSearch } from './svg-icons/icon-search';
import { IconStar } from './svg-icons/icon-star';
import { IconSun } from './svg-icons/icon-sun';
import { IconTick } from './svg-icons/icon-tick';
import { IconTicket } from './svg-icons/icon-ticket';
import { IconTransfer } from './svg-icons/icon-transfer';
@@ -75,6 +76,7 @@ export enum VegaIconNames {
QUESTION_MARK = 'question-mark',
SEARCH = 'search',
STAR = 'star',
SUN = 'sun',
TICK = 'tick',
TICKET = 'ticket',
TRANSFER = 'transfer',
@@ -125,6 +127,7 @@ export const VegaIconNameMap: Record<
plus: IconPlus,
search: IconSearch,
star: IconStar,
sun: IconSun,
tick: IconTick,
ticket: IconTicket,
transfer: IconTransfer,
@@ -5,7 +5,7 @@ import { VegaIconNameMap } from './vega-icon-record';
export interface VegaIconProps {
name: VegaIconNames;
size?: 8 | 10 | 12 | 13 | 14 | 16 | 18 | 20 | 24 | 32;
size?: 8 | 10 | 12 | 13 | 14 | 16 | 18 | 20 | 24 | 28 | 32;
}
export const VegaIcon = ({ size = 16, name }: VegaIconProps) => {
+1
View File
@@ -19,6 +19,7 @@ export * from './indicator';
export * from './input';
export * from './input-error';
export * from './key-value-table';
export * from './language-selector';
export * from './link';
export * from './loader';
export * from './lozenge';
@@ -0,0 +1 @@
export * from './language-selector';
@@ -0,0 +1,50 @@
import { VegaIcon, VegaIconNames } from '../icon';
import {
TradingDropdown,
TradingDropdownContent,
TradingDropdownItem,
TradingDropdownTrigger,
} from '../trading-dropdown';
const labels: Record<string, string> = {
en: 'English',
es: 'Español',
ru: 'Pусский',
ko: '한국인',
zh: '简体中文',
vi: 'Tiếng Việt',
};
export const LanguageSelector = ({
languages,
onSelect,
}: {
languages: readonly string[];
onSelect: (selectedLanguage: string) => void;
}) => {
return (
<TradingDropdown
trigger={
<TradingDropdownTrigger data-testid="language-selector-trigger">
<button className="flex justify-center items-center hover:bg-vega-clight-500 dark:hover:bg-vega-cdark-500 p-1 rounded-full w-7 h-7">
<VegaIcon name={VegaIconNames.GLOBE} size={16} />
</button>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{languages.map((language) => {
return (
<TradingDropdownItem
key={language}
data-testid={`language-selector-trigger-${language}`}
onSelect={() => onSelect(language)}
>
{labels[language] || language}
</TradingDropdownItem>
);
})}
</TradingDropdownContent>
</TradingDropdown>
);
};
@@ -93,7 +93,7 @@ export const Notification = ({
</div>
<div
className={classNames(
'flex flex-col items-start overflow-hidden gap-0 mt-1',
'flex flex-col items-start overflow-hidden gap-0',
'text-vega-clight-50 dark:text-vega-cdark-50',
'font-alpha',
{ 'text-sm': size === 'small', 'text-base': size === 'medium' }
@@ -1,27 +0,0 @@
type IconProps = {
className?: string;
};
export const SunIcon = ({ className }: IconProps) => (
<svg viewBox="0 0 45 45" className={className || 'w-8 h-8'}>
<path
d="M22.5 27.79a5.29 5.29 0 1 0 0-10.58 5.29 5.29 0 0 0 0 10.58Z"
fill="currentColor"
/>
<path
d="M15.01 22.5H10M35 22.5h-5.01M22.5 29.99V35M22.5 10v5.01M17.21 27.79l-3.55 3.55M31.34 13.66l-3.55 3.55M27.79 27.79l3.55 3.55M13.66 13.66l3.55 3.55"
stroke="currentColor"
strokeWidth="1.3"
strokeMiterlimit="10"
/>
</svg>
);
export const MoonIcon = ({ className }: IconProps) => (
<svg viewBox="0 0 45 45" className={className || 'w-8 h-8'}>
<path
d="M28.75 11.69A12.39 12.39 0 0 0 22.5 10a12.5 12.5 0 1 0 0 25c2.196 0 4.353-.583 6.25-1.69A12.46 12.46 0 0 0 35 22.5a12.46 12.46 0 0 0-6.25-10.81Zm-6.25 22a11.21 11.21 0 0 1-11.2-11.2 11.21 11.21 0 0 1 11.2-11.2c1.246 0 2.484.209 3.66.62a13.861 13.861 0 0 0-5 10.58 13.861 13.861 0 0 0 5 10.58 11.078 11.078 0 0 1-3.66.63v-.01Z"
fill="currentColor"
/>
</svg>
);
@@ -1,2 +1 @@
export * from './theme-switcher';
export * from './icons';
@@ -1,7 +1,8 @@
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { SunIcon, MoonIcon } from './icons';
import { Toggle } from '../toggle';
import { useT } from '../../use-t';
import classNames from 'classnames';
import { VegaIcon, VegaIconNames } from '../icon';
export const ThemeSwitcher = ({
className,
@@ -16,12 +17,15 @@ export const ThemeSwitcher = ({
<button
type="button"
onClick={() => setTheme()}
className={className}
className={classNames(
'flex justify-center items-center hover:bg-vega-clight-500 dark:hover:bg-vega-cdark-500 rounded-full w-7 h-7',
className
)}
data-testid="theme-switcher"
id="theme-switcher"
>
{theme === 'dark' && <SunIcon />}
{theme === 'light' && <MoonIcon />}
{theme === 'dark' && <VegaIcon name={VegaIconNames.SUN} size={28} />}
{theme === 'light' && <VegaIcon name={VegaIconNames.MOON} size={28} />}
</button>
);
const toggles = [
+4
View File
@@ -1 +1,5 @@
export const MAXGOINT64 = '9223372036854775807';
// The fix for the close position functionality needs MaxInt64/2 for the size
// Issue: https://github.com/vegaprotocol/vega/issues/10177
// Core PR: https://github.com/vegaprotocol/vega/pull/10178
export const HALFMAXGOINT64 = '4611686018427387903';
@@ -110,8 +110,7 @@ export const TransactionContent = ({
return (
<p className="break-all">
{t('Error: {{errorMessage}}', {
nsSeparator: '*',
replace: { errorMessage },
errorMessage,
})}
</p>
);
@@ -66,8 +66,11 @@ const EthTransactionDetails = ({ tx }: { tx: EthStoredTxState }) => {
<>
<p className="mt-[2px]">
{t(
'Awaiting confirmations {{confirmations}}/{[requiredConfirmations}}',
tx
'Awaiting confirmations {{confirmations}}/{{requiredConfirmations}}',
{
confirmations: tx.confirmations,
requiredConfirmations: tx.requiredConfirmations,
}
)}
</p>
<ProgressBar
@@ -51,10 +51,7 @@ export const useEthWithdrawApprovalsManager = () => {
update(transaction.id, {
status: ApprovalStatus.Error,
message: t(`Invalid asset source: {{source}}`, {
nsSeparator: '*',
replace: {
source: withdrawal.asset.source.__typename,
},
source: withdrawal.asset.source.__typename,
}),
failureReason: WithdrawalFailure.InvalidAsset,
});
@@ -21,6 +21,7 @@ import type {
} from './__generated__/Orders';
import type { VegaStoredTxState } from './use-vega-transaction-store';
import { VegaTxStatus } from './types';
import { type TFunction } from 'i18next';
jest.mock('@vegaprotocol/assets', () => {
const A1 = {
@@ -418,7 +419,7 @@ describe('getVegaTransactionContentIntent', () => {
});
});
describe('getOrderToastTitle', () => {
const t = (v: string) => v;
const t = ((v: string) => v) as TFunction<'web3', undefined>;
it('should return the correct title', () => {
expect(getOrderToastTitle(Types.OrderStatus.STATUS_ACTIVE, t)).toBe(
'Order submitted'
@@ -495,7 +496,7 @@ describe('getRejectionReason', () => {
marketId: '',
remaining: '',
},
(v) => v
((v) => v) as TFunction<'web3', undefined>
)
).toBe('Insufficient asset balance');
});
@@ -515,7 +516,7 @@ describe('getRejectionReason', () => {
marketId: '',
remaining: '',
},
(v) => v
((v) => v) as TFunction<'web3', undefined>
)
).toBe('Your {{timeInForce}} order was not filled and it has been stopped');
});
@@ -41,7 +41,7 @@ import {
toBigNum,
truncateByChars,
useFormatTrigger,
MAXGOINT64,
HALFMAXGOINT64,
} from '@vegaprotocol/utils';
import { useAssetsMapProvider } from '@vegaprotocol/assets';
import { useEthWithdrawApprovalsStore } from './use-ethereum-withdraw-approvals-store';
@@ -779,16 +779,10 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
<p>
{tx.order.status === Schema.OrderStatus.STATUS_STOPPED
? t('Your order has been stopped because: {{rejectionReason}}', {
nsSeparator: '*',
replace: {
rejectionReason,
},
rejectionReason,
})
: t('Your order has been rejected because: {{rejectionReason}}', {
nsSeparator: '*',
replace: {
rejectionReason,
},
rejectionReason,
})}
</p>
) : (
@@ -1020,7 +1014,7 @@ export const getVegaTransactionContentIntent = (tx: VegaStoredTxState) => {
tx.order &&
tx.order.status === Schema.OrderStatus.STATUS_STOPPED &&
tx.order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC &&
tx.order.size === MAXGOINT64 &&
tx.order.size >= HALFMAXGOINT64 &&
// isClosePositionTransaction(tx) &&
Intent.Success;

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