Compare commits

...
Author SHA1 Message Date
Matthew Russell b9d7a232a6 chore: release 0.20.0-core-0.72.14 2023-09-04 18:36:48 -07:00
Matthew Russell 678c9090c7 chore(trading): update flags for 0.72.14 (#4697) 2023-09-04 18:33:58 -07:00
Matthew Russell 21bb6a69c6 chore(trading): minor style tweaks (#4696) 2023-09-04 17:21:46 -07:00
Bartłomiej Głownia 467ed5d53d feat(deal-ticket): make margin values section expandable (#4664) 2023-09-04 17:05:53 -07:00
m.rayandMatthew Russell d268088e60 chore(trading): update dropdowns (#4694)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-09-04 20:25:24 +00:00
Matthew Russell 1208d3f2a8 feat(market-depth): additional orderbook grouping levels (#4658) 2023-09-04 07:47:27 -07:00
Sam Keen b5ba4f99d9 fix(governance,utils,react-helpers): token locale formatting issue (#4678) 2023-09-04 12:09:38 +01:00
Joe Tsang 66f25603d3 test(explorer): add e2e test for explorer oracles (#4657) 2023-09-04 09:41:04 +00:00
Edd 4afd469404 chore(explorer,trading,governance): reconfigure hooks (#4655) 2023-09-04 09:36:13 +00:00
Maciek 9e5bc9c8d1 chore(trading): handle negative decimals (#4659) 2023-09-04 09:46:02 +02:00
Matthew Russell e41aff88b1 chore(trading): make top traded default sort for market selector (#4637) 2023-09-03 11:15:41 -07:00
Matthew Russell 9209074332 chore(trading): change pane context buttons color (#4680) 2023-09-01 17:00:11 -07:00
Art 39e5836edb fix(wallet): check if snaps are supported (#4671) 2023-09-01 11:22:13 +00:00
Art 85a4981700 fix(proposals): protocol upgrade notification block querying (#4665) 2023-09-01 12:30:01 +02:00
Ben 247927e939 chore(trading): get started specs update (#4681) 2023-09-01 10:55:56 +01:00
Maciek 6523490d96 chore(trading): 4349 delayed telemetry opt in (#4642) 2023-09-01 09:00:20 +00:00
daro-maj 105a758e8d test(trading): add stop order oco spec (#4669) 2023-09-01 10:48:38 +02:00
Matthew Russell 559ef48d6d feat(trading): orderbook changes (#4652) 2023-08-31 13:34:13 -07:00
Bartłomiej Głownia 255c3752f2 feat(deal-ticket): fix flaky tests (#4667) 2023-08-31 10:41:58 -07:00
Joe Tsang 2bbf1e2b81 chore(ci): update config.hcl file (#4677) 2023-08-31 10:35:46 -07:00
Maciek 06d3ef8d73 fix: de-polish and fix links to mozilla (#4670) 2023-08-31 14:09:41 +00:00
ArtandDariusz Majcherczyk de4c7926c2 chore(deposits): formatting tweaks (#4648)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
2023-08-31 15:30:03 +02:00
124 changed files with 2013 additions and 1321 deletions
-3
View File
@@ -3,6 +3,3 @@
# Lint commit messages to ensure they follow conventional commit standards
yarn commitlint --edit "${1}"
# Lint all staged files
yarn lint-staged
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Lint all staged files
yarn lint-staged
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Lint all staged files
yarn nx format:check
# Test all projects with changes
yarn nx affected -t test --exclude trading
@@ -0,0 +1,68 @@
context('Oracle page', { tags: '@smoke' }, () => {
describe('Verify elements on page', () => {
before('create market and navigate to oracle page', () => {
cy.createMarket();
cy.visit('/oracles');
});
it('should see oracle data', () => {
cy.getByTestId('oracle-details').should('have.length.at.least', 2);
cy.getByTestId('oracle-details')
.should('exist')
.eq(0)
.within(() => {
cy.get('tr')
.eq(0)
.within(() => {
cy.get('th').should('have.text', 'ID');
cy.get('a').invoke('text').should('have.length', 64);
cy.get('a')
.should('have.attr', 'href')
.and('contain', '/oracles/');
});
cy.get('tr')
.eq(1)
.within(() => {
cy.get('th').should('have.text', 'Type');
cy.get('td').should('have.text', 'External data');
});
cy.get('tr')
.eq(2)
.within(() => {
cy.get('th').should('have.text', 'Signer');
cy.getByTestId('keytype').should('have.text', 'Vega');
cy.get('a').invoke('text').should('have.length', 64);
cy.get('a')
.should('have.attr', 'href')
.and('contain', '/parties/');
});
cy.get('tr')
.eq(3)
.within(() => {
cy.get('th').should('have.text', 'Settlement for');
cy.get('a').invoke('text').should('have.length', 64);
cy.get('a')
.should('have.attr', 'href')
.and('contain', '/markets/');
});
cy.get('tr')
.eq(4)
.within(() => {
cy.get('th').should('have.text', 'Matched data');
cy.get('td').should('have.text', '❌');
});
cy.get('details')
.eq(0)
.within(() => {
cy.contains('Filter').click();
cy.get('.language-json').should('exist');
});
cy.get('details')
.eq(1)
.within(() => {
cy.contains('JSON').click();
cy.get('.language-json').should('exist');
});
});
});
});
});
@@ -38,7 +38,12 @@ const Oracles = () => {
const dataConnection = o?.node.dataConnection;
return (
<div id={id} key={id} className="mb-10">
<div
id={id}
key={id}
className="mb-10"
data-testid="oracle-details"
>
<OracleDetails
id={id}
dataSource={o?.node}
+2 -2
View File
@@ -23,7 +23,7 @@ NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
#Test configuration variables
CYPRESS_FAIRGROUND=false
@@ -31,4 +31,4 @@ LC_ALL="en_US.UTF-8"
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=true
+2 -2
View File
@@ -20,7 +20,7 @@ NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_VEGA_REST_URL=http://localhost:3008/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
@@ -30,4 +30,4 @@ CYPRESS_FAIRGROUND=false
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
NX_METAMASK_SNAPS=false
+2 -2
View File
@@ -15,11 +15,11 @@ NX_VEGA_REST_URL=https://api.n00.devnet1.vega.xyz/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=true
+1 -1
View File
@@ -15,7 +15,7 @@ NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.vega.community/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
+1 -1
View File
@@ -14,7 +14,7 @@ NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-mirror-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.mainnet-mirror.vega.rocks/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
+1 -1
View File
@@ -11,7 +11,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
+1 -1
View File
@@ -16,7 +16,7 @@ NX_VEGA_REST_URL=https://api.n07.testnet.vega.xyz/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+1 -1
View File
@@ -13,7 +13,7 @@ NX_VEGA_REST_URL=https://api-validators-testnet.vega.rocks/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
@@ -54,7 +54,7 @@ export const WalletCardRow = ({
}) => {
const ref = React.useRef<HTMLDivElement | null>(null);
useAnimateValue(ref, value);
const [integers, decimalsPlaces] = useNumberParts(value, decimals);
const [integers, decimalsPlaces, separator] = useNumberParts(value, decimals);
return (
<div
@@ -75,7 +75,10 @@ export const WalletCardRow = ({
className="font-mono flex-1 text-right"
data-testid="associated-amount"
>
<span>{integers}.</span>
<span>
{integers}
{separator}
</span>
<span>{decimalsPlaces}</span>
</span>
)}
@@ -110,7 +113,10 @@ export const WalletCardAsset = ({
border,
subheading,
}: WalletCardAssetProps) => {
const [integers, decimalsPlaces] = useNumberParts(balance, decimals);
const [integers, decimalsPlaces, separator] = useNumberParts(
balance,
decimals
);
return (
<div className="flex flex-nowrap mt-2 mb-4">
@@ -132,7 +138,10 @@ export const WalletCardAsset = ({
</div>
</div>
<div className="px-2 basis-full font-mono" data-testid="currency-value">
<span>{integers}.</span>
<span>
{integers}
{separator}
</span>
<span className="text-neutral-400">{decimalsPlaces}</span>
</div>
</div>
@@ -133,11 +133,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
validateMarketDataRow(4, 'Decimals', '5');
validateMarketDataRow(5, 'Quantum', '1');
validateMarketDataRow(6, 'Status', 'Enabled');
validateMarketDataRow(
7,
'Contract address',
'0x0158031158Bb4dF2AD02eAA31e8963E84EA978a4'
);
validateMarketDataRow(7, 'Contract address', '0x0158…78a4');
validateMarketDataRow(8, 'Withdrawal threshold', '0.0005');
validateMarketDataRow(9, 'Lifetime limit', '1,230');
validateMarketDataRow(10, 'Infrastructure fee account balance', '0.00001');
@@ -1,117 +0,0 @@
const orderbookTab = 'Orderbook';
const orderbookTable = 'tab-orderbook';
const askPrice = 'price-9894185';
const bidPrice = 'price-9889001';
const askVolume = 'ask-vol-9894185';
const bidVolume = 'bid-vol-9889001';
const askCumulative = 'cumulative-vol-9894185';
const bidCumulative = 'cumulative-vol-9889001';
const midPrice = 'middle-mark-price-4612690000';
const priceResolution = 'resolution';
const dealTicketPrice = 'order-price';
const dealTicketSize = 'order-size';
const resPrice = 'price-990';
describe('order book', { tags: '@smoke' }, () => {
before(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
beforeEach(() => {
cy.mockTradingPage();
});
it('show order book', () => {
// 6003-ORDB-001
// 6003-ORDB-002
cy.getByTestId(orderbookTab).click();
cy.getByTestId(orderbookTable).should('be.visible');
cy.getByTestId(orderbookTable).should('not.be.empty');
});
it('show orders prices', () => {
// 6003-ORDB-003
cy.getByTestId(askPrice).should('have.text', '98.94185');
cy.getByTestId(bidPrice).should('have.text', '98.89001');
});
it('show prices volumes', () => {
// 6003-ORDB-004
cy.getByTestId(askVolume).should('have.text', '1');
cy.getByTestId(bidVolume).should('have.text', '1');
});
it('show prices cumulative volumes', () => {
// 6003-ORDB-005
cy.getByTestId(askCumulative).should('have.text', '38');
cy.getByTestId(bidCumulative).should('have.text', '7');
});
it('show mid price', () => {
// 6003-ORDB-006
cy.getByTestId(midPrice).should('have.text', '46,126.90');
});
it('sort prices descending', () => {
// 6003-ORDB-007
const prices: number[] = [];
cy.getByTestId(orderbookTable).within(() => {
cy.get('[data-testid*=price]')
.each(($el) => {
prices.push(Number($el.text()));
})
.then(() => {
expect(prices).to.deep.equal(prices.sort((a, b) => b - a));
});
});
});
it('copy price to deal ticket form', () => {
// 6003-ORDB-009
cy.getByTestId(askPrice).click();
cy.getByTestId(dealTicketPrice).should('have.value', '98.94185');
});
it('copy size to deal ticket form', () => {
// 6003-ORDB-009
cy.getByTestId(bidCumulative).click();
cy.getByTestId(dealTicketSize).should('have.value', '7');
});
it('copy size to deal ticket form', () => {
// 6003-ORDB-009
cy.getByTestId(bidVolume).click();
cy.getByTestId(dealTicketSize).should('have.value', '1');
});
it('change price resolution', () => {
// 6003-ORDB-008
const resolutions = [
'0.00000',
'0.0000',
'0.000',
'0.00',
'0.0',
'0',
'10',
'100',
'1,000',
'10,000',
];
cy.getByTestId(priceResolution).click();
cy.get('[role="menu"]')
.find('[role="menuitem"]')
.each(($el, index) => {
expect($el.text()).to.equal(resolutions[index]);
});
cy.get('[role="menuitem"]').eq(4).click();
cy.getByTestId(resPrice).should('have.text', '99.0');
cy.getByTestId(askPrice).should('not.exist');
cy.getByTestId(bidPrice).should('not.exist');
});
});
@@ -24,6 +24,9 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.getByTestId('deal-ticket-fee-margin-required').within(() => {
cy.get('button').click();
});
});
describe('limit order', () => {
@@ -43,11 +43,10 @@ describe('ethereum wallet', { tags: '@smoke', testIsolation: true }, () => {
// 0004-EWAL-005
// 0004-EWAL-006
const ethWalletAddress = Cypress.env('ETHEREUM_WALLET_ADDRESS');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
connectEthereumWallet('MetaMask');
cy.getByTestId('ethereum-address').should('have.text', ethWalletAddress);
cy.getByTestId('ethereum-address').should('have.text', '0xEe7D…d94F');
cy.getByTestId('disconnect-ethereum-wallet')
.should('have.text', 'Disconnect')
.click();
+1 -1
View File
@@ -13,7 +13,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
+1 -1
View File
@@ -13,7 +13,7 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_ETH_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supply green clown session"
+1 -1
View File
@@ -14,7 +14,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+5 -5
View File
@@ -14,14 +14,14 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-mainnet/codfcglpplgmmlokgilfkpcjnmkbfiel
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet-mainnet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet
# TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.20.21-core-0.71.6
NX_APP_VERSION=v0.21.0-core-0.72.14
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
+4 -4
View File
@@ -14,14 +14,14 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.mainnet-mirror.vega.rocks
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-mainnet/codfcglpplgmmlokgilfkpcjnmkbfiel
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet-mainnet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet
# TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.20.19-core-0.71.6
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
+2 -2
View File
@@ -14,11 +14,11 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
+1 -1
View File
@@ -15,7 +15,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+4 -4
View File
@@ -16,12 +16,12 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
@@ -1,27 +1,49 @@
import React, { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { LocalStoragePersistTabs as Tabs, Tab } from '@vegaprotocol/ui-toolkit';
import {
LocalStoragePersistTabs as Tabs,
Tab,
TradingAnchorButton,
} from '@vegaprotocol/ui-toolkit';
import { Markets } from './markets';
import { Proposed } from './proposed';
import { usePageTitleStore } from '../../stores';
import { Closed } from './closed';
import {
DApp,
TOKEN_NEW_MARKET_PROPOSAL,
useLinks,
} from '@vegaprotocol/environment';
export const MarketsPage = () => {
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
const tokenLink = useLinks(DApp.Token);
const externalLink = tokenLink(TOKEN_NEW_MARKET_PROPOSAL);
useEffect(() => {
updateTitle(titlefy(['Markets']));
}, [updateTitle]);
return (
<div className="h-full pt-0.5 pb-3 px-1.5">
<div className="h-full my-1 border border-default rounded-sm">
<div className="h-full my-1 border rounded-sm border-default">
<Tabs storageKey="console-markets">
<Tab id="open-markets" name={t('Open markets')}>
<Markets />
</Tab>
<Tab id="proposed-markets" name={t('Proposed markets')}>
<Tab
id="proposed-markets"
name={t('Proposed markets')}
menu={
<TradingAnchorButton size="extra-small" href={externalLink}>
{t('Propose a new market')}
</TradingAnchorButton>
}
>
<Proposed />
</Tab>
<Tab id="closed-markets" name={t('Closed markets')}>
+1 -19
View File
@@ -1,24 +1,6 @@
import { t } from '@vegaprotocol/i18n';
import {
DApp,
TOKEN_NEW_MARKET_PROPOSAL,
useLinks,
} from '@vegaprotocol/environment';
import { ProposalsList } from '@vegaprotocol/proposals';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { SuccessorMarketRenderer } from './successor-market-cell';
export const Proposed = () => {
const tokenLink = useLinks(DApp.Token);
const externalLink = tokenLink(TOKEN_NEW_MARKET_PROPOSAL);
return (
<>
<div className="h-[400px]">
<ProposalsList SuccessorMarketRenderer={SuccessorMarketRenderer} />
</div>
<ExternalLink className="py-4 px-[11px] text-sm" href={externalLink}>
{t('Propose a new market')}
</ExternalLink>
</>
);
return <ProposalsList SuccessorMarketRenderer={SuccessorMarketRenderer} />;
};
@@ -4,7 +4,7 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import compact from 'lodash/compact';
import uniqBy from 'lodash/uniqBy';
import type { ChangeEvent } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import type { AccountHistoryQuery } from './__generated__/AccountHistory';
import { useAccountHistoryQuery } from './__generated__/AccountHistory';
import * as Schema from '@vegaprotocol/types';
@@ -12,12 +12,13 @@ import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import {
AsyncRenderer,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Splash,
Toggle,
TradingButton,
TradingDropdown,
TradingDropdownContent,
TradingDropdownItem,
TradingDropdownTrigger,
} from '@vegaprotocol/ui-toolkit';
import { AccountTypeMapping } from '@vegaprotocol/types';
import { PriceChart } from 'pennant';
@@ -150,6 +151,7 @@ const AccountHistoryManager = ({
)
: null;
}, [accounts, marketFilterCb]);
const resolveMarket = useCallback(
(m: Market) => {
setMarket(m);
@@ -174,111 +176,114 @@ const AccountHistoryManager = ({
}),
[pubKey, asset, accountType, range, market?.id]
);
const { data } = useAccountHistoryQuery({
variables,
skip: !asset || !pubKey,
});
const accountTypeMenu = useMemo(() => {
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{accountType
? `${
AccountTypeMapping[
accountType as keyof typeof Schema.AccountType
]
} Account`
: t('Select account type')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{[
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
Schema.AccountType.ACCOUNT_TYPE_BOND,
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
].map((type) => (
<DropdownMenuItem
key={type}
onClick={() => setAccountType(type as Schema.AccountType)}
>
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}, [accountType]);
const assetsMenu = useMemo(() => {
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{asset ? asset.symbol : t('Select asset')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{assets.map((a) => (
<DropdownMenuItem key={a.id} onClick={() => setAssetId(a.id)}>
{a.symbol}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}, [asset, assets, setAssetId]);
const marketsMenu = useMemo(() => {
return accountType === Schema.AccountType.ACCOUNT_TYPE_MARGIN &&
markets?.length ? (
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{market
? market.tradableInstrument.instrument.code
: t('Select market')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{market && (
<DropdownMenuItem key="0" onClick={() => setMarket(null)}>
{t('All markets')}
</DropdownMenuItem>
)}
{markets?.map((m) => (
<DropdownMenuItem key={m.id} onClick={() => resolveMarket(m)}>
{m.tradableInstrument.instrument.code}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : null;
}, [markets, market, accountType, resolveMarket]);
useEffect(() => {
if (
accountType !== Schema.AccountType.ACCOUNT_TYPE_MARGIN ||
market?.tradableInstrument.instrument.product.settlementAsset.id !==
asset?.id
) {
setMarket(null);
}
}, [accountType, asset?.id, market]);
return (
<div className="h-full w-full flex flex-col gap-8">
<div className="w-full flex flex-col-reverse lg:flex-row items-start lg:items-center justify-between gap-4 px-2">
<div className="flex items-center gap-4 shrink-0">
<>
{accountTypeMenu}
{assetsMenu}
{marketsMenu}
</>
<div className="flex flex-col h-full gap-2">
<div className="flex flex-wrap justify-between px-1 pt-2 gap-2">
<div className="flex items-center gap-1 shrink-0">
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{accountType
? `${
AccountTypeMapping[
accountType as keyof typeof Schema.AccountType
]
} Account`
: t('Select account type')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{[
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
Schema.AccountType.ACCOUNT_TYPE_BOND,
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
].map((type) => (
<TradingDropdownItem
key={type}
onClick={() => {
setAccountType(type as Schema.AccountType);
// if not a margin account clear any market selection
if (type !== Schema.AccountType.ACCOUNT_TYPE_MARGIN) {
setMarket(null);
}
}}
>
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{asset ? asset.symbol : t('Select asset')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{assets.map((a) => (
<TradingDropdownItem
key={a.id}
onClick={() => {
setAssetId(a.id);
// if the selected asset is different to the selected market clear the market
if (
a.id !==
market?.tradableInstrument.instrument.product
.settlementAsset.id
) {
setMarket(null);
}
}}
>
{a.symbol}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{market
? market.tradableInstrument.instrument.code
: t('Select market')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{market && (
<TradingDropdownItem key="0" onClick={() => setMarket(null)}>
{t('All markets')}
</TradingDropdownItem>
)}
{markets?.map((m) => (
<TradingDropdownItem
key={m.id}
onClick={() => resolveMarket(m)}
>
{m.tradableInstrument.instrument.code}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
</div>
<div className="pt-1 justify-items-end">
<div className="justify-items-end">
<Toggle
id="account-history-date-range"
name="account-history-date-range"
@@ -287,16 +292,19 @@ const AccountHistoryManager = ({
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setRange(e.target.value as keyof typeof DateRange)
}
size="sm"
/>
</div>
</div>
<div className="h-5/6 px-4">
<div className="flex-1">
{asset && (
<AccountHistoryChart
data={data}
accountType={accountType}
asset={asset}
/>
<div className="h-full">
<AccountHistoryChart
data={data}
accountType={accountType}
asset={asset}
/>
</div>
)}
</div>
</div>
@@ -1,5 +1,5 @@
import { t } from '@vegaprotocol/i18n';
import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
import { TradingButton } from '@vegaprotocol/ui-toolkit';
import { ViewType, useSidebar } from '../sidebar';
export const AccountsMenu = () => {
@@ -8,7 +8,6 @@ export const AccountsMenu = () => {
return (
<>
<TradingButton
intent={Intent.Primary}
size="extra-small"
data-testid="open-transfer"
onClick={() => setView({ type: ViewType.Transfer })}
@@ -16,7 +15,6 @@ export const AccountsMenu = () => {
{t('Transfer')}
</TradingButton>
<TradingButton
intent={Intent.Primary}
size="extra-small"
onClick={() => setView({ type: ViewType.Deposit })}
>
@@ -1,5 +1,5 @@
import { t } from '@vegaprotocol/i18n';
import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
import { TradingButton } from '@vegaprotocol/ui-toolkit';
import { ViewType, useSidebar } from '../sidebar';
export const DepositsMenu = () => {
@@ -7,7 +7,6 @@ export const DepositsMenu = () => {
return (
<TradingButton
intent={Intent.Primary}
size="extra-small"
onClick={() => setView({ type: ViewType.Deposit })}
data-testid="deposit-button"
@@ -1,13 +1,12 @@
import { t } from '@vegaprotocol/i18n';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItemIndicator,
DropdownMenuTrigger,
VegaIcon,
VegaIconNames,
TradingDropdown,
TradingDropdownCheckboxItem,
TradingDropdownContent,
TradingDropdownItemIndicator,
TradingDropdownTrigger,
} from '@vegaprotocol/ui-toolkit';
import { MarketSelectorButton } from './market-selector-button';
type Assets = Array<{ id: string; symbol: string }>;
@@ -25,17 +24,19 @@ export const AssetDropdown = ({
}
return (
<DropdownMenu
<TradingDropdown
trigger={
<DropdownMenuTrigger data-testid="asset-trigger">
<TriggerText assets={assets} checkedAssets={checkedAssets} />
</DropdownMenuTrigger>
<TradingDropdownTrigger data-testid="asset-trigger">
<MarketSelectorButton>
{triggerText({ assets, checkedAssets })}
</MarketSelectorButton>
</TradingDropdownTrigger>
}
>
<DropdownMenuContent>
<TradingDropdownContent>
{assets?.map((a) => {
return (
<DropdownMenuCheckboxItem
<TradingDropdownCheckboxItem
key={a.id}
checked={checkedAssets.includes(a.id)}
onCheckedChange={(checked) => {
@@ -46,16 +47,16 @@ export const AssetDropdown = ({
data-testid={`asset-id-${a.id}`}
>
{a.symbol}
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
<TradingDropdownItemIndicator />
</TradingDropdownCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</TradingDropdownContent>
</TradingDropdown>
);
};
const TriggerText = ({
const triggerText = ({
assets,
checkedAssets,
}: {
@@ -72,9 +73,5 @@ const TriggerText = ({
text = t(`${checkedAssets.length} Assets`);
}
return (
<span className="flex justify-between items-center">
{text} <VegaIcon name={VegaIconNames.CHEVRON_DOWN} />
</span>
);
return text;
};
@@ -1,2 +1,3 @@
export * from './market-selector';
export * from './market-selector-item';
export * from './market-selector-button';
@@ -0,0 +1,23 @@
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import type { ButtonHTMLAttributes } from 'react';
import { forwardRef } from 'react';
export const MarketSelectorButton = forwardRef<
HTMLButtonElement,
ButtonHTMLAttributes<HTMLButtonElement>
>((props, ref) => (
<button
{...props}
className={classNames(
'flex items-center justify-between px-2 border rounded gap-1',
'border-vega-clight-600 dark:border-vega-cdark-600 bg-vega-clight-700 dark:bg-vega-cdark-700',
'text-secondary data-[state=open]:text-vega-clight-50 dark:data-[state=open]:text-vega-cdark-50'
)}
ref={ref}
>
{props.children}
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} />
</button>
));
MarketSelectorButton.displayName = 'MarketSelectorButton';
@@ -69,6 +69,7 @@ describe('MarketSelectorItem', () => {
targetStake: '1000000',
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
priceMonitoringBounds: null,
lastTradedPrice: '100',
};
const candles = [
@@ -31,7 +31,7 @@ export const MarketSelectorItem = ({
<div style={style} role="row">
<Link
to={`/markets/${market.id}`}
className={classNames('h-full flex items-center gap-2 px-4', {
className={classNames('h-full flex items-center gap-2 mx-2 px-2', {
'hover:bg-vega-clight-700 dark:hover:bg-vega-cdark-700':
market.id !== currentMarketId,
'bg-vega-clight-600 dark:bg-vega-cdark-600':
@@ -94,7 +94,7 @@ const MarketData = ({
return (
<>
<div className="w-2/5" role="gridcell">
<h3 className="text-ellipsis text-sm lg:text-base whitespace-nowrap overflow-hidden">
<h3 className="overflow-hidden text-sm text-ellipsis lg:text-base whitespace-nowrap">
{market.tradableInstrument.instrument.code}{' '}
{allProducts && productType && (
<MarketProductPill productType={productType} />
@@ -107,7 +107,7 @@ const MarketData = ({
)}
</div>
<div
className="w-1/5 text-xs lg:text-sm whitespace-nowrap text-ellipsis overflow-hidden"
className="w-1/5 overflow-hidden text-xs lg:text-sm whitespace-nowrap text-ellipsis"
title={instrument.product.settlementAsset.symbol}
data-testid="market-selector-price"
role="gridcell"
@@ -115,14 +115,14 @@ const MarketData = ({
{price} {instrument.product.settlementAsset.symbol}
</div>
<div
className="w-1/5 text-xs lg:text-sm text-right whitespace-nowrap text-ellipsis overflow-hidden"
className="w-1/5 overflow-hidden text-xs text-right lg:text-sm whitespace-nowrap text-ellipsis"
title={t('24h vol')}
data-testid="market-selector-volume"
role="gridcell"
>
{volume}
</div>
<div className="w-1/5 flex justify-end" role="gridcell">
<div className="flex justify-end w-1/5" role="gridcell">
{oneDayCandles && (
<Sparkline
width={64}
@@ -262,9 +262,7 @@ describe('MarketSelector', () => {
await userEvent.click(screen.getByTestId('sort-trigger'));
const options = screen.getAllByTestId(/sort-item/);
expect(options.map((o) => o.textContent?.trim())).toEqual(
Object.entries(Sort)
.filter(([key]) => key !== Sort.None)
.map(([key]) => SortTypeMapping[key as SortType])
Object.entries(Sort).map(([key]) => SortTypeMapping[key as SortType])
);
await userEvent.click(screen.getByTestId('sort-item-Gained'));
expect(
@@ -40,7 +40,7 @@ export const MarketSelector = ({
const [filter, setFilter] = useState<Filter>({
searchTerm: '',
product: Product.All,
sort: Sort.None,
sort: Sort.TopTraded,
assets: [],
});
const allProducts = filter.product === Product.All;
@@ -52,8 +52,8 @@ export const MarketSelector = ({
}, [reload]);
return (
<div data-testid="market-selector">
<div className="pt-2 px-2 mb-2">
<div data-testid="market-selector" className="md:w-[580px]">
<div className="px-2 pt-2 mb-2">
<ProductSelector
product={filter.product}
onSelect={(product) => {
@@ -106,9 +106,6 @@ export const MarketSelector = ({
currentSort={filter.sort}
onSelect={(sort) => {
setFilter((curr) => {
if (curr.sort === sort) {
return { ...curr, sort: Sort.None };
}
return {
...curr,
sort,
@@ -294,9 +291,9 @@ const List = ({
const Skeleton = () => {
return (
<div className="mb-2 px-2">
<div className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg p-4">
<div className="w-full h-3 bg-vega-light-200 dark:bg-vega-dark-200 mb-2" />
<div className="px-2 mb-2">
<div className="p-4 rounded-lg bg-vega-light-100 dark:bg-vega-dark-100">
<div className="w-full h-3 mb-2 bg-vega-light-200 dark:bg-vega-dark-200" />
<div className="w-2/3 h-3 bg-vega-light-200 dark:bg-vega-dark-200" />
</div>
</div>
@@ -33,11 +33,14 @@ export const ProductSelector = ({
return (
<div className="flex mb-2">
{Object.keys(Product).map((t) => {
const classes = classNames('px-3 py-1.5 rounded', {
'bg-vega-clight-500 dark:bg-vega-cdark-500 text-default':
t === product,
'text-secondary': t !== product,
});
const classes = classNames(
'text-sm px-3 py-1.5 rounded hover:text-vega-clight-50 dark:hover:text-vega-cdark-50',
{
'bg-vega-clight-500 dark:bg-vega-cdark-500 text-default':
t === product,
'text-secondary': t !== product,
}
);
return (
<button
key={t}
@@ -53,7 +56,7 @@ export const ProductSelector = ({
})}
<Link
to={Routes.MARKETS}
className="flex items-center gap-2 ml-auto"
className="flex items-center ml-auto text-sm gap-2"
title={t('See all markets')}
>
<span className="underline underline-offset-4">{t('Browse')}</span>
@@ -1,17 +1,16 @@
import { t } from '@vegaprotocol/i18n';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItemIndicator,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
TradingDropdown,
TradingDropdownContent,
TradingDropdownItemIndicator,
TradingDropdownRadioGroup,
TradingDropdownRadioItem,
TradingDropdownTrigger,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { MarketSelectorButton } from './market-selector-button';
export const Sort = {
None: 'None',
Gained: 'Gained',
Lost: 'Lost',
New: 'New',
@@ -23,17 +22,15 @@ export type SortType = keyof typeof Sort;
export const SortTypeMapping: {
[key in SortType]: string;
} = {
[Sort.None]: 'None',
[Sort.TopTraded]: 'Top traded',
[Sort.Gained]: 'Top gaining',
[Sort.Lost]: 'Top losing',
[Sort.New]: 'New markets',
[Sort.TopTraded]: 'Top traded',
};
const SortIconMapping: {
[key in SortType]: VegaIconNames;
} = {
[Sort.None]: null as unknown as VegaIconNames, // not shown in list
[Sort.Gained]: VegaIconNames.TREND_UP,
[Sort.Lost]: VegaIconNames.TREND_DOWN,
[Sort.New]: VegaIconNames.STAR,
@@ -48,43 +45,38 @@ export const SortDropdown = ({
onSelect: (sort: SortType) => void;
}) => {
return (
<DropdownMenu
<TradingDropdown
trigger={
<DropdownMenuTrigger data-testid="sort-trigger">
<span className="flex justify-between items-center">
{currentSort === SortTypeMapping.None
? t('Sort')
: SortTypeMapping[currentSort]}{' '}
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} />
</span>
</DropdownMenuTrigger>
<TradingDropdownTrigger data-testid="sort-trigger">
<MarketSelectorButton>
{SortTypeMapping[currentSort]}
</MarketSelectorButton>
</TradingDropdownTrigger>
}
>
<DropdownMenuContent>
<DropdownMenuRadioGroup
<TradingDropdownContent>
<TradingDropdownRadioGroup
value={currentSort}
onValueChange={(value) => onSelect(value as SortType)}
>
{Object.keys(Sort)
.filter((s) => s !== Sort.None)
.map((key) => {
return (
<DropdownMenuRadioItem
inset
key={key}
value={key}
data-testid={`sort-item-${key}`}
>
<span className="flex gap-2">
<VegaIcon name={SortIconMapping[key as SortType]} />{' '}
{SortTypeMapping[key as SortType]}
</span>
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
);
})}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
{Object.keys(Sort).map((key) => {
return (
<TradingDropdownRadioItem
inset
key={key}
value={key}
data-testid={`sort-item-${key}`}
>
<span className="flex gap-2">
<VegaIcon name={SortIconMapping[key as SortType]} />{' '}
{SortTypeMapping[key as SortType]}
</span>
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
);
})}
</TradingDropdownRadioGroup>
</TradingDropdownContent>
</TradingDropdown>
);
};
@@ -12,7 +12,10 @@ import { useMarketList } from '@vegaprotocol/markets';
import type { Filter } from '../../components/market-selector';
import { subDays } from 'date-fns';
jest.mock('@vegaprotocol/markets');
jest.mock('@vegaprotocol/markets', () => ({
...jest.requireActual('@vegaprotocol/markets'),
useMarketList: jest.fn(),
}));
const mockUseMarketList = useMarketList as jest.Mock;
describe('useMarketSelectorList', () => {
@@ -20,7 +23,7 @@ describe('useMarketSelectorList', () => {
const defaultArgs: Filter = {
searchTerm: '',
product: Product.Future,
sort: Sort.None,
sort: Sort.TopTraded,
assets: [],
};
return renderHook((args) => useMarketSelectorList(args), {
@@ -109,21 +112,21 @@ describe('useMarketSelectorList', () => {
rerender({
searchTerm: '',
product: Product.Spot as 'Future',
sort: Sort.None,
sort: Sort.TopTraded,
assets: [],
});
expect(result.current.markets).toEqual([markets[1]]);
rerender({
searchTerm: '',
product: Product.Perpetual as 'Future',
sort: Sort.None,
sort: Sort.TopTraded,
assets: [],
});
expect(result.current.markets).toEqual([markets[2]]);
rerender({
searchTerm: '',
product: Product.All,
sort: Sort.None,
sort: Sort.TopTraded,
assets: [],
});
expect(result.current.markets).toEqual(markets);
@@ -189,7 +192,7 @@ describe('useMarketSelectorList', () => {
const { result, rerender } = setup({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
sort: Sort.TopTraded,
assets: ['asset-0'],
});
expect(result.current.markets).toEqual([markets[0], markets[1]]);
@@ -197,7 +200,7 @@ describe('useMarketSelectorList', () => {
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
sort: Sort.TopTraded,
assets: ['asset-0', 'asset-1'],
});
@@ -210,7 +213,7 @@ describe('useMarketSelectorList', () => {
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
sort: Sort.TopTraded,
assets: ['asset-0', 'asset-1', 'asset-2'],
});
@@ -220,7 +223,7 @@ describe('useMarketSelectorList', () => {
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
sort: Sort.TopTraded,
assets: ['asset-invalid'],
});
@@ -275,28 +278,28 @@ describe('useMarketSelectorList', () => {
const { result, rerender } = setup({
searchTerm: 'abc',
product: Product.Future,
sort: Sort.None,
sort: Sort.TopTraded,
assets: [],
});
expect(result.current.markets).toEqual([markets[0]]);
rerender({
searchTerm: 'def',
product: Product.Future,
sort: Sort.None,
sort: Sort.TopTraded,
assets: [],
});
expect(result.current.markets).toEqual([markets[1], markets[2]]);
rerender({
searchTerm: 'defg',
product: Product.Future,
sort: Sort.None,
sort: Sort.TopTraded,
assets: [],
});
expect(result.current.markets).toEqual([markets[2]]);
rerender({
searchTerm: 'zzz',
product: Product.Future,
sort: Sort.None,
sort: Sort.TopTraded,
assets: [],
});
expect(result.current.markets).toEqual([]);
@@ -305,14 +308,14 @@ describe('useMarketSelectorList', () => {
rerender({
searchTerm: 'aaa',
product: Product.Future,
sort: Sort.None,
sort: Sort.TopTraded,
assets: [],
});
expect(result.current.markets).toEqual([markets[0]]);
rerender({
searchTerm: 'ggg',
product: Product.Future,
sort: Sort.None,
sort: Sort.TopTraded,
assets: [],
});
expect(result.current.markets).toEqual([
@@ -322,11 +325,15 @@ describe('useMarketSelectorList', () => {
]);
});
it('sorts by state and volume by default', () => {
it('sorts by top traded by default', () => {
const markets = [
createMarketFragment({
id: 'market-0',
state: MarketState.STATE_PENDING,
state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment
data: {
markPrice: '1',
},
// @ts-ignore candles not on fragment
candles: [
{
@@ -337,30 +344,42 @@ describe('useMarketSelectorList', () => {
createMarketFragment({
id: 'market-1',
state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment
data: {
markPrice: '1',
},
// @ts-ignore candles not on fragment
candles: [
{
volume: '200',
volume: '100',
},
],
}),
createMarketFragment({
id: 'market-2',
state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment
data: {
markPrice: '1',
},
// @ts-ignore candles not on fragment
candles: [
{
volume: '100',
volume: '300',
},
],
}),
createMarketFragment({
state: MarketState.STATE_PENDING,
id: 'market-3',
state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment
data: {
markPrice: '1',
},
// @ts-ignore candles not on fragment
candles: [
{
volume: '100',
volume: '400',
},
],
}),
@@ -375,14 +394,15 @@ describe('useMarketSelectorList', () => {
const { result } = setup({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
sort: Sort.TopTraded,
assets: [],
});
expect(result.current.markets).toEqual([
markets[1],
markets[3],
markets[2],
markets[0],
markets[3],
markets[1],
]);
});
@@ -1,11 +1,7 @@
import { useMemo } from 'react';
import orderBy from 'lodash/orderBy';
import { MarketState } from '@vegaprotocol/types';
import {
calcCandleVolume,
calcTradedFactor,
useMarketList,
} from '@vegaprotocol/markets';
import { calcTradedFactor, useMarketList } from '@vegaprotocol/markets';
import { priceChangePercentage } from '@vegaprotocol/utils';
import type { Filter } from '../../components/market-selector/market-selector';
import { Sort } from './sort-dropdown';
@@ -60,22 +56,6 @@ export const useMarketSelectorList = ({
return false;
});
if (sort === Sort.None) {
// Sort by market state primarily and AtoZ secondarily
return orderBy(
markets,
[
(m) => MARKET_TEMPLATE.indexOf(m.state),
(m) => {
if (!m.candles?.length) return 0;
const vol = calcCandleVolume(m.candles);
return Number(vol || 0);
},
],
['asc', 'desc']
);
}
if (sort === Sort.Gained || sort === Sort.Lost) {
const dir = sort === Sort.Gained ? 'desc' : 'asc';
return orderBy(
@@ -1,5 +1,5 @@
import { t } from '@vegaprotocol/i18n';
import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
import { TradingButton } from '@vegaprotocol/ui-toolkit';
import { usePositionsStore } from '../positions-container';
export const PositionsMenu = () => {
@@ -7,7 +7,6 @@ export const PositionsMenu = () => {
const toggle = usePositionsStore((store) => store.toggleClosedMarkets);
return (
<TradingButton
intent={Intent.Primary}
size="extra-small"
data-testid="open-transfer"
onClick={toggle}
@@ -24,8 +24,8 @@ export const Settings = () => {
>
<Switch
name="settings-telemetry-switch"
onCheckedChange={(isOn) => setIsApproved(isOn)}
checked={isApproved}
onCheckedChange={(isOn) => setIsApproved(isOn ? 'true' : 'false')}
checked={isApproved === 'true'}
/>
</SettingsGroup>
<SettingsGroup label={t('Toast location')}>
@@ -1 +0,0 @@
export * from './vega-wallet-container';
@@ -1,27 +0,0 @@
import { render, screen } from '@testing-library/react';
import { VegaWalletContainer } from './vega-wallet-container';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import type { PartialDeep } from 'type-fest';
const generateJsx = (context: PartialDeep<VegaWalletContextShape>) => {
return (
<VegaWalletContext.Provider value={context as VegaWalletContextShape}>
<VegaWalletContainer>
<div data-testid="child" />
</VegaWalletContainer>
</VegaWalletContext.Provider>
);
};
describe('VegaWalletContainer', () => {
it('doesnt render children if not connected', () => {
render(generateJsx({ pubKey: null }));
expect(screen.queryByTestId('child')).not.toBeInTheDocument();
});
it('renders children if connected', () => {
render(generateJsx({ pubKey: '0x123' }));
expect(screen.getByTestId('child')).toBeInTheDocument();
});
});
@@ -1,35 +0,0 @@
import type { ReactNode } from 'react';
import { t } from '@vegaprotocol/i18n';
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
interface VegaWalletContainerProps {
children: ReactNode;
}
export const VegaWalletContainer = ({ children }: VegaWalletContainerProps) => {
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
const { pubKey } = useVegaWallet();
if (!pubKey) {
return (
<Splash>
<div className="text-center">
<p className="mb-4" data-testid="connect-vega-wallet-text">
{t('Connect your Vega wallet')}
</p>
<Button
onClick={openVegaWalletDialog}
data-testid="vega-wallet-connect"
>
{t('Connect')}
</Button>
</div>
</Splash>
);
}
return <>{children}</>;
};
@@ -23,6 +23,7 @@ import { Links, Routes } from '../../pages/client-router';
import { useGlobalStore } from '../../stores';
import { useSidebar, ViewType } from '../sidebar';
import * as constants from '../constants';
import { useOnboardingStore } from './welcome-dialog';
interface Props {
lead?: string;
@@ -35,7 +36,7 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => {
constants.ONBOARDING_VIEWED_KEY
);
const update = useGlobalStore((store) => store.update);
const dismiss = useOnboardingStore((store) => store.dismiss);
const marketId = useGlobalStore((store) => store.marketId);
const link = marketId ? Links[Routes.MARKET](marketId) : Links[Routes.HOME]();
const openVegaWalletDialog = useVegaWalletDialogStore(
@@ -61,7 +62,7 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => {
onClickHandle = () => {
navigate(link);
setView({ type: ViewType.Deposit });
update({ onBoardingDismissed: true });
dismiss();
};
} else if (step === OnboardingStep.ONBOARDING_ORDER_STEP) {
buttonText = t('Dismiss');
@@ -2,29 +2,35 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TelemetryApproval } from './telemetry-approval';
jest.mock('@vegaprotocol/logger', () => ({
SentryInit: () => undefined,
SentryClose: () => undefined,
}));
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({ VEGA_ENV: 'test', SENTRY_DSN: 'sentry-dsn' }),
}));
describe('TelemetryApproval', () => {
it('click on checkbox should be properly handled', async () => {
const helpText = 'My help text';
render(<TelemetryApproval helpText={helpText} />);
expect(screen.getByRole('checkbox')).toHaveAttribute(
'data-state',
'unchecked'
it('click on buttons should be properly handled', async () => {
const mockSetTelemetryValue = jest.fn();
render(
<TelemetryApproval
telemetryValue="false"
setTelemetryValue={mockSetTelemetryValue}
/>
);
await userEvent.click(screen.getByRole('checkbox'));
expect(screen.getByRole('checkbox')).toHaveAttribute(
'data-state',
'checked'
expect(
screen.getByRole('button', { name: 'No thanks' })
).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'No thanks' }));
expect(mockSetTelemetryValue).toHaveBeenCalledWith('false');
expect(screen.getByText('Share data')).toBeInTheDocument();
await userEvent.click(screen.getByText('Share data'));
expect(mockSetTelemetryValue).toHaveBeenCalledWith('true');
});
it('confirm button should have proper text', async () => {
const mockSetTelemetryValue = jest.fn();
render(
<TelemetryApproval
telemetryValue="true"
setTelemetryValue={mockSetTelemetryValue}
/>
);
expect(screen.getByText('Share usage data')).toBeInTheDocument();
expect(screen.getByText(helpText)).toBeInTheDocument();
expect(screen.getByText('Continue sharing data')).toBeInTheDocument();
await userEvent.click(screen.getByText('Continue sharing data'));
expect(mockSetTelemetryValue).toHaveBeenCalledWith('true');
});
});
@@ -1,21 +1,69 @@
import { TradingCheckbox } from '@vegaprotocol/ui-toolkit';
import {
Intent,
TradingButton,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
export const TelemetryApproval = ({ helpText }: { helpText: string }) => {
const [isApproved, setIsApproved] = useTelemetryApproval();
interface Props {
telemetryValue: string;
setTelemetryValue: (value: string) => void;
}
export const TelemetryApproval = ({
telemetryValue,
setTelemetryValue,
}: Props) => {
return (
<div className="flex flex-col py-3">
<div className="flex flex-col">
<div className="mr-4" role="form">
<TradingCheckbox
label={<span className="text-lg pl-1">{t('Share usage data')}</span>}
checked={isApproved}
name="telemetry-approval"
onCheckedChange={() => setIsApproved(!isApproved)}
/>
</div>
<div className="text-sm text-vega-light-300 dark:text-vega-dark-300 ml-6">
<span>{helpText}</span>
<p className="mb-4">
{t(
'Help us identify bugs and improve Vega Governance by sharing anonymous usage data.'
)}
</p>
<div className="flex items-start mb-2 gap-3">
<VegaIcon name={VegaIconNames.EYE_OFF} size={18} />
<div className="flex flex-col gap-1">
<h6 className="font-semibold">{t('Anonymous')}</h6>
<p className="text-muted">
{t('Your identity is always anonymous on Vega')}
</p>
</div>
</div>
<div className="flex items-start mb-4 gap-3">
<VegaIcon name={VegaIconNames.COG} size={18} />
<div className="flex flex-col gap-1">
<h6 className="font-semibold">{t('Optional')}</h6>
<p className="text-muted">
{t('You can opt out any time via settings')}
</p>
</div>
</div>
<div className="flex flex-col items-center justify-around gap-2">
<TradingButton
onClick={() => setTelemetryValue('false')}
size="small"
intent={Intent.None}
data-testid="do-not-share-data-button"
fill
>
{t('No thanks')}
</TradingButton>
<TradingButton
onClick={() => setTelemetryValue('true')}
intent={Intent.Info}
data-testid="share-data-button"
size="small"
fill
>
{telemetryValue === 'true'
? t('Continue sharing data')
: t('Share data')}
</TradingButton>
</div>
</div>
</div>
);
@@ -5,17 +5,17 @@ import { useNavigate } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import type { ReactNode } from 'react';
import { useGlobalStore } from '../../stores';
import { useOnboardingStore } from './welcome-dialog';
export const WelcomeDialogContent = () => {
const { VEGA_ENV } = useEnvironment();
const update = useGlobalStore((store) => store.update);
const dismiss = useOnboardingStore((store) => store.dismiss);
const navigate = useNavigate();
const browseMarkets = () => {
const link = Links[Routes.MARKETS]();
navigate(link);
update({ onBoardingDismissed: true });
dismiss();
};
const lead =
VEGA_ENV === Networks.MAINNET
@@ -1,7 +1,9 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { Dialog, Intent, useToasts } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { useEnvironment } from '@vegaprotocol/environment';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { WelcomeDialogContent } from './welcome-dialog-content';
@@ -12,15 +14,41 @@ import {
OnboardingStep,
} from './use-get-onboarding-step';
import * as constants from '../constants';
import { TelemetryApproval } from './telemetry-approval';
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
import { useCallback } from 'react';
const ONBOARDING_STORAGE_KEY = 'vega_onboarding_dismiss_store';
export const useOnboardingStore = create<{
dismissed: boolean;
dismiss: () => void;
}>()(
persist(
(set) => ({
dismissed: false,
dismiss: () => set(() => ({ dismissed: true })),
}),
{
name: ONBOARDING_STORAGE_KEY,
}
)
);
const TELEMETRY_APPROVAL_TOAST_ID = 'telemetry_tost_id';
export const WelcomeDialog = () => {
const { VEGA_ENV } = useEnvironment();
const [onBoardingViewed] = useLocalStorage(constants.ONBOARDING_VIEWED_KEY);
const update = useGlobalStore((store) => store.update);
const dismissed = useGlobalStore((store) => store.onBoardingDismissed);
const currentStep = useGetOnboardingStep();
const navigate = useNavigate();
const [telemetryValue, setTelemetryValue, isTelemetryNeeded, closeTelemetry] =
useTelemetryApproval();
const [onBoardingViewed] = useLocalStorage(constants.ONBOARDING_VIEWED_KEY);
const dismiss = useOnboardingStore((store) => store.dismiss);
const dismissed = useOnboardingStore((store) => store.dismissed);
const currentStep = useGetOnboardingStep();
const isTelemetryPopupNeeded =
isTelemetryNeeded &&
(onBoardingViewed === 'true' ||
currentStep > OnboardingStep.ONBOARDING_ORDER_STEP);
const isOnboardingDialogNeeded =
onBoardingViewed !== 'true' &&
currentStep &&
@@ -29,12 +57,58 @@ export const WelcomeDialog = () => {
const marketId = useGlobalStore((store) => store.marketId);
const onClose = () => {
const link = marketId
? Links[Routes.MARKET](marketId)
: Links[Routes.HOME]();
navigate(link);
update({ onBoardingDismissed: true });
if (isTelemetryPopupNeeded) {
closeTelemetry();
} else {
const link = marketId
? Links[Routes.MARKET](marketId)
: Links[Routes.HOME]();
navigate(link);
dismiss();
}
};
const [setToast, hasToast, removeToast] = useToasts((store) => [
store.setToast,
store.hasToast,
store.remove,
]);
const onApprovalClose = useCallback(() => {
closeTelemetry();
removeToast(TELEMETRY_APPROVAL_TOAST_ID);
}, [removeToast, closeTelemetry]);
const setTelemetryApprovalAndClose = useCallback(
(value: string) => {
setTelemetryValue(value);
onApprovalClose();
},
[setTelemetryValue, onApprovalClose]
);
if (isTelemetryPopupNeeded) {
const toast: Toast = {
id: TELEMETRY_APPROVAL_TOAST_ID,
intent: Intent.Primary,
content: (
<>
<h3 className="mb-1 text-sm uppercase">
{t('Improve vega console')}
</h3>
<TelemetryApproval
telemetryValue={telemetryValue}
setTelemetryValue={setTelemetryApprovalAndClose}
/>
</>
),
onClose: onApprovalClose,
};
if (!hasToast(TELEMETRY_APPROVAL_TOAST_ID)) {
setToast(toast);
}
return;
}
const title = (
<span className="font-alpha calt" data-testid="welcome-title">
{t('Console')}{' '}
@@ -1,5 +1,5 @@
import { t } from '@vegaprotocol/i18n';
import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
import { TradingButton } from '@vegaprotocol/ui-toolkit';
import { ViewType, useSidebar } from '../sidebar';
export const WithdrawalsMenu = () => {
@@ -7,7 +7,6 @@ export const WithdrawalsMenu = () => {
return (
<TradingButton
intent={Intent.Primary}
size="extra-small"
onClick={() => setView({ type: ViewType.Withdraw })}
data-testid="withdraw-dialog-button"
@@ -1,19 +1,40 @@
import { renderHook, act, waitFor } from '@testing-library/react';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { SentryInit, SentryClose } from '@vegaprotocol/logger';
import { STORAGE_KEY, useTelemetryApproval } from './use-telemetry-approval';
import {
STORAGE_KEY,
STORAGE_SECOND_KEY,
useTelemetryApproval,
} from './use-telemetry-approval';
import { Networks } from '@vegaprotocol/environment';
const mockSetValue = jest.fn();
const mockRemoveValue = jest.fn();
let mockStorageHookApprovalResult: [string | null, jest.Mock] = [
null,
mockSetValue,
];
const mockSetSecondValue = jest.fn();
let mockStorageHookViewedResult: [string | null, jest.Mock] = [
null,
mockSetSecondValue,
];
jest.mock('@vegaprotocol/logger');
jest.mock('@vegaprotocol/react-helpers', () => ({
...jest.requireActual('@vegaprotocol/react-helpers'),
useLocalStorage: jest
.fn()
.mockImplementation(() => [false, mockSetValue, mockRemoveValue]),
useLocalStorage: jest.fn((key: string) => {
if (key === 'vega_telemetry_approval') {
return mockStorageHookApprovalResult;
}
return mockStorageHookViewedResult;
}),
}));
let mockVegaEnv = 'test';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({ VEGA_ENV: 'test', SENTRY_DSN: 'sentry-dsn' }),
...jest.requireActual('@vegaprotocol/environment'),
useEnvironment: jest.fn(() => ({
VEGA_ENV: mockVegaEnv,
SENTRY_DSN: 'sentry-dsn',
})),
}));
describe('useTelemetryApproval', () => {
@@ -21,32 +42,71 @@ describe('useTelemetryApproval', () => {
jest.clearAllMocks();
});
it('hook should return proper array', () => {
it('when empty hook should return proper array', () => {
const { result } = renderHook(() => useTelemetryApproval());
expect(result.current[0]).toEqual(false);
expect(result.current[0]).toEqual('');
expect(result.current[1]).toEqual(expect.any(Function));
expect(result.current[2]).toEqual(true);
expect(result.current[3]).toEqual(expect.any(Function));
expect(useLocalStorage).toHaveBeenCalledWith(STORAGE_KEY);
expect(useLocalStorage).toHaveBeenCalledWith(STORAGE_SECOND_KEY);
expect(mockSetValue).toHaveBeenCalledWith('true');
expect(mockSetSecondValue).not.toHaveBeenCalledWith('true');
});
it('when approval not empty but viewed is empty should return proper array', () => {
mockStorageHookApprovalResult = ['false', mockSetValue];
const { result } = renderHook(() => useTelemetryApproval());
expect(result.current[0]).toEqual('false');
expect(result.current[1]).toEqual(expect.any(Function));
expect(result.current[2]).toEqual(true);
expect(result.current[3]).toEqual(expect.any(Function));
expect(useLocalStorage).toHaveBeenCalledWith(STORAGE_KEY);
expect(useLocalStorage).toHaveBeenCalledWith(STORAGE_SECOND_KEY);
expect(mockSetValue).not.toHaveBeenCalled();
expect(mockSetSecondValue).not.toHaveBeenCalled();
});
it('when NOT empty hook should return proper array', () => {
mockStorageHookApprovalResult = ['false', mockSetValue];
mockStorageHookViewedResult = ['true', mockSetSecondValue];
const { result } = renderHook(() => useTelemetryApproval());
expect(result.current[0]).toEqual('false');
expect(result.current[1]).toEqual(expect.any(Function));
expect(result.current[2]).toEqual(false);
expect(result.current[3]).toEqual(expect.any(Function));
expect(useLocalStorage).toHaveBeenCalledWith(STORAGE_KEY);
expect(mockSetValue).not.toHaveBeenCalled();
});
it('on mainnet hook should init properly', () => {
mockStorageHookApprovalResult = [null, mockSetValue];
mockVegaEnv = Networks.MAINNET;
renderHook(() => useTelemetryApproval());
expect(mockSetValue).toHaveBeenCalledWith('false');
});
it('hook should init stuff properly', async () => {
const { result } = renderHook(() => useTelemetryApproval());
await act(() => {
result.current[1](true);
result.current[1]('true');
});
await waitFor(() => {
expect(SentryInit).toHaveBeenCalled();
expect(mockSetValue).toHaveBeenCalledWith('1');
expect(mockSetValue).toHaveBeenCalledWith('true');
expect(mockSetSecondValue).toHaveBeenCalledWith('true');
});
});
it('hook should close stuff properly', async () => {
const { result } = renderHook(() => useTelemetryApproval());
await act(() => {
result.current[1](false);
result.current[1]('false');
});
await waitFor(() => {
expect(SentryClose).toHaveBeenCalled();
expect(mockRemoveValue).toHaveBeenCalledWith();
expect(mockSetValue).toHaveBeenCalledWith('false');
expect(mockSetSecondValue).toHaveBeenCalledWith('true');
});
});
});
@@ -1,25 +1,51 @@
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { useCallback } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { SentryInit, SentryClose } from '@vegaprotocol/logger';
import { useEnvironment } from '@vegaprotocol/environment';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
export const STORAGE_KEY = 'vega_telemetry_approval';
export const STORAGE_SECOND_KEY = 'vega_telemetry_viewed';
export const useTelemetryApproval = (): [
value: boolean,
setValue: (value: boolean) => void
value: string,
setValue: (value: string) => void,
shouldOpen: boolean,
close: () => void
] => {
const { VEGA_ENV, SENTRY_DSN } = useEnvironment();
const [value, setValue, removeValue] = useLocalStorage(STORAGE_KEY);
const setApprove = useCallback(
(value: boolean) => {
if (value && SENTRY_DSN) {
const defaultTelemetryValue =
VEGA_ENV === Networks.MAINNET ? 'false' : 'true';
const [value, setValue] = useLocalStorage(STORAGE_KEY);
const [viewedValue, setViewedValue] = useLocalStorage(STORAGE_SECOND_KEY);
const [shouldOpen, setShouldOpen] = useState(!value || !viewedValue);
const close = useCallback(() => {
setShouldOpen(false);
setViewedValue('true');
}, [setViewedValue]);
const manageValue = useCallback(
(value: string) => {
if (value === 'true' && SENTRY_DSN) {
SentryInit(SENTRY_DSN, VEGA_ENV);
return setValue('1');
return setValue('true');
}
SentryClose();
removeValue();
setValue('false');
},
[setValue, removeValue, SENTRY_DSN, VEGA_ENV]
[setValue, SENTRY_DSN, VEGA_ENV]
);
return [Boolean(value), setApprove];
const setTelemetryValue = useCallback(
(value: string) => {
setShouldOpen(false);
setViewedValue('true');
manageValue(value);
},
[manageValue, setViewedValue]
);
useEffect(() => {
if (!value) {
manageValue(defaultTelemetryValue);
}
}, [value, manageValue, defaultTelemetryValue]);
return [value || '', setTelemetryValue, shouldOpen, close];
};
-2
View File
@@ -4,7 +4,6 @@ import produce from 'immer';
interface GlobalStore {
marketId: string | null;
onBoardingDismissed: boolean;
eagerConnecting: boolean;
update: (store: Partial<Omit<GlobalStore, 'update'>>) => void;
}
@@ -16,7 +15,6 @@ interface PageTitleStore {
export const useGlobalStore = create<GlobalStore>()((set) => ({
marketId: LocalStorage.getItem('marketId') || null,
onBoardingDismissed: false,
eagerConnecting: false,
update: (newState) => {
set(
@@ -2,8 +2,8 @@ import { ETHERSCAN_ADDRESS, useEtherscanLink } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import {
ActionsDropdown,
DropdownMenuCopyItem,
DropdownMenuItem,
TradingDropdownCopyItem,
TradingDropdownItem,
Link,
VegaIcon,
VegaIconNames,
@@ -30,49 +30,49 @@ export const AccountsActionsDropdown = ({
return (
<ActionsDropdown>
<DropdownMenuItem
<TradingDropdownItem
key={'deposit'}
data-testid="deposit"
onClick={onClickDeposit}
>
<VegaIcon name={VegaIconNames.DEPOSIT} size={16} />
{t('Deposit')}
</DropdownMenuItem>
<DropdownMenuItem
</TradingDropdownItem>
<TradingDropdownItem
key={'withdraw'}
data-testid="withdraw"
onClick={onClickWithdraw}
>
<VegaIcon name={VegaIconNames.WITHDRAW} size={16} />
{t('Withdraw')}
</DropdownMenuItem>
<DropdownMenuItem
</TradingDropdownItem>
<TradingDropdownItem
key={'transfer'}
data-testid="transfer"
onClick={onClickTransfer}
>
<VegaIcon name={VegaIconNames.TRANSFER} size={16} />
{t('Transfer')}
</DropdownMenuItem>
<DropdownMenuItem
</TradingDropdownItem>
<TradingDropdownItem
key={'breakdown'}
data-testid="breakdown"
onClick={onClickBreakdown}
>
<VegaIcon name={VegaIconNames.BREAKDOWN} size={16} />
{t('View usage breakdown')}
</DropdownMenuItem>
<DropdownMenuItem
</TradingDropdownItem>
<TradingDropdownItem
onClick={(e) => {
openAssetDialog(assetId, e.target as HTMLElement);
}}
>
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View asset details')}
</DropdownMenuItem>
<DropdownMenuCopyItem value={assetId} text={t('Copy asset ID')} />
</TradingDropdownItem>
<TradingDropdownCopyItem value={assetId} text={t('Copy asset ID')} />
{assetContractAddress && (
<DropdownMenuItem>
<TradingDropdownItem>
<Link
href={etherscanLink(
ETHERSCAN_ADDRESS.replace(':hash', assetContractAddress)
@@ -84,7 +84,7 @@ export const AccountsActionsDropdown = ({
{t('View on Etherscan')}
</span>
</Link>
</DropdownMenuItem>
</TradingDropdownItem>
)}
</ActionsDropdown>
);
+15 -11
View File
@@ -11,9 +11,13 @@ import type {
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { COL_DEFS } from '@vegaprotocol/datagrid';
import { Button, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import {
Intent,
TradingButton,
VegaIcon,
VegaIconNames,
TooltipCellComponent,
} from '@vegaprotocol/ui-toolkit';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type {
IGetRowsParams,
@@ -186,7 +190,7 @@ export const AccountTable = ({
) : (
<>
<span className="underline">{valueFormatted}</span>
<span className="ml-2 inline-block w-14 text-muted">
<span className="inline-block ml-2 w-14 text-muted">
{t('0.00%')}
</span>
</>
@@ -248,26 +252,26 @@ export const AccountTable = ({
colId: 'accounts-actions',
field: 'asset.id',
...COL_DEFS.actions,
minWidth: showDepositButton ? 130 : COL_DEFS.actions.minWidth,
maxWidth: showDepositButton ? 130 : COL_DEFS.actions.maxWidth,
minWidth: showDepositButton ? 105 : COL_DEFS.actions.minWidth,
maxWidth: showDepositButton ? 105 : COL_DEFS.actions.maxWidth,
cellRenderer: ({
value: assetId,
node,
}: VegaICellRendererParams<AccountFields, 'asset.id'>) => {
if (!assetId) return null;
if (node.rowPinned && node.data?.total === '0') {
if (node.rowPinned && node.data?.balance === '0') {
return (
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
<Button
size="xs"
variant="primary"
<TradingButton
size="extra-small"
intent={Intent.Primary}
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(assetId);
}}
>
<VegaIcon name={VegaIconNames.DEPOSIT} /> {t('Deposit')}
</Button>
</TradingButton>
</CenteredGridCellWrapper>
);
}
+8 -8
View File
@@ -8,7 +8,6 @@ import {
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
Button,
TradingFormGroup,
TradingInput,
TradingInputError,
@@ -16,6 +15,7 @@ import {
TradingSelect,
Tooltip,
TradingCheckbox,
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import type { Transfer } from '@vegaprotocol/wallet';
import { normalizeTransfer } from '@vegaprotocol/wallet';
@@ -276,9 +276,9 @@ export const TransferForm = ({
decimals={asset?.decimals}
/>
)}
<Button type="submit" variant="primary" fill={true}>
<TradingButton type="submit" fill={true}>
{t('Confirm transfer')}
</Button>
</TradingButton>
</form>
);
};
@@ -309,8 +309,8 @@ export const TransferFee = ({
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
return (
<div className="mb-4 flex flex-col gap-2 text-xs">
<div className="flex justify-between gap-1 items-center flex-wrap">
<div className="flex flex-col mb-4 text-xs gap-2">
<div className="flex flex-wrap items-center justify-between gap-1">
<Tooltip
description={t(
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to %s`,
@@ -324,7 +324,7 @@ export const TransferFee = ({
{formatNumber(fee, decimals)}
</div>
</div>
<div className="flex justify-between gap-1 items-center flex-wrap">
<div className="flex flex-wrap items-center justify-between gap-1">
<Tooltip
description={t(
`The total amount to be transferred (without the fee)`
@@ -337,7 +337,7 @@ export const TransferFee = ({
{formatNumber(amount, decimals)}
</div>
</div>
<div className="flex justify-between gap-1 items-center flex-wrap">
<div className="flex flex-wrap items-center justify-between gap-1">
<Tooltip
description={t(
`The total amount taken from your account. The amount to be transferred plus the fee.`
@@ -384,7 +384,7 @@ export const AddressField = ({
setIsInput((curr) => !curr);
onChange();
}}
className="ml-auto text-sm absolute top-0 right-0 underline"
className="absolute top-0 right-0 ml-auto text-sm underline"
>
{isInput ? t('Select from wallet') : t('Enter manually')}
</button>
+4 -3
View File
@@ -2,9 +2,10 @@ import { t } from '@vegaprotocol/i18n';
import {
Button,
Dialog,
Icon,
Splash,
SyntaxHighlighter,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { create } from 'zustand';
import { AssetDetailsTable } from './asset-details-table';
@@ -82,7 +83,7 @@ export const AssetDetailsDialog = ({
return (
<Dialog
title={title}
icon={<Icon name="info-sign"></Icon>}
icon={<VegaIcon name={VegaIconNames.INFO} />}
open={open}
onChange={(isOpen) => onChange(isOpen)}
onCloseAutoFocus={(e) => {
@@ -97,7 +98,7 @@ export const AssetDetailsDialog = ({
}}
>
{content}
<p className="text-sm my-4">
<p className="my-4 text-xs">
{t(
'There is 1 unit of the settlement asset (%s) to every 1 quote unit.',
[assetSymbol]
+7 -4
View File
@@ -3,7 +3,8 @@ import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type * as Schema from '@vegaprotocol/types';
import type { KeyValueTableRowProps } from '@vegaprotocol/ui-toolkit';
import { CopyWithTooltip, Icon } from '@vegaprotocol/ui-toolkit';
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { CopyWithTooltip, truncateMiddle } from '@vegaprotocol/ui-toolkit';
import {
KeyValueTable,
KeyValueTableRow,
@@ -56,7 +57,7 @@ export const rows: Rows = [
key: AssetDetail.ID,
label: t('ID'),
tooltip: '',
value: (asset) => asset.id,
value: (asset) => truncateMiddle(asset.id),
},
{
key: AssetDetail.TYPE,
@@ -109,10 +110,12 @@ export const rows: Rows = [
return (
<>
<EtherscanLink address={asset.source.contractAddress} />{' '}
<EtherscanLink address={asset.source.contractAddress}>
{truncateMiddle(asset.source.contractAddress)}
</EtherscanLink>{' '}
<CopyWithTooltip text={asset.source.contractAddress}>
<button title={t('Copy address to clipboard')}>
<Icon size={3} name="duplicate" />
<VegaIcon size={14} name={VegaIconNames.COPY} />
</button>
</CopyWithTooltip>
</>
+2 -2
View File
@@ -1,4 +1,4 @@
import { TradingOption } from '@vegaprotocol/ui-toolkit';
import { TradingOption, truncateMiddle } from '@vegaprotocol/ui-toolkit';
import type { AssetFieldsFragment } from './__generated__/Asset';
import classNames from 'classnames';
import { t } from '@vegaprotocol/i18n';
@@ -45,7 +45,7 @@ export const AssetOption = ({ asset, balance }: AssetOptionProps) => {
{balance}
<div className="text-[12px] font-mono w-full text-left break-all">
<span className="text-vega-light-300 dark:text-vega-dark-300">
{asset.id}
{truncateMiddle(asset.id)}
</span>
</div>
</div>
@@ -3,15 +3,31 @@ import userEvent from '@testing-library/user-event';
import { CandlesMenu } from './candles-menu';
describe('CandlesMenu', () => {
it('should render with volume study showing by default', async () => {
it('should render with the correct default studies', async () => {
render(<CandlesMenu />);
await userEvent.click(
screen.getByText('Studies', {
selector: '[type="button"]',
screen.getByRole('button', {
name: 'Studies',
})
);
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(screen.getByText('Volume')).toHaveAttribute('data-state', 'checked');
expect(screen.getByText('MACD')).toHaveAttribute('data-state', 'checked');
});
it('should render with the correct default overlays', async () => {
render(<CandlesMenu />);
await userEvent.click(
screen.getByRole('button', {
name: 'Overlays',
})
);
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(screen.getByText('Moving average')).toHaveAttribute(
'data-state',
'checked'
);
});
});
+61 -51
View File
@@ -10,13 +10,14 @@ import {
studyLabels,
} from 'pennant';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItemIndicator,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
TradingButton,
TradingDropdown,
TradingDropdownCheckboxItem,
TradingDropdownContent,
TradingDropdownItemIndicator,
TradingDropdownRadioGroup,
TradingDropdownRadioItem,
TradingDropdownTrigger,
Icon,
} from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@blueprintjs/icons';
@@ -44,69 +45,76 @@ export const CandlesMenu = () => {
} = useCandlesChartSettings();
const triggerClasses = 'text-xs';
const contentAlign = 'end';
const triggerButtonProps = { size: 'extra-small' } as const;
return (
<>
<DropdownMenu
<TradingDropdown
trigger={
<DropdownMenuTrigger className={triggerClasses}>
{t(`Interval: ${intervalLabels[interval]}`)}
</DropdownMenuTrigger>
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
{t(`Interval: ${intervalLabels[interval]}`)}
</TradingButton>
</TradingDropdownTrigger>
}
>
<DropdownMenuContent align={contentAlign}>
<DropdownMenuRadioGroup
<TradingDropdownContent align={contentAlign}>
<TradingDropdownRadioGroup
value={interval}
onValueChange={(value) => {
setInterval(value as Interval);
}}
>
{Object.values(Interval).map((timeInterval) => (
<DropdownMenuRadioItem
<TradingDropdownRadioItem
key={timeInterval}
inset
value={timeInterval}
>
{intervalLabels[timeInterval]}
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
</TradingDropdownRadioGroup>
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<DropdownMenuTrigger className={triggerClasses}>
<Icon name={chartTypeIcon.get(chartType) as IconName} />
</DropdownMenuTrigger>
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
<Icon name={chartTypeIcon.get(chartType) as IconName} />
</TradingButton>
</TradingDropdownTrigger>
}
>
<DropdownMenuContent align={contentAlign}>
<DropdownMenuRadioGroup
<TradingDropdownContent align={contentAlign}>
<TradingDropdownRadioGroup
value={chartType}
onValueChange={(value) => {
setType(value as ChartType);
}}
>
{Object.values(ChartType).map((type) => (
<DropdownMenuRadioItem key={type} inset value={type}>
<TradingDropdownRadioItem key={type} inset value={type}>
{chartTypeLabels[type]}
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
</TradingDropdownRadioGroup>
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<DropdownMenuTrigger className={triggerClasses}>
{t('Overlays')}
</DropdownMenuTrigger>
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
{t('Overlays')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<DropdownMenuContent align={contentAlign}>
<TradingDropdownContent align={contentAlign}>
{Object.values(Overlay).map((overlay) => (
<DropdownMenuCheckboxItem
<TradingDropdownCheckboxItem
key={overlay}
checked={overlays.includes(overlay)}
onCheckedChange={() => {
@@ -121,21 +129,23 @@ export const CandlesMenu = () => {
}}
>
{overlayLabels[overlay]}
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
<TradingDropdownItemIndicator />
</TradingDropdownCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<DropdownMenuTrigger className={triggerClasses}>
{t('Studies')}
</DropdownMenuTrigger>
<TradingDropdownTrigger className={triggerClasses}>
<TradingButton {...triggerButtonProps}>
{t('Studies')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<DropdownMenuContent align={contentAlign}>
<TradingDropdownContent align={contentAlign}>
{Object.values(Study).map((study) => (
<DropdownMenuCheckboxItem
<TradingDropdownCheckboxItem
key={study}
checked={studies.includes(study)}
onCheckedChange={() => {
@@ -150,11 +160,11 @@ export const CandlesMenu = () => {
}}
>
{studyLabels[study]}
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
<TradingDropdownItemIndicator />
</TradingDropdownCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</TradingDropdownContent>
</TradingDropdown>
</>
);
};
@@ -15,8 +15,8 @@ interface StoredSettings {
const DEFAULT_CHART_SETTINGS = {
interval: Interval.I15M,
type: ChartType.CANDLE,
overlays: [],
studies: [Study.VOLUME],
overlays: [Overlay.MOVING_AVERAGE],
studies: [Study.MACD, Study.VOLUME],
};
export const useCandlesChartSettingsStore = create<
@@ -65,6 +65,8 @@ export function addSetVegaWallet() {
Cypress.Commands.add('setVegaWallet', () => {
cy.window().then((win) => {
win.localStorage.setItem('vega_onboarding_viewed', 'true');
win.localStorage.setItem('vega_telemetry_approval', 'false');
win.localStorage.setItem('vega_telemetry_viewed', 'true');
win.localStorage.setItem(
'vega_wallet_config',
JSON.stringify({
@@ -81,6 +83,8 @@ export function addSetOnBoardingViewed() {
Cypress.Commands.add('setOnBoardingViewed', () => {
cy.window().then((win) => {
win.localStorage.setItem('vega_onboarding_viewed', 'true');
win.localStorage.setItem('vega_telemetry_approval', 'false');
win.localStorage.setItem('vega_telemetry_viewed', 'true');
});
});
}
@@ -12,6 +12,8 @@ import { formatRange, formatValue } from '@vegaprotocol/utils';
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
import * as Accordion from '@radix-ui/react-accordion';
import {
MARGIN_DIFF_TOOLTIP_TEXT,
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
@@ -22,6 +24,7 @@ import {
} from '../../constants';
import { useEstimateFees } from '../../hooks';
import { KeyValue } from './key-value';
import { TOOLTIP_TRIGGER_CLASS_NAME } from '@vegaprotocol/ui-toolkit';
const emptyValue = '-';
@@ -244,55 +247,72 @@ export const DealTicketMarginDetails = ({
return (
<>
<KeyValue
label={t('Margin required')}
value={formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
)}
formattedValue={formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals,
quantum
)}
labelDescription={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}
symbol={assetSymbol}
/>
<KeyValue
label={t('Total margin available')}
indent
value={formatValue(totalMarginAvailable, assetDecimals)}
formattedValue={formatValue(
totalMarginAvailable,
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={TOTAL_MARGIN_AVAILABLE(
formatValue(generalAccountBalance, assetDecimals, quantum),
formatValue(marginAccountBalance, assetDecimals, quantum),
formatValue(currentMargins?.maintenanceLevel, assetDecimals, quantum),
assetSymbol
)}
/>
{deductionFromCollateral}
<KeyValue
label={t('Current margin allocation')}
indent
onClick={
generalAccountBalance ? () => setBreakdownDialog(true) : undefined
}
value={formatValue(marginAccountBalance, assetDecimals)}
symbol={assetSymbol}
labelDescription={MARGIN_ACCOUNT_TOOLTIP_TEXT}
formattedValue={formatValue(
marginAccountBalance,
assetDecimals,
quantum
)}
/>
<Accordion.Root type="single" collapsible>
<Accordion.Item value="margin">
<KeyValue
id="margin-required"
label={
<Accordion.Trigger className={TOOLTIP_TRIGGER_CLASS_NAME}>
{t('Margin required')}
</Accordion.Trigger>
}
value={formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
)}
formattedValue={formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals,
quantum
)}
labelDescription={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}
symbol={assetSymbol}
/>
<Accordion.Content>
<KeyValue
label={t('Total margin available')}
indent
value={formatValue(totalMarginAvailable, assetDecimals)}
formattedValue={formatValue(
totalMarginAvailable,
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={TOTAL_MARGIN_AVAILABLE(
formatValue(generalAccountBalance, assetDecimals, quantum),
formatValue(marginAccountBalance, assetDecimals, quantum),
formatValue(
currentMargins?.maintenanceLevel,
assetDecimals,
quantum
),
assetSymbol
)}
/>
{deductionFromCollateral}
<KeyValue
label={t('Current margin allocation')}
indent
onClick={
generalAccountBalance
? () => setBreakdownDialog(true)
: undefined
}
value={formatValue(marginAccountBalance, assetDecimals)}
symbol={assetSymbol}
labelDescription={MARGIN_ACCOUNT_TOOLTIP_TEXT}
formattedValue={formatValue(
marginAccountBalance,
assetDecimals,
quantum
)}
/>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
{projectedMargin}
<KeyValue
label={t('Liquidation price estimate')}
@@ -432,9 +432,9 @@ describe('StopOrder', () => {
});
it('sets expiry time/date to now if expiry is changed to checked', async () => {
const now = Math.round(Date.now() / 1000) * 1000;
const now = 24 * 60 * 60 * 1000;
render(generateJsx());
jest.spyOn(global.Date, 'now').mockImplementationOnce(() => now);
jest.spyOn(global.Date, 'now').mockImplementation(() => now);
await userEvent.click(screen.getByTestId(expire));
// expiry time/date was empty it should be set to now
@@ -21,6 +21,8 @@ import {
import * as positionsTools from '@vegaprotocol/positions';
import { OrdersDocument } from '@vegaprotocol/orders';
import { formatForInput } from '@vegaprotocol/utils';
import type { PartialDeep } from 'type-fest';
import type { Market } from '@vegaprotocol/markets';
jest.mock('zustand');
jest.mock('./deal-ticket-fee-details', () => ({
@@ -36,12 +38,19 @@ const market = generateMarket();
const marketData = generateMarketData();
const submit = jest.fn();
function generateJsx(mocks: MockedResponse[] = []) {
function generateJsx(
mocks: MockedResponse[] = [],
marketOverrides: PartialDeep<Market> = {}
) {
const joinedMarket: Market = {
...market,
...marketOverrides,
} as Market;
return (
<MockedProvider mocks={[...mocks]}>
<VegaWalletContext.Provider value={{ pubKey, isReadOnly: false } as any}>
<DealTicket
market={market}
market={joinedMarket}
marketData={marketData}
marketPrice={marketPrice}
submit={submit}
@@ -367,7 +376,6 @@ describe('DealTicket', () => {
expect(screen.getByTestId('iceberg')).toBeDisabled();
});
// eslint-disable-next-line jest/no-disabled-tests
it('handles TIF select box dependent on order type', async () => {
render(generateJsx());
@@ -533,6 +541,25 @@ describe('DealTicket', () => {
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
});
it('validates size when positionDecimalPlaces is negative', async () => {
render(generateJsx([], { positionDecimalPlaces: -4 }));
const sizeErrorMessage = 'deal-ticket-error-message-size';
const sizeInput = 'order-size';
await userEvent.click(screen.getByTestId('place-order'));
// default value should be invalid
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
expect(screen.getByTestId(sizeErrorMessage)).toHaveTextContent(
'Size cannot be lower than 10000'
);
await userEvent.type(screen.getByTestId(sizeInput), '10001');
expect(screen.getByTestId(sizeErrorMessage)).toHaveTextContent(
'Size must be a multiple of 10000 for this market'
);
await userEvent.clear(screen.getByTestId(sizeInput));
await userEvent.type(screen.getByTestId(sizeInput), '10000');
expect(screen.queryByTestId(sizeErrorMessage)).toBeNull();
});
it('validates iceberg field', async () => {
const peakSizeErrorMessage = 'deal-ticket-peak-error-message';
const minimumSizeErrorMessage = 'deal-ticket-minimum-error-message';
@@ -590,9 +617,9 @@ describe('DealTicket', () => {
it('sets expiry time/date to now if expiry is changed to checked', async () => {
const datePicker = 'date-picker-field';
const now = Math.round(Date.now() / 1000) * 1000;
const now = 24 * 60 * 60 * 1000;
render(generateJsx());
jest.spyOn(global.Date, 'now').mockImplementationOnce(() => now);
jest.spyOn(global.Date, 'now').mockImplementation(() => now);
await userEvent.selectOptions(
screen.getByTestId('order-tif'),
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
@@ -496,11 +496,12 @@ export const DealTicket = ({
onSelect={(value) => {
// If GTT is selected and no expiresAt time is set, or its
// behind current time then reset the value to current time
const now = Date.now();
if (
value === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT &&
(!expiresAt || new Date(expiresAt).getTime() < Date.now())
(!expiresAt || new Date(expiresAt).getTime() < now)
) {
setValue('expiresAt', formatForInput(new Date()), {
setValue('expiresAt', formatForInput(new Date(now)), {
shouldValidate: true,
});
}
@@ -3,7 +3,8 @@ import classnames from 'classnames';
import type { ReactNode } from 'react';
export interface KeyValuePros {
label: string;
id?: string;
label: ReactNode;
value?: string | null | undefined;
symbol: string;
indent?: boolean | undefined;
@@ -13,6 +14,7 @@ export interface KeyValuePros {
}
export const KeyValue = ({
id,
label,
value,
labelDescription,
@@ -31,9 +33,11 @@ export const KeyValue = ({
);
return (
<div
data-testid={
'deal-ticket-fee-' + label.toLocaleLowerCase().replace(/\s/g, '-')
}
data-testid={`deal-ticket-fee-${
!id && typeof label === 'string'
? label.toLocaleLowerCase().replace(/\s/g, '-')
: id
}`}
key={typeof label === 'string' ? label : 'value-dropdown'}
className={classnames(
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
+15 -2
View File
@@ -2,7 +2,11 @@ import type { Asset } from '@vegaprotocol/assets';
import { EtherscanLink } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { Intent, Notification } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
import {
formatNumber,
getUnlimitedThreshold,
quantumDecimalPlaces,
} from '@vegaprotocol/utils';
import type { EthStoredTxState } from '@vegaprotocol/web3';
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
@@ -188,6 +192,15 @@ const ApprovalTxFeedback = ({
}
if (tx.status === EthTxStatus.Confirmed) {
const approvedAllowanceValue = (
allowance || new BigNumber(0)
).isGreaterThan(getUnlimitedThreshold(selectedAsset.decimals))
? '∞'
: formatNumber(
allowance?.toString() || 0,
quantumDecimalPlaces(selectedAsset.quantum, selectedAsset.decimals)
);
return (
<div className="mb-4">
<Notification
@@ -198,7 +211,7 @@ const ApprovalTxFeedback = ({
<p>
{t('You approved deposits of up to %s %s.', [
selectedAsset?.symbol,
formatNumber(allowance?.toString() || 0),
approvedAllowanceValue,
])}
</p>
{txLink && <p>{txLink}</p>}
+4 -5
View File
@@ -15,6 +15,7 @@ import { useWeb3ConnectStore } from '@vegaprotocol/web3';
import { useWeb3React } from '@web3-react/core';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import type { DepositBalances } from './use-deposit-balances';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
jest.mock('@vegaprotocol/wallet');
jest.mock('@vegaprotocol/web3');
@@ -90,7 +91,7 @@ describe('Deposit form', () => {
// Assert default values (including) from/to provided by useVegaWallet and useWeb3React
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
MOCK_ETH_ADDRESS
truncateMiddle(MOCK_ETH_ADDRESS)
);
expect(screen.getByLabelText('Asset')).toHaveValue('');
expect(screen.getByLabelText('To (Vega key)')).toHaveValue('');
@@ -304,9 +305,7 @@ describe('Deposit form', () => {
target: { value: '8' },
});
fireEvent.click(
screen.getByText('Deposit', { selector: '[type="submit"]' })
);
fireEvent.click(screen.getByRole('button', { name: 'Deposit' }));
await waitFor(() => {
expect(props.submitDeposit).toHaveBeenCalledWith({
@@ -353,7 +352,7 @@ describe('Deposit form', () => {
).not.toBeInTheDocument();
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
MOCK_ETH_ADDRESS
truncateMiddle(MOCK_ETH_ADDRESS)
);
});
+11 -11
View File
@@ -13,7 +13,6 @@ import {
import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import {
Button,
TradingFormGroup,
TradingInput,
TradingInputError,
@@ -22,6 +21,8 @@ import {
Intent,
ButtonLink,
TradingSelect,
truncateMiddle,
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core';
@@ -173,7 +174,7 @@ export const DepositForm = ({
return (
<div className="text-sm" aria-describedby="ethereum-address">
<p className="mb-1 break-all" data-testid="ethereum-address">
{account}
{truncateMiddle(account)}
</p>
<DisconnectEthereumButton
onDisconnect={() => {
@@ -185,14 +186,14 @@ export const DepositForm = ({
);
}
return (
<Button
<TradingButton
onClick={openDialog}
variant="primary"
intent={Intent.Primary}
type="button"
data-testid="connect-eth-wallet-btn"
>
{t('Connect')}
</Button>
</TradingButton>
);
}}
/>
@@ -434,15 +435,14 @@ const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
/>
</div>
)}
<Button
<TradingButton
type="submit"
data-testid="deposit-submit"
variant={isActive ? 'primary' : 'default'}
fill
disabled={invalidChain}
disabled={!isActive || invalidChain}
>
{t('Deposit')}
</Button>
</TradingButton>
</>
);
};
@@ -454,7 +454,7 @@ const UseButton = (props: UseButtonProps) => {
<button
{...props}
type="button"
className="ml-auto text-sm absolute top-0 right-0 underline"
className="absolute top-0 right-0 ml-auto text-sm underline"
/>
);
};
@@ -512,7 +512,7 @@ export const AddressField = ({
setIsInput((curr) => !curr);
onChange();
}}
className="ml-auto text-sm absolute top-0 right-0 underline"
className="absolute top-0 right-0 ml-auto text-sm underline"
data-testid="enter-pubkey-manually"
>
{isInput ? t('Select from wallet') : t('Enter manually')}
+10 -4
View File
@@ -7,7 +7,7 @@ import {
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import type BigNumber from 'bignumber.js';
import { formatNumber } from '@vegaprotocol/utils';
import { formatNumber, quantumDecimalPlaces } from '@vegaprotocol/utils';
// Note: all of the values here are with correct asset's decimals
// See `libs/deposits/src/lib/use-deposit-balances.ts`
@@ -35,7 +35,10 @@ export const DepositLimits = ({
label: t('Balance available'),
rawValue: balance,
value: balance ? (
<CompactNumber number={balance} decimals={asset.decimals} />
<CompactNumber
number={balance}
decimals={quantumDecimalPlaces(asset.quantum, asset.decimals)}
/>
) : (
'-'
),
@@ -73,7 +76,7 @@ export const DepositLimits = ({
value: !exempt ? (
<CompactNumber
number={max.minus(deposited)}
decimals={asset.decimals}
decimals={quantumDecimalPlaces(asset.quantum, asset.decimals)}
/>
) : (
<div data-testid="exempt">{t('Exempt')}</div>
@@ -97,7 +100,10 @@ export const DepositLimits = ({
),
rawValue: allowance,
value: allowance ? (
<CompactNumber number={allowance} decimals={asset.decimals} />
<CompactNumber
number={allowance}
decimals={quantumDecimalPlaces(asset.quantum, asset.decimals)}
/>
) : (
'-'
),
+21 -1
View File
@@ -4,7 +4,6 @@ import { getDateTimeFormat } from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
import type { Trade } from './fills-data-provider';
import { FillsTable, getFeesBreakdown } from './fills-table';
import { generateFill } from './test-helpers';
@@ -215,6 +214,27 @@ describe('FillsTable', () => {
).toBeInTheDocument();
});
it('negative positionDecimalPoints should be properly rendered in size column', async () => {
const partyId = 'party-id';
const negativeDecimalPositionFill = generateFill({
...defaultFill,
market: {
...defaultFill.market,
positionDecimalPlaces: -4,
},
});
await act(async () => {
render(
<FillsTable partyId={partyId} rowData={[negativeDecimalPositionFill]} />
);
});
const sizeCell = screen
.getAllByRole('gridcell')
.find((c) => c.getAttribute('col-id') === 'size');
expect(sizeCell).toHaveTextContent('3,000,000,000');
});
describe('getFeesBreakdown', () => {
it('should return correct fees breakdown for a taker', () => {
const fees = {
+3 -4
View File
@@ -1,7 +1,7 @@
import { useRef, useState } from 'react';
import { z } from 'zod';
import {
Button,
TradingButton,
Loader,
TradingFormGroup,
TradingInput,
@@ -157,15 +157,14 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
<Loader size="small" />
</div>
)}
<Button
variant="primary"
<TradingButton
fill
disabled={disabled}
type="submit"
data-testid="ledger-download-button"
>
{t('Download')}
</Button>
</TradingButton>
</div>
</form>
);
@@ -0,0 +1,148 @@
import { useState } from 'react';
import {
VegaIcon,
VegaIconNames,
TradingDropdown,
TradingDropdownTrigger,
TradingDropdownContent,
TradingDropdownItem,
} from '@vegaprotocol/ui-toolkit';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
export const OrderbookControls = ({
lastTradedPrice,
resolution,
decimalPlaces,
setResolution,
}: {
lastTradedPrice: string;
resolution: number;
decimalPlaces: number;
setResolution: (resolution: number) => void;
}) => {
const [isOpen, setOpen] = useState(false);
const resolutions = createResolutions(lastTradedPrice, decimalPlaces);
const increaseResolution = () => {
const index = resolutions.indexOf(resolution);
if (index < resolutions.length - 1) {
setResolution(resolutions[index + 1]);
}
};
const decreaseResolution = () => {
const index = resolutions.indexOf(resolution);
if (index > 0) {
setResolution(resolutions[index - 1]);
}
};
return (
<div className="flex h-6">
<button
onClick={increaseResolution}
disabled={resolutions.indexOf(resolution) >= resolutions.length - 1}
className="flex items-center px-2 border-r cursor-pointer border-default disabled:cursor-default"
data-testid="plus-button"
>
<VegaIcon size={12} name={VegaIconNames.PLUS} />
</button>
<TradingDropdown
open={isOpen}
onOpenChange={(open) => setOpen(open)}
trigger={
<TradingDropdownTrigger data-testid="resolution">
<button
className="flex items-center justify-between px-2 gap-1"
style={{
minWidth: `${
Math.max.apply(
null,
resolutions.map(
(item) => formatResolution(item, decimalPlaces).length
)
) + 5
}ch`,
}}
>
<VegaIcon
size={12}
name={
isOpen ? VegaIconNames.CHEVRON_UP : VegaIconNames.CHEVRON_DOWN
}
/>
{formatResolution(resolution, decimalPlaces)}
</button>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent align="start">
{resolutions.map((r) => (
<TradingDropdownItem
key={r}
onClick={() => setResolution(r)}
className="justify-end"
>
{formatResolution(r, decimalPlaces)}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<button
onClick={decreaseResolution}
disabled={resolutions.indexOf(resolution) <= 0}
className="flex items-center px-2 cursor-pointer border-x border-default disabled:cursor-default"
data-testid="minus-button"
>
<VegaIcon size={12} name={VegaIconNames.MINUS} />
</button>
</div>
);
};
export const formatResolution = (r: number, decimalPlaces: number) => {
let num = addDecimalsFormatNumber(r, decimalPlaces);
// Remove trailing zeroes
num = num.replace(/\.?0+$/, '');
return num;
};
/**
* Create a list of resolutions based on the largest and smallest
* possible values using the last traded price and the market
* decimal places
*/
export const createResolutions = (
lastTradedPrice: string,
decimalPlaces: number
) => {
// number of levels determined by either the number
// of digits in the last traded price OR the number of decimal
// places. For example:
//
// last traded = 1 (0.001)
// dps = 3
// result = 3
//
// last traded = 100001 (1000.01
// dps = 2
// result = 6
const levelCount = Math.max(lastTradedPrice.length ?? 0, decimalPlaces + 1);
const generatedResolutions = new Array(levelCount)
.fill(null)
.map((_, i) => Math.pow(10, i));
const customResolutions = [2, 5, 20, 50, 200, 500];
const combined = customResolutions.concat(generatedResolutions);
combined.sort((a, b) => a - b);
// Remove any resolutions higher than the generated ones as
// we dont want a custom resolution higher than necessary
const resolutions = combined.filter((r) => {
return r <= generatedResolutions[generatedResolutions.length - 1];
});
return resolutions;
};
@@ -31,21 +31,12 @@ describe('compactRows', () => {
it('counts cumulative vol', () => {
const asks = compactRows(sell, VolumeType.ask, 10);
const bids = compactRows(buy, VolumeType.bid, 10);
expect(asks[0].cumulativeVol.value).toEqual(4950);
expect(bids[0].cumulativeVol.value).toEqual(579);
expect(asks[10].cumulativeVol.value).toEqual(390);
expect(bids[10].cumulativeVol.value).toEqual(4950);
expect(bids[bids.length - 1].cumulativeVol.value).toEqual(4950);
expect(asks[asks.length - 1].cumulativeVol.value).toEqual(390);
});
it('updates relative data', () => {
const asks = compactRows(sell, VolumeType.ask, 10);
const bids = compactRows(buy, VolumeType.bid, 10);
expect(asks[0].cumulativeVol.relativeValue).toEqual(100);
expect(bids[0].cumulativeVol.relativeValue).toEqual(12);
expect(asks[10].cumulativeVol.relativeValue).toEqual(8);
expect(bids[10].cumulativeVol.relativeValue).toEqual(100);
expect(asks[0].cumulativeVol).toEqual(4950);
expect(bids[0].cumulativeVol).toEqual(579);
expect(asks[10].cumulativeVol).toEqual(390);
expect(bids[10].cumulativeVol).toEqual(4950);
expect(bids[bids.length - 1].cumulativeVol).toEqual(4950);
expect(asks[asks.length - 1].cumulativeVol).toEqual(390);
});
});
+20 -38
View File
@@ -5,18 +5,14 @@ export enum VolumeType {
bid,
ask,
}
export interface CumulativeVol {
value: number;
relativeValue?: number;
}
export interface OrderbookRowData {
price: string;
value: number;
cumulativeVol: CumulativeVol;
volume: number;
cumulativeVol: number;
}
export const getPriceLevel = (price: string | bigint, resolution: number) => {
export const getPriceLevel = (price: string, resolution: number) => {
const p = BigInt(price);
const r = BigInt(resolution);
let priceLevel = (p / r) * r;
@@ -26,25 +22,6 @@ export const getPriceLevel = (price: string | bigint, resolution: number) => {
return priceLevel.toString();
};
const getMaxVolumes = (orderbookData: OrderbookRowData[]) => ({
cumulativeVol: Math.max(
orderbookData[0]?.cumulativeVol.value,
orderbookData[orderbookData.length - 1]?.cumulativeVol.value
),
});
// round instead of ceil so we will not show 0 if value if different than 0
const toPercentValue = (value?: number) => Math.ceil((value ?? 0) * 100);
const updateRelativeData = (data: OrderbookRowData[]) => {
const { cumulativeVol } = getMaxVolumes(data);
data.forEach((data, i) => {
data.cumulativeVol.relativeValue = toPercentValue(
data.cumulativeVol.value / cumulativeVol
);
});
};
const updateCumulativeVolumeByType = (
data: OrderbookRowData[],
dataType: VolumeType
@@ -53,21 +30,20 @@ const updateCumulativeVolumeByType = (
const maxIndex = data.length - 1;
if (dataType === VolumeType.bid) {
for (let i = 0; i <= maxIndex; i++) {
data[i].cumulativeVol.value =
data[i].value + (i !== 0 ? data[i - 1].cumulativeVol.value : 0);
data[i].cumulativeVol =
data[i].volume + (i !== 0 ? data[i - 1].cumulativeVol : 0);
}
} else {
for (let i = maxIndex; i >= 0; i--) {
data[i].cumulativeVol.value =
data[i].value +
(i !== maxIndex ? data[i + 1].cumulativeVol.value : 0);
data[i].cumulativeVol =
data[i].volume + (i !== maxIndex ? data[i + 1].cumulativeVol : 0);
}
}
}
};
export const compactRows = (
data: PriceLevelFieldsFragment[] | null | undefined,
data: PriceLevelFieldsFragment[],
dataType: VolumeType,
resolution: number
) => {
@@ -75,6 +51,7 @@ export const compactRows = (
getPriceLevel(row.price, resolution)
);
const orderbookData: OrderbookRowData[] = [];
Object.keys(groupedByLevel).forEach((price) => {
const { volume } = groupedByLevel[price].pop() as PriceLevelFieldsFragment;
let value = Number(volume);
@@ -83,7 +60,11 @@ export const compactRows = (
value += Number(subRow.volume);
subRow = groupedByLevel[price].pop();
}
orderbookData.push({ price, value, cumulativeVol: { value: 0 } });
orderbookData.push({
price,
volume: value,
cumulativeVol: 0,
});
});
orderbookData.sort((a, b) => {
@@ -95,8 +76,9 @@ export const compactRows = (
}
return 1;
});
updateCumulativeVolumeByType(orderbookData, dataType);
updateRelativeData(orderbookData);
return orderbookData;
};
@@ -140,7 +122,7 @@ export interface MockDataGeneratorParams {
numberOfSellRows: number;
numberOfBuyRows: number;
overlap: number;
midPrice?: string;
lastTradedPrice: string;
bestStaticBidPrice: number;
bestStaticOfferPrice: number;
}
@@ -148,14 +130,14 @@ export interface MockDataGeneratorParams {
export const generateMockData = ({
numberOfSellRows,
numberOfBuyRows,
midPrice,
lastTradedPrice,
overlap,
bestStaticBidPrice,
bestStaticOfferPrice,
}: MockDataGeneratorParams) => {
let matrix = new Array(numberOfSellRows).fill(undefined);
let price =
Number(midPrice) + (numberOfSellRows - Math.ceil(overlap / 2) + 1);
Number(lastTradedPrice) + (numberOfSellRows - Math.ceil(overlap / 2) + 1);
const sell: PriceLevelFieldsFragment[] = matrix.map((row, i) => ({
price: (price -= 1).toString(),
volume: (numberOfSellRows - i + 1).toString(),
@@ -171,7 +153,7 @@ export const generateMockData = ({
return {
asks: sell,
bids: buy,
midPrice,
lastTradedPrice,
bestStaticBidPrice: bestStaticBidPrice.toString(),
bestStaticOfferPrice: bestStaticOfferPrice.toString(),
};
+12 -10
View File
@@ -17,7 +17,7 @@ export type OrderbookData = {
interface OrderbookManagerProps {
marketId: string;
onClick?: (args: { price?: string; size?: string }) => void;
onClick: (args: { price?: string; size?: string }) => void;
}
export const OrderbookManager = ({
@@ -61,15 +61,17 @@ export const OrderbookManager = ({
data={data}
reload={reload}
>
<Orderbook
bids={data?.depth.buy ?? []}
asks={data?.depth.sell ?? []}
decimalPlaces={market?.decimalPlaces ?? 0}
positionDecimalPlaces={market?.positionDecimalPlaces ?? 0}
assetSymbol={market?.tradableInstrument.instrument.product.quoteName}
onClick={onClick}
midPrice={marketData?.midPrice}
/>
{market && marketData && (
<Orderbook
bids={data?.depth.buy ?? []}
asks={data?.depth.sell ?? []}
decimalPlaces={market.decimalPlaces}
positionDecimalPlaces={market.positionDecimalPlaces}
assetSymbol={market.tradableInstrument.instrument.product.quoteName}
onClick={onClick}
lastTradedPrice={marketData.lastTradedPrice}
/>
)}
</AsyncRenderer>
);
};
+109 -114
View File
@@ -1,158 +1,153 @@
import React, { memo } from 'react';
import type { ReactNode } from 'react';
import { memo } from 'react';
import { addDecimal, addDecimalsFixedFormatNumber } from '@vegaprotocol/utils';
import { NumericCell, PriceCell } from '@vegaprotocol/datagrid';
import { NumericCell } from '@vegaprotocol/datagrid';
import { VolumeType } from './orderbook-data';
import classNames from 'classnames';
const HIDE_VOL_WIDTH = 190;
const HIDE_CUMULATIVE_VOL_WIDTH = 260;
interface OrderbookRowProps {
value: number;
cumulativeValue?: number;
cumulativeRelativeValue?: number;
volume: number;
cumulativeVolume: number;
decimalPlaces: number;
positionDecimalPlaces: number;
priceFormatDecimalPlaces: number;
price: string;
onClick?: (args: { price?: string; size?: string }) => void;
onClick: (args: { price?: string; size?: string }) => void;
type: VolumeType;
width: number;
maxVol: number;
}
const HIDE_VOL_WIDTH = 150;
const HIDE_CUMULATIVE_VOL_WIDTH = 220;
const CumulationBar = ({
cumulativeValue = 0,
type,
}: {
cumulativeValue?: number;
type: VolumeType;
}) => {
return (
<div
data-testid={`${VolumeType.bid === type ? 'bid' : 'ask'}-bar`}
className={classNames(
'absolute top-0 left-0 h-full',
type === VolumeType.bid
? 'bg-market-green-300 dark:bg-market-green/50'
: 'bg-market-red-300 dark:bg-market-red/30'
)}
style={{
width: `${cumulativeValue}%`,
}}
/>
);
};
const CumulativeVol = memo(
export const OrderbookRow = memo(
({
testId,
positionDecimalPlaces,
cumulativeValue,
onClick,
}: {
ask?: number;
bid?: number;
cumulativeValue?: number;
testId?: string;
className?: string;
positionDecimalPlaces: number;
onClick?: (size?: string | number) => void;
}) => {
const volume = cumulativeValue ? (
<NumericCell
testId={testId}
value={cumulativeValue}
valueFormatted={addDecimalsFixedFormatNumber(
cumulativeValue,
positionDecimalPlaces ?? 0
)}
/>
) : null;
return onClick && volume ? (
<button
onClick={() => onClick(cumulativeValue)}
className="hover:dark:bg-neutral-800 hover:bg-neutral-200 text-right pr-1"
>
{volume}
</button>
) : (
<div className="pr-1" data-testid={testId}>
{volume}
</div>
);
}
);
CumulativeVol.displayName = 'OrderBookCumulativeVol';
export const OrderbookRow = React.memo(
({
value,
cumulativeValue,
cumulativeRelativeValue,
volume,
cumulativeVolume,
decimalPlaces,
positionDecimalPlaces,
priceFormatDecimalPlaces,
price,
onClick,
type,
width,
maxVol,
}: OrderbookRowProps) => {
const txtId = type === VolumeType.bid ? 'bid' : 'ask';
const cols =
width >= HIDE_CUMULATIVE_VOL_WIDTH ? 3 : width >= HIDE_VOL_WIDTH ? 2 : 1;
return (
<div className="relative pr-1">
<CumulationBar cumulativeValue={cumulativeRelativeValue} type={type} />
<div className="relative px-1">
<CumulationBar
cumulativeVolume={cumulativeVolume}
type={type}
maxVol={maxVol}
/>
<div
data-testid={`${txtId}-rows-container`}
className={classNames('grid gap-1 text-right', `grid-cols-${cols}`)}
>
<PriceCell
testId={`price-${price}`}
value={BigInt(price)}
onClick={() =>
onClick && onClick({ price: addDecimal(price, decimalPlaces) })
}
valueFormatted={addDecimalsFixedFormatNumber(price, decimalPlaces)}
className={
type === VolumeType.ask
? 'text-market-red dark:text-market-red'
: 'text-market-green-600 dark:text-market-green'
}
/>
{width >= HIDE_VOL_WIDTH && (
<PriceCell
testId={`${txtId}-vol-${price}`}
onClick={(value) =>
onClick &&
value &&
onClick({
size: addDecimal(value, positionDecimalPlaces),
})
}
value={value}
<OrderBookRowCell
onClick={() => onClick({ price: addDecimal(price, decimalPlaces) })}
>
<NumericCell
testId={`price-${price}`}
value={BigInt(price)}
valueFormatted={addDecimalsFixedFormatNumber(
value,
positionDecimalPlaces
price,
decimalPlaces,
priceFormatDecimalPlaces
)}
className={classNames({
'text-market-red dark:text-market-red': type === VolumeType.ask,
'text-market-green-600 dark:text-market-green':
type === VolumeType.bid,
})}
/>
</OrderBookRowCell>
{width >= HIDE_VOL_WIDTH && (
<OrderBookRowCell
onClick={() =>
onClick({ size: addDecimal(volume, positionDecimalPlaces) })
}
>
<NumericCell
testId={`${txtId}-vol-${price}`}
value={volume}
valueFormatted={addDecimalsFixedFormatNumber(
volume,
positionDecimalPlaces ?? 0
)}
/>
</OrderBookRowCell>
)}
{width >= HIDE_CUMULATIVE_VOL_WIDTH && (
<CumulativeVol
testId={`cumulative-vol-${price}`}
<OrderBookRowCell
onClick={() =>
onClick &&
cumulativeValue &&
onClick({
size: addDecimal(cumulativeValue, positionDecimalPlaces),
size: addDecimal(cumulativeVolume, positionDecimalPlaces),
})
}
positionDecimalPlaces={positionDecimalPlaces}
cumulativeValue={cumulativeValue}
/>
>
<NumericCell
testId={`cumulative-vol-${price}`}
value={cumulativeVolume}
valueFormatted={addDecimalsFixedFormatNumber(
cumulativeVolume,
positionDecimalPlaces
)}
/>
</OrderBookRowCell>
)}
</div>
</div>
);
}
);
OrderbookRow.displayName = 'OrderbookRow';
const OrderBookRowCell = ({
children,
onClick,
}: {
children: ReactNode;
onClick: () => void;
}) => {
return (
<button
className="overflow-hidden text-right text-ellipsis whitespace-nowrap hover:dark:bg-neutral-800 hover:bg-neutral-200"
onClick={onClick}
>
{children}
</button>
);
};
const CumulationBar = ({
cumulativeVolume = 0,
type,
maxVol,
}: {
cumulativeVolume: number;
type: VolumeType;
maxVol: number;
}) => {
const width = (cumulativeVolume / maxVol) * 100;
return (
<div
data-testid={`${VolumeType.bid === type ? 'bid' : 'ask'}-bar`}
className={classNames(
'absolute top-0 left-0 h-full',
type === VolumeType.bid
? 'bg-market-green/10 dark:bg-market-green/10'
: 'bg-market-red/10 dark:bg-market-red/10'
)}
style={{
width: `${width}%`,
}}
/>
);
};
+131 -22
View File
@@ -1,8 +1,9 @@
import { render, waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { generateMockData, VolumeType } from './orderbook-data';
import { Orderbook } from './orderbook';
import { Orderbook, OrderbookMid } from './orderbook';
import * as orderbookData from './orderbook-data';
import { createResolutions, formatResolution } from './orderbook-controls';
function mockOffsetSize(width: number, height: number) {
Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', {
@@ -24,7 +25,7 @@ describe('Orderbook', () => {
numberOfSellRows: 100,
numberOfBuyRows: 100,
step: 1,
midPrice: '122900',
lastTradedPrice: '122900',
bestStaticBidPrice: 122905,
bestStaticOfferPrice: 122895,
decimalPlaces: 3,
@@ -37,20 +38,22 @@ describe('Orderbook', () => {
jest.clearAllMocks();
mockOffsetSize(800, 768);
});
it('markPrice should be in the middle', async () => {
it('lastTradedPrice should be in the middle', async () => {
render(
<Orderbook
decimalPlaces={decimalPlaces}
positionDecimalPlaces={0}
{...generateMockData(params)}
assetSymbol="USD"
onClick={jest.fn()}
/>
);
await waitFor(() =>
screen.getByTestId(`middle-mark-price-${params.midPrice}`)
screen.getByTestId(`last-traded-${params.lastTradedPrice}`)
);
expect(
screen.getByTestId(`middle-mark-price-${params.midPrice}`)
screen.getByTestId(`last-traded-${params.lastTradedPrice}`)
).toHaveTextContent('122.90');
});
@@ -68,10 +71,11 @@ describe('Orderbook', () => {
/>
);
expect(
await screen.findByTestId(`middle-mark-price-${params.midPrice}`)
await screen.findByTestId(`last-traded-${params.lastTradedPrice}`)
).toBeInTheDocument();
// Before resolution change the price is 122.934
await userEvent.click(await screen.getByTestId('price-122901'));
await userEvent.click(screen.getByTestId('price-122901'));
expect(onClickSpy).toBeCalledWith({ price: '122.901' });
await userEvent.click(screen.getByTestId('resolution'));
@@ -85,15 +89,16 @@ describe('Orderbook', () => {
expect(orderbookData.compactRows).toHaveBeenCalledWith(
mockedData.bids,
VolumeType.bid,
10
2
);
expect(orderbookData.compactRows).toHaveBeenCalledWith(
mockedData.asks,
VolumeType.ask,
10
2
);
await userEvent.click(await screen.getByTestId('price-12294'));
expect(onClickSpy).toBeCalledWith({ price: '122.94' });
await userEvent.click(screen.getByTestId('price-122938'));
expect(onClickSpy).toBeCalledWith({ price: '122.938' });
});
it('plus - minus buttons should change resolution', async () => {
@@ -113,26 +118,30 @@ describe('Orderbook', () => {
1
);
expect(screen.getByTestId('minus-button')).toBeDisabled();
userEvent.click(screen.getByTestId('plus-button'));
await userEvent.click(screen.getByTestId('plus-button'));
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
2
);
await userEvent.click(screen.getByTestId('plus-button'));
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
5
);
expect(screen.getByTestId('minus-button')).not.toBeDisabled();
await userEvent.click(screen.getByTestId('minus-button'));
await waitFor(() => {
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
10
2
);
});
expect(screen.getByTestId('minus-button')).not.toBeDisabled();
userEvent.click(screen.getByTestId('minus-button'));
await waitFor(() => {
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
1
);
});
expect(screen.getByTestId('minus-button')).toBeDisabled();
await userEvent.click(screen.getByTestId('resolution'));
await userEvent.click(screen.getByTestId('resolution'));
await waitFor(() => {
expect(screen.getByRole('menu')).toBeInTheDocument();
});
await userEvent.click(screen.getAllByRole('menuitem')[5]);
await userEvent.click(screen.getAllByRole('menuitem')[11]);
await waitFor(() => {
expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual(
100000
@@ -177,3 +186,103 @@ describe('Orderbook', () => {
});
});
});
describe('OrderbookMid', () => {
const props = {
lastTradedPrice: '100',
decimalPlaces: 0,
assetSymbol: 'BTC',
bestAskPrice: '101',
bestBidPrice: '99',
};
it('renders no change until lastTradedPrice changes', () => {
const { rerender } = render(<OrderbookMid {...props} />);
expect(screen.getByTestId(/last-traded/)).toHaveTextContent(
props.lastTradedPrice
);
expect(screen.getByText(props.assetSymbol)).toBeInTheDocument();
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
expect(screen.getByTestId('spread')).toHaveTextContent('(2)');
// rerender with no change should not show the icon
rerender(<OrderbookMid {...props} />);
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
rerender(
<OrderbookMid {...props} lastTradedPrice="101" bestAskPrice="102" />
);
expect(screen.getByTestId('icon-arrow-up')).toBeInTheDocument();
expect(screen.getByTestId('spread')).toHaveTextContent('(3)');
// rerender again with the same price, should still be set to 'up'
rerender(
<OrderbookMid
{...props}
lastTradedPrice="101"
bestAskPrice="102"
bestBidPrice="98"
/>
);
expect(screen.getByTestId('icon-arrow-up')).toBeInTheDocument();
expect(screen.getByTestId('spread')).toHaveTextContent('(4)');
rerender(<OrderbookMid {...props} lastTradedPrice="100" />);
expect(screen.getByTestId('icon-arrow-down')).toBeInTheDocument();
});
});
describe('createResolutions', () => {
it('create resolutions relative to the market', () => {
expect(
createResolutions(
'1', // 0.001
3
)
).toEqual([1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]);
expect(
createResolutions(
'190017', // 1900.17
2
)
).toEqual([1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 10000, 100000]);
expect(
createResolutions(
'123456789', // 1234.56789
5
)
).toEqual([
1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 10000, 100000, 1000000,
10000000, 100000000,
]);
});
it('removes resolutions that arent precise enough for the market', () => {
expect(
createResolutions(
'1', // 0.01
2
)
).toEqual([1, 2, 5, 10, 20, 50, 100]);
});
});
describe('formatResolution', () => {
it('formats less than 1', () => {
expect(formatResolution(1, 2)).toEqual('0.01');
expect(formatResolution(1, 3)).toEqual('0.001');
expect(formatResolution(2, 4)).toEqual('0.0002');
expect(formatResolution(5, 8)).toEqual('0.00000005');
expect(formatResolution(10000, 5)).toEqual('0.1');
});
it('formats greater than 1', () => {
expect(formatResolution(1000, 2)).toEqual('10');
expect(formatResolution(100000, 4)).toEqual('10');
expect(formatResolution(10000000, 2)).toEqual('100,000');
expect(formatResolution(500, 2)).toEqual('5');
expect(formatResolution(500, 1)).toEqual('50');
});
});
@@ -9,9 +9,9 @@ type Props = Omit<MockDataGeneratorParams, 'resolution'> & {
const OrderbookMockDataProvider = ({ decimalPlaces, ...props }: Props) => {
return (
<div className="absolute inset-0 dark:bg-black dark:text-neutral-200 bg-white text-neutral-800">
<div className="absolute inset-0 bg-white dark:bg-black dark:text-neutral-200 text-neutral-800">
<div
className="absolute left-0 top-0 bottom-0"
className="absolute top-0 bottom-0 left-0"
style={{ width: '400px' }}
>
<Orderbook
@@ -19,6 +19,7 @@ const OrderbookMockDataProvider = ({ decimalPlaces, ...props }: Props) => {
decimalPlaces={decimalPlaces}
{...generateMockData({ ...props })}
assetSymbol="USD"
onClick={() => undefined}
/>
</div>
</div>
+124 -152
View File
@@ -1,25 +1,15 @@
import { useMemo, useRef, useState } from 'react';
import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer';
import {
addDecimalsFormatNumber,
formatNumberFixed,
} from '@vegaprotocol/utils';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { usePrevious } from '@vegaprotocol/react-helpers';
import { OrderbookRow } from './orderbook-row';
import type { OrderbookRowData } from './orderbook-data';
import { compactRows, VolumeType } from './orderbook-data';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Splash,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { Splash, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth';
import { OrderbookControls } from './orderbook-controls';
// Sets row height, will be used to calculate number of rows that can be
// displayed each side of the book without overflow
@@ -27,35 +17,26 @@ export const rowHeight = 17;
const rowGap = 1;
const midHeight = 30;
type PriceChange = 'up' | 'down' | 'none';
const PRICE_CHANGE_ICON_MAP: Readonly<Record<PriceChange, VegaIconNames>> = {
up: VegaIconNames.ARROW_UP,
down: VegaIconNames.ARROW_DOWN,
none: VegaIconNames.BULLET,
};
const PRICE_CHANGE_CLASS_MAP: Readonly<Record<PriceChange, string>> = {
up: 'text-market-green-600 dark:text-market-green',
down: 'text-market-red dark:text-market-red',
none: 'text-vega-blue-500',
};
const OrderbookTable = ({
const OrderbookSide = ({
rows,
resolution,
type,
decimalPlaces,
positionDecimalPlaces,
priceFormatDecimalPlaces,
onClick,
width,
maxVol,
}: {
rows: OrderbookRowData[];
resolution: number;
decimalPlaces: number;
positionDecimalPlaces: number;
priceFormatDecimalPlaces: number;
type: VolumeType;
onClick?: (args: { price?: string; size?: string }) => void;
onClick: (args: { price?: string; size?: string }) => void;
width: number;
maxVol: number;
}) => {
return (
<div
@@ -74,15 +55,16 @@ const OrderbookTable = ({
{rows.map((data) => (
<OrderbookRow
key={data.price}
price={(BigInt(data.price) / BigInt(resolution)).toString()}
price={data.price}
onClick={onClick}
decimalPlaces={decimalPlaces - Math.log10(resolution)}
decimalPlaces={decimalPlaces}
positionDecimalPlaces={positionDecimalPlaces}
value={data.value}
cumulativeValue={data.cumulativeVol.value}
cumulativeRelativeValue={data.cumulativeVol.relativeValue}
priceFormatDecimalPlaces={priceFormatDecimalPlaces}
volume={data.volume}
cumulativeVolume={data.cumulativeVol}
type={type}
width={width}
maxVol={maxVol}
/>
))}
</div>
@@ -90,31 +72,88 @@ const OrderbookTable = ({
);
};
export const OrderbookMid = ({
lastTradedPrice,
decimalPlaces,
assetSymbol,
bestAskPrice,
bestBidPrice,
}: {
lastTradedPrice: string;
decimalPlaces: number;
assetSymbol: string;
bestAskPrice: string;
bestBidPrice: string;
}) => {
const previousLastTradedPrice = usePrevious(lastTradedPrice);
const priceChangeRef = useRef<'up' | 'down' | 'none'>('none');
const spread = (BigInt(bestAskPrice) - BigInt(bestBidPrice)).toString();
if (previousLastTradedPrice !== lastTradedPrice) {
priceChangeRef.current =
Number(previousLastTradedPrice) > Number(lastTradedPrice) ? 'down' : 'up';
}
return (
<div className="flex items-center justify-center text-base gap-2">
{priceChangeRef.current !== 'none' && (
<span
className={classNames('flex flex-col justify-center', {
'text-market-green-600 dark:text-market-green':
priceChangeRef.current === 'up',
'text-market-red dark:text-market-red':
priceChangeRef.current === 'down',
})}
>
<VegaIcon
name={
priceChangeRef.current === 'up'
? VegaIconNames.ARROW_UP
: VegaIconNames.ARROW_DOWN
}
/>
</span>
)}
<span
// monospace sizing doesn't quite align with alpha
className="font-mono text-[15px]"
data-testid={`last-traded-${lastTradedPrice}`}
title={t('Last traded price')}
>
{addDecimalsFormatNumber(lastTradedPrice, decimalPlaces)}
</span>
<span>{assetSymbol}</span>
<span
title={t('Spread')}
className="font-mono text-xs text-muted"
data-testid="spread"
>
({addDecimalsFormatNumber(spread, decimalPlaces)})
</span>
</div>
);
};
interface OrderbookProps {
decimalPlaces: number;
positionDecimalPlaces: number;
onClick?: (args: { price?: string; size?: string }) => void;
midPrice?: string;
onClick: (args: { price?: string; size?: string }) => void;
lastTradedPrice: string;
bids: PriceLevelFieldsFragment[];
asks: PriceLevelFieldsFragment[];
assetSymbol: string | undefined;
assetSymbol: string;
}
export const Orderbook = ({
decimalPlaces,
positionDecimalPlaces,
onClick,
midPrice,
lastTradedPrice,
asks,
bids,
assetSymbol,
}: OrderbookProps) => {
const [resolution, setResolution] = useState(1);
const resolutions = new Array(
Math.max(midPrice?.toString().length ?? 0, decimalPlaces + 1)
)
.fill(null)
.map((v, i) => Math.pow(10, i));
const groupedAsks = useMemo(() => {
return compactRows(asks, VolumeType.ask, resolution);
@@ -123,45 +162,18 @@ export const Orderbook = ({
const groupedBids = useMemo(() => {
return compactRows(bids, VolumeType.bid, resolution);
}, [bids, resolution]);
const [isOpen, setOpen] = useState(false);
const previousMidPrice = usePrevious(midPrice);
const priceChangeRef = useRef<'up' | 'down' | 'none'>('none');
if (midPrice && previousMidPrice !== midPrice) {
priceChangeRef.current =
(previousMidPrice || '') > midPrice ? 'down' : 'up';
}
const priceChangeIcon = (
<span
className={classNames(PRICE_CHANGE_CLASS_MAP[priceChangeRef.current])}
>
<VegaIcon name={PRICE_CHANGE_ICON_MAP[priceChangeRef.current]} />
</span>
// get the best bid/ask, note that we are using the pre aggregated
// values so we can render the most accurate spread in the mid section
const bestAskPrice = asks[0] ? asks[0].price : '0';
const bestBidPrice = bids[0] ? bids[0].price : '0';
// we'll want to only display a relevant number of dps based on the
// current resolution selection
const priceFormatDecimalPlaces = Math.ceil(
decimalPlaces - Math.log10(resolution)
);
const formatResolution = (r: number) => {
return formatNumberFixed(
Math.log10(r) - decimalPlaces > 0
? Math.pow(10, Math.log10(r) - decimalPlaces)
: 0,
decimalPlaces - Math.log10(r)
);
};
const increaseResolution = () => {
const index = resolutions.indexOf(resolution);
if (index < resolutions.length - 1) {
setResolution(resolutions[index + 1]);
}
};
const decreaseResolution = () => {
const index = resolutions.indexOf(resolution);
if (index > 0) {
setResolution(resolutions[index - 1]);
}
};
return (
<div className="h-full text-xs grid grid-rows-[1fr_min-content]">
<div>
@@ -171,55 +183,61 @@ export const Orderbook = ({
1,
Math.floor((height - midHeight) / 2 / (rowHeight + rowGap))
);
const askRows = groupedAsks?.slice(limit * -1) ?? [];
const bidRows = groupedBids?.slice(0, limit) ?? [];
const askRows = groupedAsks.slice(limit * -1);
const bidRows = groupedBids.slice(0, limit);
// this is used for providing a scale to render the volume
// bars based on the visible book
const deepestVisibleAsk = askRows[0];
const deepestVisibleBid = bidRows[bidRows.length - 1];
const maxVol = Math.max(
deepestVisibleAsk?.cumulativeVol || 0,
deepestVisibleBid?.cumulativeVol || 0
);
return (
<div
className="overflow-hidden grid"
data-testid="orderbook-grid-element"
style={{
width: width + 'px',
height: height + 'px',
width,
height,
gridTemplateRows: `1fr ${midHeight}px 1fr`, // cannot use tailwind here as tailwind will not parse a class string with interpolation
}}
>
{askRows.length || bidRows.length ? (
<>
<OrderbookTable
<OrderbookSide
rows={askRows}
type={VolumeType.ask}
resolution={resolution}
decimalPlaces={decimalPlaces}
positionDecimalPlaces={positionDecimalPlaces}
priceFormatDecimalPlaces={priceFormatDecimalPlaces}
onClick={onClick}
width={width}
maxVol={maxVol}
/>
<div className="flex items-center justify-center gap-2">
{midPrice && (
<>
<span
className="font-mono text-lg"
data-testid={`middle-mark-price-${midPrice}`}
>
{addDecimalsFormatNumber(midPrice, decimalPlaces)}
</span>
<span className="text-base">{assetSymbol}</span>
{priceChangeIcon}
</>
)}
</div>
<OrderbookTable
<OrderbookMid
lastTradedPrice={lastTradedPrice}
decimalPlaces={decimalPlaces}
assetSymbol={assetSymbol}
bestAskPrice={bestAskPrice}
bestBidPrice={bestBidPrice}
/>
<OrderbookSide
rows={bidRows}
type={VolumeType.bid}
resolution={resolution}
decimalPlaces={decimalPlaces}
positionDecimalPlaces={positionDecimalPlaces}
priceFormatDecimalPlaces={priceFormatDecimalPlaces}
onClick={onClick}
width={width}
maxVol={maxVol}
/>
</>
) : (
<div className="inset-0 absolute">
<div className="absolute inset-0">
<Splash>{t('No data')}</Splash>
</div>
)}
@@ -228,59 +246,13 @@ export const Orderbook = ({
}}
</ReactVirtualizedAutoSizer>
</div>
<div className="border-t border-default flex">
<button
onClick={increaseResolution}
disabled={resolutions.indexOf(resolution) >= resolutions.length - 1}
className="flex items-center border-r border-default px-2 cursor-pointer"
data-testid="plus-button"
>
<VegaIcon size={12} name={VegaIconNames.PLUS} />
</button>
<DropdownMenu
open={isOpen}
onOpenChange={(open) => setOpen(open)}
trigger={
<DropdownMenuTrigger
data-testid="resolution"
className="flex justify-between px-1 items-center"
style={{
width: `${
Math.max.apply(
null,
resolutions.map((item) => formatResolution(item).length)
) + 3
}ch`,
}}
>
<VegaIcon
size={12}
name={
isOpen ? VegaIconNames.CHEVRON_UP : VegaIconNames.CHEVRON_DOWN
}
/>
<div className="text-xs text-left">
{formatResolution(resolution)}
</div>
</DropdownMenuTrigger>
}
>
<DropdownMenuContent align="start">
{resolutions.map((r) => (
<DropdownMenuItem key={r} onClick={() => setResolution(r)}>
{formatResolution(r)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<button
onClick={decreaseResolution}
disabled={resolutions.indexOf(resolution) <= 0}
className="flex items-center border-x border-default px-2 cursor-pointer"
data-testid="minus-button"
>
<VegaIcon size={12} name={VegaIconNames.MINUS} />
</button>
<div className="border-t border-default">
<OrderbookControls
lastTradedPrice={lastTradedPrice}
resolution={resolution}
decimalPlaces={decimalPlaces}
setResolution={setResolution}
/>
</div>
</div>
);
+6 -4
View File
@@ -3,23 +3,23 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type MarketDataUpdateFieldsFragment = { __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null };
export type MarketDataUpdateFieldsFragment = { __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null };
export type MarketDataUpdateSubscriptionVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type MarketDataUpdateSubscription = { __typename?: 'Subscription', marketsData: Array<{ __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }> };
export type MarketDataUpdateSubscription = { __typename?: 'Subscription', marketsData: Array<{ __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }> };
export type MarketDataFieldsFragment = { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null };
export type MarketDataFieldsFragment = { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null };
export type MarketDataQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type MarketDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null } | null } }> } | null };
export type MarketDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null } | null } }> } | null };
export const MarketDataUpdateFieldsFragmentDoc = gql`
fragment MarketDataUpdateFields on ObservableMarketData {
@@ -56,6 +56,7 @@ export const MarketDataUpdateFieldsFragmentDoc = gql`
suppliedStake
targetStake
trigger
lastTradedPrice
}
`;
export const MarketDataFieldsFragmentDoc = gql`
@@ -95,6 +96,7 @@ export const MarketDataFieldsFragmentDoc = gql`
suppliedStake
targetStake
trigger
lastTradedPrice
}
`;
export const MarketDataUpdateDocument = gql`
+2
View File
@@ -32,6 +32,7 @@ fragment MarketDataUpdateFields on ObservableMarketData {
suppliedStake
targetStake
trigger
lastTradedPrice
}
subscription MarketDataUpdate($marketId: ID!) {
@@ -76,6 +77,7 @@ fragment MarketDataFields on MarketData {
suppliedStake
targetStake
trigger
lastTradedPrice
}
query MarketData($marketId: ID!) {
+2
View File
@@ -62,6 +62,7 @@ const marketDataFields: MarketDataFieldsFragment = {
markPrice: '4612690058',
midPrice: '4612690000',
openInterest: '0',
lastTradedPrice: '4612690000',
priceMonitoringBounds: [
{
minValidPrice: '654701',
@@ -99,6 +100,7 @@ const marketDataUpdateFields: MarketDataUpdateFieldsFragment = {
marketValueProxy: '',
markPrice: '4612690058',
midPrice: '0',
lastTradedPrice: '0',
openInterest: '0',
staticMidPrice: '0',
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
+1 -1
View File
@@ -199,7 +199,7 @@ export const allMarketsWithLiveDataProvider = makeDerivedDataProvider<
return data.find(
(market) =>
market.id ===
(parts[1].delta as MarketDataUpdateFieldsFragment).marketId
(parts[1].delta as MarketDataUpdateFieldsFragment)?.marketId
);
}
);
@@ -1,5 +1,5 @@
import { t } from '@vegaprotocol/i18n';
import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
import { TradingButton } from '@vegaprotocol/ui-toolkit';
import { useVegaTransactionStore, useVegaWallet } from '@vegaprotocol/wallet';
import { useHasAmendableOrder } from '../../order-hooks';
@@ -28,12 +28,7 @@ export const OpenOrdersMenu = ({ marketId }: { marketId: string }) => {
};
const CancelAllOrdersButton = ({ onClick }: { onClick: () => void }) => (
<TradingButton
intent={Intent.Primary}
size="extra-small"
onClick={onClick}
data-testid="cancelAll"
>
<TradingButton size="extra-small" onClick={onClick} data-testid="cancelAll">
{t('Cancel all')}
</TradingButton>
);
@@ -6,7 +6,7 @@ import type { PartialDeep } from 'type-fest';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { MockedProvider } from '@apollo/client/testing';
import type { OrderFieldsFragment, OrderListTableProps } from '../';
import type { Order, OrderFieldsFragment, OrderListTableProps } from '../';
import { OrderListTable } from '../';
import {
generateOrder,
@@ -164,6 +164,24 @@ describe('OrderListTable', () => {
);
});
it('negative positionDecimalPoints should be properly rendered in size column', async () => {
const localMarketOrder = {
...marketOrder,
size: '3000',
market: {
...marketOrder.market,
positionDecimalPlaces: -4,
},
} as Order;
await act(async () => {
render(generateJsx({ rowData: [localMarketOrder] }));
});
const cells = screen.getAllByRole('gridcell');
expect(cells[2]).toHaveTextContent('+30,000,000');
});
describe('amend cell', () => {
it('allows cancelling and editing for permitted orders', async () => {
const mockEdit = jest.fn();
@@ -10,7 +10,7 @@ import {
ActionsDropdown,
ButtonLink,
TradingDropdownCopyItem,
DropdownMenuItem,
TradingDropdownItem,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
@@ -277,7 +277,7 @@ export const OrderListTable = memo<
if (!data) return null;
return (
<div className="flex gap-2 items-center justify-end">
<div className="flex items-center justify-end gap-2">
{isOrderAmendable(data) && !props.isReadOnly && (
<>
{!data.icebergOrder && (
@@ -301,14 +301,14 @@ export const OrderListTable = memo<
value={data.id}
text={t('Copy order ID')}
/>
<DropdownMenuItem
<TradingDropdownItem
key={'view-order'}
data-testid="view-order"
onClick={() => onView(data)}
>
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View order details')}
</DropdownMenuItem>
</TradingDropdownItem>
</ActionsDropdown>
</div>
);
@@ -12,7 +12,7 @@ import {
ButtonLink,
VegaIcon,
VegaIconNames,
DropdownMenuItem,
TradingDropdownItem,
TradingDropdownCopyItem,
Pill,
} from '@vegaprotocol/ui-toolkit';
@@ -246,7 +246,7 @@ export const StopOrdersTable = memo(
if (!data) return null;
return (
<div className="flex gap-2 items-center justify-end">
<div className="flex items-center justify-end gap-2">
{data.status === Schema.StopOrderStatus.STATUS_PENDING &&
!props.isReadOnly && (
<ButtonLink
@@ -263,7 +263,7 @@ export const StopOrdersTable = memo(
value={data.order.id}
text={t('Copy order ID')}
/>
<DropdownMenuItem
<TradingDropdownItem
key={'view-order'}
data-testid="view-order"
onClick={() =>
@@ -273,7 +273,7 @@ export const StopOrdersTable = memo(
>
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View order details')}
</DropdownMenuItem>
</TradingDropdownItem>
</ActionsDropdown>
)}
</div>
@@ -61,7 +61,7 @@ describe('Positions', () => {
'Market',
'Size / Notional',
'Entry / Mark',
'Margin',
'Margin / Leverage',
'Liquidation',
'Realised PNL',
'Unrealised PNL',
@@ -201,6 +201,19 @@ describe('Positions', () => {
).not.toBeInTheDocument();
});
it('handle negative positionDecimalPlaces', async () => {
await renderComponent({
...singleRow,
openVolume: '-2000',
positionDecimalPlaces: -4,
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[1];
expect(within(cell).getByTestId('stack-cell-primary')).toHaveTextContent(
'-20,000,000'
);
});
describe('PNLCell', () => {
const props = {
data: undefined,
+4 -4
View File
@@ -292,7 +292,7 @@ export const PositionsTable = ({
},
},
{
headerName: t('Margin'),
headerName: t('Margin / Leverage'),
colId: 'margin',
type: 'rightAligned',
cellClass: 'font-mono text-right',
@@ -456,7 +456,7 @@ export const PositionsTable = ({
...COL_DEFS.actions,
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
return (
<div className="flex gap-2 items-center justify-end">
<div className="flex items-center justify-end gap-2">
{data?.openVolume &&
data?.openVolume !== '0' &&
data.partyId === pubKey ? (
@@ -548,9 +548,9 @@ const WarningCell = ({
showIcon?: boolean;
}) => {
return (
<div className="flex justify-end items-center">
<div className="flex items-center justify-end">
{showIcon && (
<span className="text-black dark:text-white mr-2">
<span className="mr-2 text-black dark:text-white">
<VegaIcon name={VegaIconNames.EXCLAIMATION_MARK} size={12} />
</span>
)}
@@ -35,16 +35,13 @@ export const ProposalsList = ({
const { columnDefs, defaultColDef } = useColumnDefs();
return (
<div className="relative h-full">
<AgGrid
className="w-full h-full"
columnDefs={columnDefs}
rowData={filteredData}
defaultColDef={defaultColDef}
getRowId={({ data }) => data.id}
overlayNoRowsTemplate={t('No markets')}
components={{ SuccessorMarketRenderer, MarketNameProposalCell }}
/>
</div>
<AgGrid
columnDefs={columnDefs}
rowData={filteredData}
defaultColDef={defaultColDef}
getRowId={({ data }) => data.id}
overlayNoRowsTemplate={t('No markets')}
components={{ SuccessorMarketRenderer, MarketNameProposalCell }}
/>
);
};
@@ -26,22 +26,36 @@ export const ProtocolUpgradeInProgressNotification = () => {
const [nextUpgrade] = useLocalStorageSnapshot(
NEXT_PROTOCOL_UPGRADE_PROPOSAL_SNAPSHOT
);
const { blocksRising, block } = useBlockRising();
const detailsLink = useProtocolUpgradeProposalLink();
let vegaReleaseTag: string | undefined;
let upgradeBlockHeight: string | undefined;
if (error && !data && nextUpgrade && ALLOW_STORED_PROPOSAL_DATA) {
const hasData = data && !error;
const hasStoredData = nextUpgrade && ALLOW_STORED_PROPOSAL_DATA;
if (hasData) {
// gets tag and height from the data api
vegaReleaseTag = data.vegaReleaseTag;
upgradeBlockHeight = data.upgradeBlockHeight;
} else if (hasStoredData) {
// gets tag and height from stored value if data api is unavailable
try {
const stored = JSON.parse(nextUpgrade) as StoredNextProtocolUpgradeData;
vegaReleaseTag = stored.vegaReleaseTag;
upgradeBlockHeight = stored.upgradeBlockHeight;
} catch {
// no op
// NOOP - could not parse stored data
}
}
const hasUpgradeInfo = vegaReleaseTag && upgradeBlockHeight;
const { blocksRising, block } = useBlockRising(
// skips querying blocks if there's no upgrade information available
!hasUpgradeInfo
);
/**
* If upgrade is in progress then none of the nodes should produce blocks,
* same should be with the tendermint block info otherwise it's a network
@@ -50,10 +64,7 @@ export const ProtocolUpgradeInProgressNotification = () => {
* Once the networks is back then the notification disappears.
*/
const upgradeInProgress =
vegaReleaseTag &&
upgradeBlockHeight &&
!blocksRising &&
block <= Number(upgradeBlockHeight);
hasUpgradeInfo && !blocksRising && block <= Number(upgradeBlockHeight);
if (!upgradeInProgress) return null;
@@ -15,31 +15,33 @@ const CHECK_INTERVAL = 5000; // ms
*/
const ALLOW_STALE = 2; // times -> MAX(this, 1) * CHECK_INTERVAL ~> min check time
export const useBlockRising = () => {
export const useBlockRising = (skip = false) => {
const [blocksRising, setBlocksRising] = useState(true);
const [block, setBlock] = useState(0);
const nodes = useEnvironment((state) => state.nodes);
const clients = useMemo(() => {
return nodes.map(
(n) =>
n &&
n.length > 0 &&
createClient({
return nodes.map((n) => {
if (n && n.length > 0) {
const client = createClient({
url: n,
cacheConfig: undefined,
retry: false,
connectToDevTools: false,
connectToHeaderStore: true,
})
);
});
return client;
}
return undefined;
});
}, [nodes]);
const { refetch: fetchBlockInfo } = useBlockInfo();
useEffect(() => {
if (skip) return;
let stale = 0;
let prev = 0;
const check = async () => {
const queries = clients.map((client, index) =>
const queries = clients.map((client) =>
client
? client
.query<BlockStatisticsQuery>({
@@ -47,18 +49,16 @@ export const useBlockRising = () => {
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
})
.catch((err) =>
Promise.reject(
`could not retrieve statistics from ${nodes[index]}`
)
)
.catch(() => {
// NOOP - could not retrieve statistics for that node (network error)
})
: undefined
);
const blockInfo = await fetchBlockInfo();
const results = (await Promise.allSettled(compact(queries))).map(
(res) => {
if (res && res.status === 'fulfilled') {
if (res && res.status === 'fulfilled' && res.value) {
return res.value.data.statistics;
} else {
return undefined;
@@ -86,7 +86,7 @@ export const useBlockRising = () => {
return () => {
clearInterval(interval);
};
}, [clients, fetchBlockInfo, blocksRising, nodes]);
}, [clients, fetchBlockInfo, blocksRising, nodes, skip]);
return { blocksRising, block };
};
@@ -5,6 +5,6 @@ import { toNumberParts } from '@vegaprotocol/utils';
export const useNumberParts = (
value: BigNumber | null | undefined,
decimals: number
): [integers: string, decimalPlaces: string] => {
): [integers: string, decimalPlaces: string, separator: string | undefined] => {
return useMemo(() => toNumberParts(value, decimals), [decimals, value]);
};
+10 -1
View File
@@ -1,9 +1,14 @@
import { BigNumber } from 'bignumber.js';
import { getUserLocale, formatNumber } from '@vegaprotocol/utils';
import {
getUserLocale,
formatNumber,
getUnlimitedThreshold,
} from '@vegaprotocol/utils';
const INFINITY = '∞';
const DEFAULT_COMPACT_ABOVE = 1_000_000;
const DEFAULT_COMPACT_CAP = new BigNumber(1e24);
/**
* Compacts given number to human readable format.
* @param number
@@ -35,6 +40,10 @@ export const CompactNumber = ({
const decimalPlaces =
(decimals === 'infer' ? number.decimalPlaces() : decimals) || 0;
if (number.isGreaterThan(getUnlimitedThreshold(decimalPlaces))) {
return <span data-testid={testId}>{INFINITY}</span>;
}
if (number.isLessThan(DEFAULT_COMPACT_ABOVE)) {
return (
<span data-testid={testId}>{formatNumber(number, decimalPlaces)}</span>

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