Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7ecf2ed8d | ||
|
|
6523490d96 | ||
|
|
a619b4a87e | ||
|
|
105a758e8d | ||
|
|
ce89961238 | ||
|
|
5fe92e0fb9 | ||
|
|
559ef48d6d | ||
|
|
255c3752f2 | ||
|
|
2bbf1e2b81 | ||
|
|
c471096d62 | ||
|
|
2ff5cc0ec8 | ||
|
|
06d3ef8d73 | ||
|
|
de4c7926c2 | ||
|
|
30c51ec415 | ||
|
|
36f558868d | ||
|
|
685a7d6278 | ||
|
|
50959b4c50 | ||
|
|
824495e070 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,475 +0,0 @@
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import type { DataSourceDefinition } from '@vegaprotocol/types';
|
||||
import {
|
||||
MarketState,
|
||||
MarketStateMapping,
|
||||
PropertyKeyType,
|
||||
} from '@vegaprotocol/types';
|
||||
import { addDays, subDays } from 'date-fns';
|
||||
import {
|
||||
chainIdQuery,
|
||||
statisticsQuery,
|
||||
createDataConnection,
|
||||
oracleSpecDataConnectionQuery,
|
||||
createMarketFragment,
|
||||
marketsQuery,
|
||||
marketsDataQuery,
|
||||
createMarketsDataFragment,
|
||||
assetQuery,
|
||||
networkParamsQuery,
|
||||
nodeGuardQuery,
|
||||
} from '@vegaprotocol/mock';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
getDateTimeFormat,
|
||||
} from '@vegaprotocol/utils';
|
||||
|
||||
describe('Closed markets', { tags: '@smoke' }, () => {
|
||||
const settlementDataProperty = 'settlement-data-property';
|
||||
const settlementDataPropertyKey = {
|
||||
__typename: 'PropertyKey' as const,
|
||||
name: settlementDataProperty,
|
||||
type: PropertyKeyType.TYPE_INTEGER,
|
||||
numberDecimalPlaces: 2,
|
||||
};
|
||||
const settlementDataSourceData: DataSourceDefinition = {
|
||||
sourceType: {
|
||||
sourceType: {
|
||||
filters: [
|
||||
{
|
||||
__typename: 'Filter',
|
||||
key: settlementDataPropertyKey,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const rowSelector =
|
||||
'[data-testid="tab-closed-markets"] .ag-center-cols-container .ag-row';
|
||||
|
||||
const assetsResult = assetQuery();
|
||||
// @ts-ignore asset definitely exists
|
||||
const settlementAsset = assetsResult.assetsConnection.edges[0].node;
|
||||
|
||||
const settledMarket = createMarketFragment({
|
||||
id: '0',
|
||||
state: MarketState.STATE_SETTLED,
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 10).toISOString(),
|
||||
close: subDays(new Date(), 4).toISOString(),
|
||||
},
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
dataSourceSpecBinding: {
|
||||
settlementDataProperty,
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
id: 'market-1-trading-termination-oracle-id',
|
||||
},
|
||||
dataSourceSpecForSettlementData: {
|
||||
id: 'market-1-settlement-data-oracle-id',
|
||||
data: settlementDataSourceData,
|
||||
},
|
||||
settlementAsset,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const terminatedMarket = createMarketFragment({
|
||||
id: '1',
|
||||
state: MarketState.STATE_TRADING_TERMINATED,
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 10).toISOString(),
|
||||
close: null, // market
|
||||
},
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
metadata: {
|
||||
tags: [
|
||||
`settlement-expiry-date:${addDays(new Date(), 4).toISOString()}`,
|
||||
],
|
||||
},
|
||||
product: {
|
||||
dataSourceSpecBinding: {
|
||||
settlementDataProperty,
|
||||
},
|
||||
dataSourceSpecForSettlementData: {
|
||||
id: 'market-1-settlement-data-oracle-id',
|
||||
data: settlementDataSourceData,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const delayedSettledMarket = createMarketFragment({
|
||||
id: '2',
|
||||
state: MarketState.STATE_TRADING_TERMINATED,
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 10).toISOString(),
|
||||
close: null, // market
|
||||
},
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
metadata: {
|
||||
tags: [
|
||||
`settlement-expiry-date:${subDays(new Date(), 2).toISOString()}`,
|
||||
],
|
||||
},
|
||||
product: {
|
||||
dataSourceSpecBinding: {
|
||||
settlementDataProperty,
|
||||
},
|
||||
dataSourceSpecForSettlementData: {
|
||||
id: 'market-1-settlement-data-oracle-id',
|
||||
data: settlementDataSourceData,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const unknownMarket = createMarketFragment({
|
||||
id: '3',
|
||||
state: MarketState.STATE_SETTLED,
|
||||
});
|
||||
|
||||
const closedMarketsResult = [
|
||||
{
|
||||
node: settledMarket,
|
||||
},
|
||||
{
|
||||
node: terminatedMarket,
|
||||
},
|
||||
{
|
||||
node: delayedSettledMarket,
|
||||
},
|
||||
{ node: unknownMarket },
|
||||
{
|
||||
node: createMarketFragment({ id: '4', state: MarketState.STATE_PENDING }),
|
||||
},
|
||||
{
|
||||
node: createMarketFragment({ id: '5', state: MarketState.STATE_ACTIVE }),
|
||||
},
|
||||
];
|
||||
|
||||
const settledMarketData = createMarketsDataFragment({
|
||||
market: {
|
||||
id: settledMarket.id,
|
||||
},
|
||||
bestBidPrice: '1000',
|
||||
bestOfferPrice: '2000',
|
||||
markPrice: '1500',
|
||||
});
|
||||
|
||||
const closedMarketsDataResult = [
|
||||
{
|
||||
node: {
|
||||
data: settledMarketData,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
data: createMarketsDataFragment({
|
||||
market: {
|
||||
id: terminatedMarket.id,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
data: createMarketsDataFragment({
|
||||
market: {
|
||||
id: delayedSettledMarket.id,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
data: createMarketsDataFragment({
|
||||
market: {
|
||||
id: unknownMarket.id,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const specDataConnection = createDataConnection();
|
||||
|
||||
before(() => {
|
||||
cy.setOnBoardingViewed();
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'ChainId', chainIdQuery());
|
||||
aliasGQLQuery(req, 'Statistics', statisticsQuery());
|
||||
aliasGQLQuery(req, 'NodeGuard', nodeGuardQuery());
|
||||
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
|
||||
aliasGQLQuery(
|
||||
req,
|
||||
'Markets',
|
||||
marketsQuery({
|
||||
marketsConnection: {
|
||||
edges: closedMarketsResult,
|
||||
},
|
||||
})
|
||||
);
|
||||
aliasGQLQuery(
|
||||
req,
|
||||
'MarketsData',
|
||||
marketsDataQuery({
|
||||
marketsConnection: {
|
||||
edges: closedMarketsDataResult,
|
||||
},
|
||||
})
|
||||
);
|
||||
aliasGQLQuery(
|
||||
req,
|
||||
'OracleSpecDataConnection',
|
||||
oracleSpecDataConnectionQuery()
|
||||
);
|
||||
});
|
||||
|
||||
cy.mockSubscription();
|
||||
|
||||
cy.visit('/#/markets/all');
|
||||
cy.get('[data-testid="Closed markets"]').click();
|
||||
});
|
||||
|
||||
it('renders a settled market', () => {
|
||||
const expectedMarkets = closedMarketsResult.filter((edge) => {
|
||||
return [
|
||||
MarketState.STATE_SETTLED,
|
||||
MarketState.STATE_TRADING_TERMINATED,
|
||||
].includes(edge.node.state);
|
||||
});
|
||||
const product = settledMarket.tradableInstrument.instrument.product;
|
||||
|
||||
// rows should be filtered to only include settled/terminated markets
|
||||
cy.get(rowSelector).should('have.length', expectedMarkets.length);
|
||||
|
||||
// check each column in the first row renders correctly
|
||||
// 6001-MARK-001
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="code"]')
|
||||
.find('[data-testid="market-code"]')
|
||||
.should('have.text', settledMarket.tradableInstrument.instrument.code);
|
||||
|
||||
// 6001-MARK-071
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[title="Future"]')
|
||||
.should('have.text', 'Futr');
|
||||
|
||||
// 6001-MARK-002
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="name"]')
|
||||
.should('have.text', settledMarket.tradableInstrument.instrument.name);
|
||||
|
||||
// 6001-MARK-003
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="state"]')
|
||||
.should('have.text', MarketStateMapping[settledMarket.state]);
|
||||
|
||||
// 6001-MARK-004
|
||||
// 6001-MARK-005
|
||||
// 6001-MARK-009
|
||||
// 6001-MARK-008
|
||||
// 6001-MARK-010
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="settlementDate"]')
|
||||
.find('[data-testid="link"]')
|
||||
.should(($el) => {
|
||||
const href = $el.attr('href');
|
||||
expect(href).to.match(
|
||||
new RegExp(
|
||||
`/oracles/${product.dataSourceSpecForTradingTermination.id}`
|
||||
)
|
||||
);
|
||||
})
|
||||
.should('have.text', '4 days ago')
|
||||
.should(
|
||||
'have.attr',
|
||||
'title',
|
||||
getDateTimeFormat().format(
|
||||
new Date(settledMarket.marketTimestamps.close)
|
||||
)
|
||||
);
|
||||
|
||||
// 6001-MARK-011
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="bestBidPrice"]')
|
||||
.should(
|
||||
'have.text',
|
||||
addDecimalsFormatNumber(
|
||||
settledMarketData.bestBidPrice,
|
||||
settledMarket.decimalPlaces
|
||||
)
|
||||
);
|
||||
|
||||
// 6001-MARK-012
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="bestOfferPrice"]')
|
||||
.should(
|
||||
'have.text',
|
||||
addDecimalsFormatNumber(
|
||||
settledMarketData.bestOfferPrice,
|
||||
settledMarket.decimalPlaces
|
||||
)
|
||||
);
|
||||
|
||||
// 6001-MARK-013
|
||||
cy.get(rowSelector).first().find('[col-id="markPrice"]').should(
|
||||
'have.text',
|
||||
|
||||
addDecimalsFormatNumber(
|
||||
settledMarketData.markPrice,
|
||||
settledMarket.decimalPlaces
|
||||
)
|
||||
);
|
||||
|
||||
// 6001-MARK-014
|
||||
// 6001-MARK-015
|
||||
// 6001-MARK-016
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="settlementDataOracleId"]')
|
||||
.find('[data-testid="link"]')
|
||||
.should(($el) => {
|
||||
const href = $el.attr('href');
|
||||
expect(href).to.match(
|
||||
new RegExp(`/oracles/${product.dataSourceSpecForSettlementData.id}`)
|
||||
);
|
||||
})
|
||||
.should(
|
||||
'have.text',
|
||||
addDecimalsFormatNumber(
|
||||
// @ts-ignore cannot deep un-partial
|
||||
specDataConnection.externalData.data.data[0].value,
|
||||
settlementDataPropertyKey.numberDecimalPlaces
|
||||
)
|
||||
);
|
||||
|
||||
// 6001-MARK-018
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="settlementAsset"]')
|
||||
.should('have.text', product.settlementAsset.symbol);
|
||||
|
||||
// 6001-MARK-020
|
||||
cy.get('.ag-pinned-right-cols-container')
|
||||
.find('[col-id="market-actions"]')
|
||||
.first()
|
||||
.find('button svg')
|
||||
.should('exist');
|
||||
if (Cypress.env('NX_SUCCESSOR_MARKETS')) {
|
||||
cy.get(rowSelector)
|
||||
.find('[col-id="successorMarket"]')
|
||||
.first()
|
||||
.should('have.text', '-');
|
||||
}
|
||||
});
|
||||
|
||||
// test market list for market in terminated state
|
||||
it('renders a terminated market', () => {
|
||||
cy.get(rowSelector)
|
||||
.eq(1)
|
||||
.find('[col-id="state"]')
|
||||
.should('have.text', MarketStateMapping[terminatedMarket.state]);
|
||||
|
||||
// 6001-MARK-006
|
||||
// 6001-MARK-007
|
||||
cy.get(rowSelector)
|
||||
.eq(1)
|
||||
.find('[col-id="settlementDate"]')
|
||||
.find('[data-testid="link"]')
|
||||
.should('have.text', 'Expected in 4 days');
|
||||
});
|
||||
|
||||
it('renders a terminated market which was expected to have settled', () => {
|
||||
cy.get(rowSelector)
|
||||
.eq(2)
|
||||
.find('[col-id="settlementDate"]')
|
||||
.should('have.class', 'text-danger')
|
||||
.find('[data-testid="link"]')
|
||||
.should('have.text', 'Expected 2 days ago');
|
||||
});
|
||||
|
||||
it('renders terminated market which doesnt have settlement date metadata', () => {
|
||||
cy.get(rowSelector)
|
||||
.eq(3)
|
||||
.find('[col-id="settlementDate"]')
|
||||
.find('[data-testid="link"]')
|
||||
.should('have.text', 'Unknown');
|
||||
});
|
||||
|
||||
it('can open asset detail dialog', () => {
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'Asset', assetsResult);
|
||||
});
|
||||
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="settlementAsset"]')
|
||||
.find('button')
|
||||
.click();
|
||||
|
||||
// 6001-MARK-019
|
||||
cy.get('[data-testid="dialog-title"]').should(
|
||||
'have.text',
|
||||
`Asset details - ${settlementAsset.symbol}`
|
||||
);
|
||||
|
||||
cy.get('[data-testid="dialog-close"]').click();
|
||||
});
|
||||
|
||||
it('can open row actions', () => {
|
||||
cy.get('.ag-pinned-right-cols-container')
|
||||
.find('[col-id="market-actions"]')
|
||||
.first()
|
||||
.find('button')
|
||||
.click();
|
||||
|
||||
const dropdownContent = '[data-testid="market-actions-content"]';
|
||||
const dropdownContentItem = '[role="menuitem"]';
|
||||
cy.get(dropdownContent)
|
||||
.find(dropdownContentItem)
|
||||
.eq(0)
|
||||
// Cannot click the copy button as it falls back to window.prompt, blocking the test.
|
||||
.should('have.text', 'Copy Market ID');
|
||||
|
||||
cy.get(dropdownContent)
|
||||
.find(dropdownContentItem)
|
||||
.eq(1)
|
||||
.find('a')
|
||||
.then(($el) => {
|
||||
const href = $el.attr('href');
|
||||
expect(/\/markets\/0/.test(href || '')).to.equal(true);
|
||||
})
|
||||
.should('have.text', 'View on Explorer');
|
||||
});
|
||||
});
|
||||
|
||||
describe('no closed markets', { tags: '@smoke', testIsolation: true }, () => {
|
||||
before(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.setOnBoardingViewed();
|
||||
cy.visit('/#/markets/all');
|
||||
cy.get('[data-testid="Closed markets"]').click();
|
||||
});
|
||||
|
||||
it('can see no markets message', () => {
|
||||
// 6001-MARK-034
|
||||
cy.getByTestId('tab-closed-markets').should('contain.text', 'No markets');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 midPrice = 'last-traded-4612690000';
|
||||
const priceResolution = 'resolution';
|
||||
const dealTicketPrice = 'order-price';
|
||||
const dealTicketSize = 'order-size';
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,7 +14,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.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
|
||||
|
||||
|
||||
@@ -14,7 +14,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.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
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,7 @@ 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
|
||||
|
||||
@@ -69,6 +69,7 @@ describe('MarketSelectorItem', () => {
|
||||
targetStake: '1000000',
|
||||
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
|
||||
priceMonitoringBounds: null,
|
||||
lastTradedPrice: '100',
|
||||
};
|
||||
|
||||
const candles = [
|
||||
|
||||
@@ -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')}>
|
||||
|
||||
@@ -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,66 @@
|
||||
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="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>
|
||||
<div className="mt-2">
|
||||
{t(
|
||||
'Help us identify bugs and improve Vega Governance by sharing anonymous usage data.'
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<VegaIcon name={VegaIconNames.EYE_OFF} size={24} />
|
||||
<div className="flex flex-col gap-1 ml-6">
|
||||
<div className="font-semibold">{t('Anonymous')}</div>
|
||||
<div>{t('Your identity is always anonymous on Vega')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center mt-2">
|
||||
<VegaIcon name={VegaIconNames.COG} size={24} />
|
||||
<div className="flex flex-col gap-1 ml-6">
|
||||
<div className="font-semibold">{t('Optional')}</div>
|
||||
<div>{t('You can opt out any time via settings')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-around items-center mt-10 gap-4 w-full px-4">
|
||||
<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="text-sm uppercase mb-1">
|
||||
{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,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];
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -97,7 +97,7 @@ export const AssetDetailsDialog = ({
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
<p className="text-sm my-4">
|
||||
<p className="text-xs my-4">
|
||||
{t(
|
||||
'There is 1 unit of the settlement asset (%s) to every 1 quote unit.',
|
||||
[assetSymbol]
|
||||
|
||||
@@ -3,7 +3,11 @@ 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 {
|
||||
CopyWithTooltip,
|
||||
Icon,
|
||||
truncateMiddle,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
@@ -56,7 +60,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,7 +113,9 @@ 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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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('');
|
||||
@@ -353,7 +354,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)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
Intent,
|
||||
ButtonLink,
|
||||
TradingSelect,
|
||||
truncateMiddle,
|
||||
} 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={() => {
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
TradingDropdown,
|
||||
TradingDropdownTrigger,
|
||||
TradingDropdownContent,
|
||||
TradingDropdownItem,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { formatNumberFixed } from '@vegaprotocol/utils';
|
||||
|
||||
export const OrderbookControls = ({
|
||||
lastTradedPrice,
|
||||
resolution,
|
||||
decimalPlaces,
|
||||
setResolution,
|
||||
}: {
|
||||
lastTradedPrice: string | undefined;
|
||||
resolution: number;
|
||||
decimalPlaces: number;
|
||||
setResolution: (resolution: number) => void;
|
||||
}) => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
|
||||
const resolutions = new Array(
|
||||
Math.max(lastTradedPrice?.toString().length ?? 0, decimalPlaces + 1)
|
||||
)
|
||||
.fill(null)
|
||||
.map((v, i) => Math.pow(10, i));
|
||||
|
||||
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="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"
|
||||
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 px-2 text-left gap-1"
|
||||
style={{
|
||||
minWidth: `${
|
||||
Math.max.apply(
|
||||
null,
|
||||
resolutions.map((item) => formatResolution(item).length)
|
||||
) + 5
|
||||
}ch`,
|
||||
}}
|
||||
>
|
||||
<VegaIcon
|
||||
size={12}
|
||||
name={
|
||||
isOpen ? VegaIconNames.CHEVRON_UP : VegaIconNames.CHEVRON_DOWN
|
||||
}
|
||||
/>
|
||||
{formatResolution(resolution)}
|
||||
</button>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
>
|
||||
<TradingDropdownContent align="start">
|
||||
{resolutions.map((r) => (
|
||||
<TradingDropdownItem key={r} onClick={() => setResolution(r)}>
|
||||
{formatResolution(r)}
|
||||
</TradingDropdownItem>
|
||||
))}
|
||||
</TradingDropdownContent>
|
||||
</TradingDropdown>
|
||||
<button
|
||||
onClick={decreaseResolution}
|
||||
disabled={resolutions.indexOf(resolution) <= 0}
|
||||
className="flex items-center px-2 cursor-pointer border-x border-default"
|
||||
data-testid="minus-button"
|
||||
>
|
||||
<VegaIcon size={12} name={VegaIconNames.MINUS} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,15 +5,11 @@ 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) => {
|
||||
@@ -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,14 +30,13 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,158 +1,149 @@
|
||||
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;
|
||||
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,
|
||||
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
|
||||
)}
|
||||
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}%`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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';
|
||||
|
||||
function mockOffsetSize(width: number, height: number) {
|
||||
@@ -24,7 +24,7 @@ describe('Orderbook', () => {
|
||||
numberOfSellRows: 100,
|
||||
numberOfBuyRows: 100,
|
||||
step: 1,
|
||||
midPrice: '122900',
|
||||
lastTradedPrice: '122900',
|
||||
bestStaticBidPrice: 122905,
|
||||
bestStaticOfferPrice: 122895,
|
||||
decimalPlaces: 3,
|
||||
@@ -44,13 +44,14 @@ describe('Orderbook', () => {
|
||||
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 +69,10 @@ 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'));
|
||||
@@ -92,7 +93,7 @@ describe('Orderbook', () => {
|
||||
VolumeType.ask,
|
||||
10
|
||||
);
|
||||
await userEvent.click(await screen.getByTestId('price-12294'));
|
||||
await userEvent.click(screen.getByTestId('price-12294'));
|
||||
expect(onClickSpy).toBeCalledWith({ price: '122.94' });
|
||||
});
|
||||
|
||||
@@ -177,3 +178,48 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,20 +17,7 @@ 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,
|
||||
@@ -48,14 +25,16 @@ const OrderbookTable = ({
|
||||
positionDecimalPlaces,
|
||||
onClick,
|
||||
width,
|
||||
maxVol,
|
||||
}: {
|
||||
rows: OrderbookRowData[];
|
||||
resolution: number;
|
||||
decimalPlaces: number;
|
||||
positionDecimalPlaces: number;
|
||||
type: VolumeType;
|
||||
onClick?: (args: { price?: string; size?: string }) => void;
|
||||
onClick: (args: { price?: string; size?: string }) => void;
|
||||
width: number;
|
||||
maxVol: number;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
@@ -78,11 +57,11 @@ const OrderbookTable = ({
|
||||
onClick={onClick}
|
||||
decimalPlaces={decimalPlaces - Math.log10(resolution)}
|
||||
positionDecimalPlaces={positionDecimalPlaces}
|
||||
value={data.value}
|
||||
cumulativeValue={data.cumulativeVol.value}
|
||||
cumulativeRelativeValue={data.cumulativeVol.relativeValue}
|
||||
volume={data.volume}
|
||||
cumulativeVolume={data.cumulativeVol}
|
||||
type={type}
|
||||
width={width}
|
||||
maxVol={maxVol}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -90,31 +69,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,44 +159,11 @@ 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>
|
||||
);
|
||||
|
||||
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]);
|
||||
}
|
||||
};
|
||||
// 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';
|
||||
|
||||
return (
|
||||
<div className="h-full text-xs grid grid-rows-[1fr_min-content]">
|
||||
@@ -171,21 +174,30 @@ 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}
|
||||
@@ -193,22 +205,16 @@ export const Orderbook = ({
|
||||
positionDecimalPlaces={positionDecimalPlaces}
|
||||
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}
|
||||
@@ -216,10 +222,11 @@ export const Orderbook = ({
|
||||
positionDecimalPlaces={positionDecimalPlaces}
|
||||
onClick={onClick}
|
||||
width={width}
|
||||
maxVol={maxVol}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="inset-0 absolute">
|
||||
<div className="absolute inset-0">
|
||||
<Splash>{t('No data')}</Splash>
|
||||
</div>
|
||||
)}
|
||||
@@ -228,59 +235,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
@@ -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`
|
||||
|
||||
@@ -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!) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export const IconEyeOff = ({ size = 16 }: { size: number }) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M16 7.97v-.02-.01-.02-.02a.672.672 0 00-.17-.36c-.49-.63-1.07-1.2-1.65-1.72l-3.16 2.26a2.978 2.978 0 01-2.98 2.9c-.31 0-.6-.06-.88-.15L5.09 12.3c.44.19.9.36 1.37.47.97.23 1.94.24 2.92.05.88-.17 1.74-.54 2.53-.98 1.25-.7 2.39-1.67 3.38-2.75.18-.2.37-.41.53-.62.09-.1.15-.22.17-.36v-.02-.02-.01-.02-.03c.01-.02.01-.03.01-.04zm-.43-4.17c.25-.18.43-.46.43-.8 0-.55-.45-1-1-1-.22 0-.41.08-.57.2l-.01-.01-2.67 1.91c-.69-.38-1.41-.69-2.17-.87a6.8 6.8 0 00-2.91-.05c-.88.18-1.74.54-2.53.99-1.25.7-2.39 1.67-3.38 2.75-.18.2-.37.41-.53.62-.23.29-.23.63-.01.92.51.66 1.11 1.25 1.73 1.79.18.16.38.29.56.44l-2.09 1.5.01.01c-.25.18-.43.46-.43.8 0 .55.45 1 1 1 .22 0 .41-.08.57-.2l.01.01 14-10-.01-.01zm-10.41 5a3.03 3.03 0 01-.11-.8 2.99 2.99 0 012.99-2.98c.62 0 1.19.21 1.66.53L5.16 8.8z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import { IconDeposit } from './svg-icons/icon-deposit';
|
||||
import { IconEdit } from './svg-icons/icon-edit';
|
||||
import { IconExclaimationMark } from './svg-icons/icon-exclaimation-mark';
|
||||
import { IconEye } from './svg-icons/icon-eye';
|
||||
import { IconEyeOff } from './svg-icons/icon-eye-off';
|
||||
import { IconForum } from './svg-icons/icon-forum';
|
||||
import { IconGlobe } from './svg-icons/icon-globe';
|
||||
import { IconInfo } from './svg-icons/icon-info';
|
||||
@@ -55,6 +56,7 @@ export enum VegaIconNames {
|
||||
EDIT = 'edit',
|
||||
EXCLAIMATION_MARK = 'exclaimation-mark',
|
||||
EYE = 'eye',
|
||||
EYE_OFF = 'eye-off',
|
||||
FORUM = 'forum',
|
||||
GLOBE = 'globe',
|
||||
INFO = 'info',
|
||||
@@ -90,6 +92,7 @@ export const VegaIconNameMap: Record<
|
||||
'chevron-down': IconChevronDown,
|
||||
'chevron-left': IconChevronLeft,
|
||||
'chevron-up': IconChevronUp,
|
||||
'eye-off': IconEyeOff,
|
||||
'exclaimation-mark': IconExclaimationMark,
|
||||
'open-external': IconOpenExternal,
|
||||
'question-mark': IconQuestionMark,
|
||||
|
||||
@@ -5,7 +5,11 @@ import {
|
||||
addDecimalsFormatNumberQuantum,
|
||||
formatNumber,
|
||||
formatNumberPercentage,
|
||||
getUnlimitedThreshold,
|
||||
isNumeric,
|
||||
removeDecimal,
|
||||
toBigNum,
|
||||
quantumDecimalPlaces,
|
||||
toDecimal,
|
||||
toNumberParts,
|
||||
} from './number';
|
||||
@@ -151,8 +155,94 @@ describe('number utils', () => {
|
||||
{ v: 7, o: '0.0000001' },
|
||||
{ v: 8, o: '0.00000001' },
|
||||
{ v: 9, o: '0.000000001' },
|
||||
{ v: -1, o: '10' },
|
||||
{ v: -2, o: '100' },
|
||||
{ v: -3, o: '1000' },
|
||||
])('formats with toNumber given number correctly', ({ v, o }) => {
|
||||
expect(toDecimal(v)).toStrictEqual(o);
|
||||
});
|
||||
});
|
||||
|
||||
describe('positive and negative decimals should be handled correctly', () => {
|
||||
const baseNum = '2000';
|
||||
const methods = [removeDecimal, toBigNum];
|
||||
it.each([
|
||||
{ decimals: 0, result: ['2000', '2000'] },
|
||||
{ decimals: 1, result: ['20000', '200'] },
|
||||
{ decimals: -1, result: ['200', '20000'] },
|
||||
{ decimals: 2, result: ['200000', '20'] },
|
||||
{ decimals: -2, result: ['20', '200000'] },
|
||||
{ decimals: 3, result: ['2000000', '2'] },
|
||||
{ decimals: -3, result: ['2', '2000000'] },
|
||||
{ decimals: 4, result: ['20000000', '0.2'] },
|
||||
{ decimals: -4, result: ['0', '20000000'] }, // removeDecimal has toFixed(0) at the end
|
||||
])(
|
||||
'number methods should handle negative decimals',
|
||||
({ decimals, result }) => {
|
||||
methods.forEach((method, i) => {
|
||||
expect(method(baseNum, decimals).toString()).toEqual(result[i]);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('quantumDecimalPlaces', () => {
|
||||
it.each([
|
||||
['1', 1, 3],
|
||||
['10', 1, 2],
|
||||
['100', 1, 1],
|
||||
['1000', 1, 0],
|
||||
['1', 2, 4],
|
||||
['10', 2, 3],
|
||||
['100', 2, 2],
|
||||
['1000', 2, 1],
|
||||
['1', 3, 5],
|
||||
['10', 3, 4],
|
||||
['100', 3, 3],
|
||||
['1000', 3, 2],
|
||||
['1', 18, 20],
|
||||
['1000000000', 18, 11],
|
||||
['5000000000', 18, 11],
|
||||
['1000000000000000000', 18, 2],
|
||||
])(
|
||||
'converts quantum %s of %d decimal places to %d quant. decimal places',
|
||||
(quantum, decimals, output) => {
|
||||
expect(quantumDecimalPlaces(quantum, decimals)).toEqual(output);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('getUnlimitedThreshold', () => {
|
||||
it.each([
|
||||
[
|
||||
0,
|
||||
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+76',
|
||||
],
|
||||
[
|
||||
1,
|
||||
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+75',
|
||||
],
|
||||
[
|
||||
2,
|
||||
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+74',
|
||||
],
|
||||
[
|
||||
3,
|
||||
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+73',
|
||||
],
|
||||
[
|
||||
10,
|
||||
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+66',
|
||||
],
|
||||
[
|
||||
18,
|
||||
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+58',
|
||||
],
|
||||
])(
|
||||
'given %d decimal places it returns unlimited threshold %s',
|
||||
(decimals, output) => {
|
||||
expect(getUnlimitedThreshold(decimals).toString()).toEqual(output);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,12 +4,24 @@ import memoize from 'lodash/memoize';
|
||||
|
||||
import { getUserLocale } from '../get-user-locale';
|
||||
|
||||
/**
|
||||
* A raw unformatted value greater than this is considered and displayed
|
||||
* as UNLIMITED.
|
||||
*/
|
||||
export const UNLIMITED_THRESHOLD = new BigNumber(2).pow(256).times(0.8);
|
||||
/**
|
||||
* Gets the unlimited threshold value for given decimal places.
|
||||
* @param decimalPlaces the asset's decimal places
|
||||
*/
|
||||
export const getUnlimitedThreshold = (decimalPlaces: number) =>
|
||||
UNLIMITED_THRESHOLD.dividedBy(Math.pow(10, decimalPlaces));
|
||||
|
||||
const MIN_FRACTION_DIGITS = 2;
|
||||
const MAX_FRACTION_DIGITS = 20;
|
||||
|
||||
export function toDecimal(numberOfDecimals: number) {
|
||||
return new BigNumber(1)
|
||||
.dividedBy(Math.pow(10, numberOfDecimals))
|
||||
.dividedBy(new BigNumber(10).exponentiatedBy(numberOfDecimals))
|
||||
.toString(10);
|
||||
}
|
||||
|
||||
@@ -17,7 +29,8 @@ export function toBigNum(
|
||||
rawValue: string | number,
|
||||
decimals: number
|
||||
): BigNumber {
|
||||
return new BigNumber(rawValue || 0).dividedBy(Math.pow(10, decimals));
|
||||
const divides = new BigNumber(10).exponentiatedBy(decimals);
|
||||
return new BigNumber(rawValue || 0).dividedBy(divides);
|
||||
}
|
||||
|
||||
export function addDecimal(
|
||||
@@ -36,7 +49,8 @@ export function removeDecimal(
|
||||
value: string | BigNumber,
|
||||
decimals: number
|
||||
): string {
|
||||
return new BigNumber(value || 0).times(Math.pow(10, decimals)).toFixed(0);
|
||||
const times = new BigNumber(10).exponentiatedBy(decimals);
|
||||
return new BigNumber(value || 0).times(times).toFixed(0);
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
|
||||
@@ -90,6 +104,25 @@ export const formatNumberFixed = (
|
||||
return getFixedNumberFormat(formatDecimals).format(Number(rawValue));
|
||||
};
|
||||
|
||||
export const quantumDecimalPlaces = (
|
||||
/** Raw asset's quantum value */
|
||||
rawQuantum: number | string,
|
||||
/** Asset's decimal places */
|
||||
decimalPlaces: number
|
||||
) => {
|
||||
// if raw quantum value is an empty string then it'll evaluate to 0
|
||||
// this check ignores NaNs and zeroes
|
||||
const formatDecimals =
|
||||
isNaN(Number(rawQuantum)) || Number(rawQuantum) === 0
|
||||
? decimalPlaces
|
||||
: Math.max(
|
||||
0,
|
||||
Math.log10(100 / Number(addDecimal(rawQuantum, decimalPlaces)))
|
||||
);
|
||||
|
||||
return Math.ceil(formatDecimals);
|
||||
};
|
||||
|
||||
export const addDecimalsFormatNumberQuantum = (
|
||||
rawValue: string | number,
|
||||
decimalPlaces: number,
|
||||
|
||||
@@ -4,6 +4,12 @@ export const validateAmount = (step: number | string, field: string) => {
|
||||
const [, stepDecimals = ''] = String(step).split('.');
|
||||
|
||||
return (value?: string) => {
|
||||
if (Number(step) > 1) {
|
||||
if (Number(value) % Number(step) > 0) {
|
||||
return t(`${field} must be a multiple of ${step} for this market`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const [, valueDecimals = ''] = (value || '').split('.');
|
||||
if (stepDecimals.length < valueDecimals.length) {
|
||||
if (stepDecimals === '') {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Dialog,
|
||||
ExternalLink,
|
||||
Intent,
|
||||
Pill,
|
||||
TradingButton,
|
||||
@@ -40,7 +39,7 @@ import { useVegaWallet } from '../use-vega-wallet';
|
||||
import { InjectedConnectorForm } from './injected-connector-form';
|
||||
import { isBrowserWalletInstalled } from '../utils';
|
||||
import { useIsWalletServiceRunning } from '../use-is-wallet-service-running';
|
||||
import { SnapStatus, useSnapStatus } from '../use-snap-status';
|
||||
import { useIsSnapRunning } from '../use-is-snap-running';
|
||||
import { useVegaWalletDialogStore } from './vega-wallet-dialog-store';
|
||||
|
||||
export const CLOSE_DELAY = 1700;
|
||||
@@ -159,7 +158,7 @@ const ConnectDialogContainer = ({
|
||||
appChainId
|
||||
);
|
||||
|
||||
const snapStatus = useSnapStatus(
|
||||
const isSnapRunning = useIsSnapRunning(
|
||||
DEFAULT_SNAP_ID,
|
||||
Boolean(connectors['snap'])
|
||||
);
|
||||
@@ -184,7 +183,7 @@ const ConnectDialogContainer = ({
|
||||
setWalletUrl={setWalletUrl}
|
||||
onSelect={handleSelect}
|
||||
isDesktopWalletRunning={isDesktopWalletRunning}
|
||||
snapStatus={snapStatus}
|
||||
isSnapRunning={isSnapRunning}
|
||||
/>
|
||||
)}
|
||||
</ConnectDialogContent>
|
||||
@@ -199,14 +198,14 @@ const ConnectorList = ({
|
||||
walletUrl,
|
||||
setWalletUrl,
|
||||
isDesktopWalletRunning,
|
||||
snapStatus,
|
||||
isSnapRunning,
|
||||
}: {
|
||||
connectors: Connectors;
|
||||
onSelect: (type: WalletType) => void;
|
||||
walletUrl: string;
|
||||
setWalletUrl: (value: string) => void;
|
||||
isDesktopWalletRunning: boolean | null;
|
||||
snapStatus: SnapStatus;
|
||||
isSnapRunning: boolean | null;
|
||||
}) => {
|
||||
const { pubKey, links } = useVegaWallet();
|
||||
const title = isBrowserWalletInstalled()
|
||||
@@ -250,7 +249,7 @@ const ConnectorList = ({
|
||||
</div>
|
||||
{connectors['snap'] !== undefined ? (
|
||||
<div>
|
||||
{snapStatus === SnapStatus.INSTALLED ? (
|
||||
{isSnapRunning ? (
|
||||
<ConnectionOption
|
||||
type="snap"
|
||||
text={
|
||||
@@ -268,34 +267,22 @@ const ConnectorList = ({
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ConnectionOption
|
||||
type="snap"
|
||||
disabled={snapStatus === SnapStatus.NOT_SUPPORTED}
|
||||
text={
|
||||
<>
|
||||
<div className="flex items-center justify-center w-full h-full text-base gap-1">
|
||||
{t('Install Vega MetaMask Snap')}
|
||||
</div>
|
||||
<div className="absolute top-0 flex items-center h-8 right-1">
|
||||
<VegaIcon name={VegaIconNames.METAMASK} size={24} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
onClick={() => {
|
||||
requestSnap(DEFAULT_SNAP_ID);
|
||||
}}
|
||||
/>
|
||||
{snapStatus === SnapStatus.NOT_SUPPORTED ? (
|
||||
<p className="pt-2 text-sm text-default">
|
||||
{t('No MetaMask version that supports snaps detected.')}{' '}
|
||||
{t('Learn more about')}{' '}
|
||||
<ExternalLink href="https://metamask.io/snaps/">
|
||||
MetaMask Snaps
|
||||
</ExternalLink>
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
<ConnectionOption
|
||||
type="snap"
|
||||
text={
|
||||
<>
|
||||
<div className="flex items-center justify-center w-full h-full text-base gap-1">
|
||||
{t('Install Vega MetaMask Snap')}
|
||||
</div>
|
||||
<div className="absolute top-0 flex items-center h-8 right-1">
|
||||
<VegaIcon name={VegaIconNames.METAMASK} size={24} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
onClick={() => {
|
||||
requestSnap(DEFAULT_SNAP_ID);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -118,10 +118,14 @@ export const getSnap = async (
|
||||
snapId: string,
|
||||
version?: string
|
||||
): Promise<Snap | undefined> => {
|
||||
const snaps = await getSnaps();
|
||||
return Object.values(snaps).find(
|
||||
(snap) => snap.id === snapId && (!version || snap.version === version)
|
||||
);
|
||||
try {
|
||||
const snaps = await getSnaps();
|
||||
return Object.values(snaps).find(
|
||||
(snap) => snap.id === snapId && (!version || snap.version === version)
|
||||
);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const invokeSnap = async <T>(
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getSnap } from './connectors';
|
||||
|
||||
const INTERVAL = 2_000;
|
||||
|
||||
export const useIsSnapRunning = (snapId: string, shouldCheck: boolean) => {
|
||||
const [running, setRunning] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!shouldCheck) return;
|
||||
|
||||
const checkState = async () => {
|
||||
const snap = await getSnap(snapId);
|
||||
setRunning(!!snap);
|
||||
};
|
||||
|
||||
const i = setInterval(() => {
|
||||
checkState();
|
||||
}, INTERVAL);
|
||||
|
||||
checkState();
|
||||
|
||||
return () => {
|
||||
clearInterval(i);
|
||||
};
|
||||
}, [snapId, shouldCheck]);
|
||||
return running;
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getSnap } from './connectors';
|
||||
|
||||
const INTERVAL = 2_000;
|
||||
|
||||
export enum SnapStatus {
|
||||
NOT_SUPPORTED,
|
||||
INSTALLED,
|
||||
NOT_INSTALLED,
|
||||
}
|
||||
|
||||
export const useSnapStatus = (snapId: string, shouldCheck: boolean) => {
|
||||
const [status, setStatus] = useState<SnapStatus>(SnapStatus.NOT_INSTALLED);
|
||||
useEffect(() => {
|
||||
if (!shouldCheck) return;
|
||||
|
||||
const checkState = async () => {
|
||||
try {
|
||||
const snap = await getSnap(snapId);
|
||||
setStatus(snap ? SnapStatus.INSTALLED : SnapStatus.NOT_INSTALLED);
|
||||
} catch (err) {
|
||||
setStatus(SnapStatus.NOT_SUPPORTED);
|
||||
}
|
||||
};
|
||||
|
||||
const i = setInterval(() => {
|
||||
checkState();
|
||||
}, INTERVAL);
|
||||
|
||||
checkState();
|
||||
|
||||
return () => {
|
||||
clearInterval(i);
|
||||
};
|
||||
}, [snapId, shouldCheck]);
|
||||
return status;
|
||||
};
|
||||
@@ -13,6 +13,8 @@ When looking to use Vega via a user interface e.g. Dapp (Decentralized web App),
|
||||
- If there is not running desktop wallet or CLI detected, I can see that I need to open my wallet. (<a name="0002-WCON-0013" href="#0002-WCON-0013">0002-WCON-0013</a>)
|
||||
- I can find out more about supported browsers i.e. there is a link (Issue "List compatible browsers" vegawallet-browser#360 has to be implemented). (<a name="0002-WCON-0013" href="#0002-WCON-0013">0002-WCON-0013</a>)
|
||||
- I can find out more about the Vega Wallet and see what "other" versions there are i.e. there is a link to the page on the website (currently - https://vega.xyz/wallet#overview). (<a name="0002-WCON-0014" href="#0002-WCON-0014">0002-WCON-0014</a>)
|
||||
- I can install and connect vega metamask snap wallet (<a name="0002-WCON-0015" href="#0002-WCON-0015">0002-WCON-0015</a>)
|
||||
|
||||
- Browser wallet:
|
||||
|
||||
- The browser extension you need is automatically detected if you are using Chrome or Firefox, presenting the specific call to action to install that browser extension in a visible way e.g. with a Chrome or Firefox icon. (<a name="0002-WCON-041" href="#0002-WCON-041">0002-WCON-041</a>)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
- **Must** There is a link to try out trading on Fairground when I'm on Mainnet (<a name="0007-FUGS-008" href="#0007-FUGS-008">0007-FUGS-008</a>)
|
||||
- **Must** There is a link to trade with real funds on Mainnet when I am on Fairground (<a name="0007-FUGS-010" href="#0007-FUGS-010">0007-FUGS-010</a>)
|
||||
- **Must** When I am on the Fairground version, I can see a warning / call out that this is Fairground meaning I can try out with virtual assets at no risk (<a name="0007-FUGS-011" href="#0007-FUGS-011">0007-FUGS-011</a>)
|
||||
- If I dismiss the popup, I **must** not see it unless I NOT accomplish full "onboarding"
|
||||
- If I dismiss the popup, I **must** not see it anymore (<a name="0007-FUGS-018" href="#0007-FUGS-018">0007-FUGS-018</a>)
|
||||
- If I dismiss the popup, I land on the default market (<a name="0007-FUGS-012" href="#0007-FUGS-012">0007-FUGS-012</a>)
|
||||
|
||||
## When the popup has been dismissed:
|
||||
@@ -33,3 +33,7 @@
|
||||
- **Must** We've replaced "connect wallet" in the top right with "get started" (<a name="0007-FUGS-015" href="#0007-FUGS-015">0007-FUGS-015</a>)
|
||||
- **Must** When I press the get started CTA, I see the wallet connect popup (<a name="0007-FUGS-016" href="#0007-FUGS-016">0007-FUGS-016</a>)
|
||||
- **Must** If I have a wallet installed already I don't see this quick start onboarding, and instead call(s) to action in Console revert to connect wallet, not "get started" (button in nav header) (<a name="0007-FUGS-017" href="#0007-FUGS-017">0007-FUGS-017</a>)
|
||||
|
||||
## When onboarding process has been accomplished:
|
||||
|
||||
- I can see telemetry approval toast: on environment other than mainnet telemetry is enabled by default (<a name="0007-FUGS-019" href="#0007-FUGS-019">0007-FUGS-019</a>)
|
||||
|
||||
@@ -11,4 +11,7 @@ As a market user I want to see information about orders existing in the market.
|
||||
- I **Must** see prices sorted descending (<a name="6003-ORDB-007" href="#6003-ORDB-007">6003-ORDB-007</a>)
|
||||
- I **Must** be able to set a resolution of data (<a name="6003-ORDB-008" href="#6003-ORDB-008">6003-ORDB-008</a>)
|
||||
- When I click specific price, it **Must** be copied to deal ticket form (<a name="6003-ORDB-009" href="#6003-ORDB-009">6003-ORDB-009</a>)
|
||||
- Order is removed from orderbook if traded away(<a name="6003-ORDB-010" href="#6003-ORDB-010">6003-ORDB-010</a>)
|
||||
- Order is removed from orderbook if traded away (<a name="6003-ORDB-010" href="#6003-ORDB-010">6003-ORDB-010</a>)
|
||||
- Spread (bestAsk - bestOffer) is show in the mid secion (<a name="6003-ORDB-011" href="#6003-ORDB-011">6003-ORDB-011</a>)
|
||||
- Cumulative volume is displayed visually (volume bars) as a proportion of the entire book volume (<a name="6003-ORDB-012" href="#6003-ORDB-012">6003-ORDB-012</a>)
|
||||
- Mid section shows the last traded price movement using an arrow (<a name="6003-ORDB-013" href="#6003-ORDB-013">6003-ORDB-013</a>)
|
||||
|
||||
@@ -174,11 +174,34 @@ When populating a deal ticket I...
|
||||
- **must** see that selecting "Post only" or "reduce only" de-select the other (if selected). As is it not possibly to apply them both to an order (<a name="7003-SORD-059" href="#7003-SORD-059">7003-SORD-059</a>)
|
||||
|
||||
- **must** see an option to select expire for stop order limit and market (<a name="7002-SORD-071" href="#7002-SORD-071">7002-SORD-071</a>)
|
||||
|
||||
- **must** select strategy for stop order limit and market (<a name="7002-SORD-072" href="#7002-SORD-072">7002-SORD-072</a>)
|
||||
- **must** see strategy submit (<a name="7002-SORD-073" href="#7002-SORD-073">7002-SORD-073</a>)
|
||||
if it is oco then submit strategy is not available (<a name="7002-SORD-081" href="#7002-SORD-081">7002-SORD-081</a>)
|
||||
- **must** see strategy cancel (<a name="7002-SORD-074" href="#7002-SORD-074">7002-SORD-074</a>)
|
||||
- **must** see expiry time/date (<a name="7002-SORD-075" href="#7002-SORD-075">7002-SORD-075</a>)
|
||||
|
||||
- **must** see an option to select oco for stop order limit and market (<a name="7002-SORD-082" href="#7002-SORD-082">7002-SORD-082</a>)
|
||||
- **must** see a type to select market or limit (<a name="7002-SORD-083" href="#7002-SORD-083">7002-SORD-083</a>)
|
||||
- **must** see for market type
|
||||
- **must** see rises above (<a name="7002-SORD-084" href="#7002-SORD-084">7002-SORD-084</a>)
|
||||
- **must** see falls below (<a name="7002-SORD-085" href="#7002-SORD-085">7002-SORD-085</a>)
|
||||
- **must** enter a trigger value (<a name="7002-SORD-086" href="#7002-SORD-086">7002-SORD-086</a>)
|
||||
- **must** select a trigger price (<a name="7002-SORD-087" href="#7002-SORD-087">7002-SORD-087</a>)
|
||||
- **must** select a trigger trailing percent offset (<a name="7002-SORD-088" href="#7002-SORD-088">7002-SORD-088</a>)
|
||||
- **must** see for limit type
|
||||
- **must** enter a price (<a name="7002-SORD-089" href="#7002-SORD-089">7002-SORD-089</a>)
|
||||
- **must** select a trigger direction (<a name="7002-SORD-090" href="#7002-SORD-090">7002-SORD-090</a>)
|
||||
- **must** see rises above (<a name="7002-SORD-099" href="#7002-SORD-099">7002-SORD-099</a>)
|
||||
- **must** see falls below (<a name="7002-SORD-091" href="#7002-SORD-091">7002-SORD-091</a>)
|
||||
- if the user has not set a preference: trigger direction **must** default to `opposite to the one selected in the stop order` (<a name="7002-SORD-092" href="#7002-SORD-092">7002-SORD-092</a>)
|
||||
- **must** enter a trigger value (<a name="7002-SORD-093" href="#7002-SORD-093">7002-SORD-093</a>)
|
||||
- **must** select a trigger type (<a name="7002-SORD-094" href="#7002-SORD-094">7002-SORD-094</a>)
|
||||
- **must** see a trigger price (<a name="7002-SORD-095" href="#7002-SORD-095">7002-SORD-095</a>)
|
||||
- **must** see a trigger trailing percent offset (<a name="7002-SORD-096" href="#7002-SORD-096">7002-SORD-096</a>)
|
||||
- if the user has not set a preference: trigger type **must** default to `price` (<a name="7002-SORD-097" href="#7002-SORD-097">7002-SORD-097</a>)
|
||||
- if the trigger price is already met on submission, **must** warn the user that the stop order will be triggered immediately (<a name="7002-SORD-098" href="#7002-SORD-098">7002-SORD-098</a>)
|
||||
|
||||
... so I can control in more detail how my order is executed
|
||||
|
||||
## Auto Populating a deal ticket non-manual methods
|
||||
|
||||
@@ -37,14 +37,11 @@ EOT
|
||||
auth_soft_fail = true
|
||||
}
|
||||
docker_service "postgres-1" {
|
||||
image = "vegaprotocol/timescaledb:2.8.0-pg14"
|
||||
# Read me for more details: https://github.com/vegaprotocol/docker/tree/main/timescaledb#timescaledb
|
||||
image = "vegaprotocol/vegacapsule-timescaledb:2.8.0-pg14-v0.0.1"
|
||||
cmd = "postgres"
|
||||
args = []
|
||||
env = {
|
||||
POSTGRES_USER="vega"
|
||||
POSTGRES_PASSWORD="vega"
|
||||
POSTGRES_DBS="vega0,vega1,vega2,vega3,vega4,vega5,vega6,vega7,vega8,vega9,vega10,vega11,vega12,vega13,vega14,vega15,vega16,vega17,vega18,vega19,vega20,vega21,vega22,vega23,vega24,vega25"
|
||||
}
|
||||
env = {}
|
||||
|
||||
static_port {
|
||||
value = 5232
|
||||
|
||||
Reference in New Issue
Block a user