Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5abb0302a3 | ||
|
|
0fb8ee3abb | ||
|
|
3422b99491 | ||
|
|
287e294281 | ||
|
|
dc959025c6 |
@@ -1,218 +0,0 @@
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import type { MarketsQuery } from '@vegaprotocol/markets';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const rowSelector =
|
||||
'[data-testid="tab-open-markets"] .ag-center-cols-container .ag-row';
|
||||
const colInstrumentCode =
|
||||
'[col-id="tradableInstrument.instrument.code"] [data-testid="market-code"]';
|
||||
|
||||
describe('markets all table', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.clearLocalStorage().then(() => {
|
||||
cy.setOnBoardingViewed();
|
||||
cy.mockTradingPage(
|
||||
Schema.MarketState.STATE_ACTIVE,
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
|
||||
);
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/all');
|
||||
});
|
||||
});
|
||||
|
||||
it('can see table headers', () => {
|
||||
cy.wait('@Markets');
|
||||
cy.wait('@MarketsData');
|
||||
const headers = [
|
||||
'Market',
|
||||
'Description',
|
||||
'Trading mode',
|
||||
'Status',
|
||||
'Successor market',
|
||||
'Best bid',
|
||||
'Best offer',
|
||||
'Mark price',
|
||||
'Settlement asset',
|
||||
'',
|
||||
];
|
||||
cy.getByTestId('tab-open-markets').within(($headers) => {
|
||||
cy.wrap($headers)
|
||||
.get('.ag-header-cell-text')
|
||||
.each(($header, i) => {
|
||||
cy.wrap($header).should('have.text', headers[i]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('markets tab should be rendered properly', () => {
|
||||
cy.get('[data-testid="Open markets"]').should(
|
||||
'have.attr',
|
||||
'data-state',
|
||||
'active'
|
||||
);
|
||||
cy.get('[data-testid="Proposed markets"]').should(
|
||||
'have.attr',
|
||||
'data-state',
|
||||
'inactive'
|
||||
);
|
||||
cy.get('[data-testid="Closed markets"]').should(
|
||||
'have.attr',
|
||||
'data-state',
|
||||
'inactive'
|
||||
);
|
||||
});
|
||||
it('renders markets correctly', () => {
|
||||
// 6001-MARK-035
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find(colInstrumentCode)
|
||||
.should('have.text', 'SOLUSD');
|
||||
|
||||
// 6001-MARK-073
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[title="Future"]')
|
||||
.should('have.text', 'Futr');
|
||||
|
||||
// 6001-MARK-036
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="tradableInstrument.instrument.name"]')
|
||||
.should('have.text', 'SUSPENDED MARKET');
|
||||
|
||||
// 6001-MARK-037
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="tradingMode"]')
|
||||
.should('have.text', 'Continuous');
|
||||
|
||||
// 6001-MARK-038
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="state"]')
|
||||
.should('have.text', 'Active');
|
||||
|
||||
// 6001-MARK-039
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="data.bestBidPrice"]')
|
||||
.should('have.text', '0.00');
|
||||
|
||||
// 6001-MARK-040
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="data.bestOfferPrice"]')
|
||||
.should('have.text', '0.00');
|
||||
|
||||
// 6001-MARK-041
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find('[col-id="data.markPrice"]')
|
||||
.should('have.text', '84.41');
|
||||
|
||||
// 6001-MARK-042
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find(
|
||||
'[col-id="tradableInstrument.instrument.product.settlementAsset.symbol"]'
|
||||
)
|
||||
.should('have.text', 'XYZalpha');
|
||||
|
||||
// 6001-MARK-043
|
||||
cy.get(rowSelector)
|
||||
.first()
|
||||
.find(
|
||||
'[col-id="tradableInstrument.instrument.product.settlementAsset.symbol"] button'
|
||||
)
|
||||
.click();
|
||||
cy.getByTestId('dialog-title').should('have.text', 'Asset details - tEURO');
|
||||
cy.getByTestId('close-asset-details-dialog').click();
|
||||
});
|
||||
|
||||
it('can open row actions', () => {
|
||||
// 6001-MARK-044
|
||||
cy.get('.ag-pinned-right-cols-container')
|
||||
.find('[col-id="market-actions"]')
|
||||
.first()
|
||||
.find('button')
|
||||
.click();
|
||||
|
||||
// 6001-MARK-045
|
||||
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');
|
||||
|
||||
// 6001-MARK-046
|
||||
cy.get(dropdownContent)
|
||||
.find(dropdownContentItem)
|
||||
.eq(1)
|
||||
.find('a')
|
||||
.then(($el) => {
|
||||
const href = $el.attr('href');
|
||||
expect(/\/markets\/market-1/.test(href || '')).to.equal(true);
|
||||
})
|
||||
.should('have.text', 'View on Explorer');
|
||||
|
||||
// 6001-MARK-047
|
||||
cy.get(dropdownContent)
|
||||
.find(dropdownContentItem)
|
||||
.eq(2)
|
||||
.should('have.text', 'View settlement asset details');
|
||||
cy.getByTestId('market-actions-content').click();
|
||||
});
|
||||
|
||||
it('able to open and sort full market list - market page', () => {
|
||||
// 6001-MARK-064
|
||||
const ExpectedSortedMarkets = [
|
||||
'AAPL.MF21',
|
||||
'BTCUSD.MF21',
|
||||
'ETHBTC.QM21',
|
||||
'SOLUSD',
|
||||
];
|
||||
cy.get('[data-testid="Open markets"]').click({ force: true });
|
||||
cy.url().should('eq', Cypress.config('baseUrl') + '/#/markets/all');
|
||||
cy.contains('AAPL.MF21').should('be.visible');
|
||||
cy.get('.ag-header-cell-label').contains('Market').click(); // sort by market name
|
||||
for (let i = 0; i < ExpectedSortedMarkets.length; i++) {
|
||||
cy.get(`[row-index=${i}]`)
|
||||
.find(colInstrumentCode)
|
||||
.should('have.text', ExpectedSortedMarkets[i]);
|
||||
}
|
||||
});
|
||||
|
||||
it('can drag and drop columns', () => {
|
||||
// 6001-MARK-065
|
||||
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
|
||||
cy.get(colInstrumentCode)
|
||||
.realMouseDown()
|
||||
.realMouseMove(700, 15)
|
||||
.realMouseUp();
|
||||
cy.get(colInstrumentCode).should(($element) => {
|
||||
const attributeValue = $element.attr('aria-colindex');
|
||||
expect(attributeValue).not.to.equal('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('no open markets', { tags: '@smoke', testIsolation: true }, () => {
|
||||
before(() => {
|
||||
cy.mockTradingPage();
|
||||
const markets: MarketsQuery = {};
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'Markets', markets);
|
||||
});
|
||||
cy.mockSubscription();
|
||||
cy.setOnBoardingViewed();
|
||||
cy.visit('/#/markets/all');
|
||||
});
|
||||
|
||||
it.skip('can see no markets message', () => {
|
||||
// 6001-MARK-048
|
||||
cy.getByTestId('tab-open-markets').should('contain.text', 'No markets');
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,10 @@ body,
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
@apply tracking-tighter;
|
||||
}
|
||||
|
||||
.text-default {
|
||||
@apply text-vega-clight-50 dark:text-vega-cdark-50;
|
||||
}
|
||||
@@ -60,6 +64,10 @@ html.dark {
|
||||
|
||||
html [data-theme='dark'],
|
||||
html [data-theme='light'] {
|
||||
/* fonts */
|
||||
--pennant-font-family-base: theme(fontFamily.alpha);
|
||||
--pennant-font-family-monospace: theme(fontFamily.mono);
|
||||
|
||||
/* sell candles only use stroke as the candle is solid (without border) */
|
||||
--pennant-color-sell-stroke: theme(colors.market.red.DEFAULT);
|
||||
|
||||
@@ -147,7 +155,7 @@ html [data-theme='dark'] {
|
||||
}
|
||||
|
||||
.vega-ag-grid .ag-header-row {
|
||||
@apply font-alpha font-normal;
|
||||
@apply font-normal font-alpha;
|
||||
}
|
||||
|
||||
/* Light variables */
|
||||
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
TradingSelect as Select,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
|
||||
import { getDerivedPrice } from '@vegaprotocol/markets';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ExpirySelector } from './expiry-selector';
|
||||
import { SideSelector } from './side-selector';
|
||||
@@ -35,10 +36,10 @@ import { TypeToggle } from './type-selector';
|
||||
import {
|
||||
useDealTicketFormValues,
|
||||
DealTicketType,
|
||||
type StopOrderFormValues,
|
||||
dealTicketTypeToOrderType,
|
||||
isStopOrderType,
|
||||
} from '../../hooks/use-form-values';
|
||||
import type { StopOrderFormValues } from '../../hooks/use-form-values';
|
||||
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-submission';
|
||||
import { DealTicketButton } from './deal-ticket-button';
|
||||
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
||||
@@ -632,11 +633,11 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
/>
|
||||
<Size control={control} sizeStep={sizeStep} />
|
||||
<TimeInForce control={control} />
|
||||
<div className="flex gap-2 pb-3 justify-end">
|
||||
<div className="flex justify-end pb-3 gap-2">
|
||||
<ReduceOnly />
|
||||
</div>
|
||||
<hr className="mb-4 border-vega-clight-500 dark:border-vega-cdark-500" />
|
||||
<div className="flex gap-2 pb-2 justify-between">
|
||||
<div className="flex justify-between pb-2 gap-2">
|
||||
<Controller
|
||||
name="oco"
|
||||
control={control}
|
||||
@@ -713,7 +714,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
/>
|
||||
<Size control={control} sizeStep={sizeStep} oco />
|
||||
<TimeInForce control={control} oco />
|
||||
<div className="flex gap-2 mb-2 justify-end">
|
||||
<div className="flex justify-end mb-2 gap-2">
|
||||
<ReduceOnly />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -38,8 +38,6 @@ import {
|
||||
} from '@vegaprotocol/utils';
|
||||
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
||||
import { getDerivedPrice } from '@vegaprotocol/markets';
|
||||
import type { OrderInfo } from '@vegaprotocol/types';
|
||||
|
||||
import {
|
||||
validateExpiration,
|
||||
validateMarketState,
|
||||
@@ -59,8 +57,6 @@ import {
|
||||
useMarketAccountBalance,
|
||||
useAccountBalance,
|
||||
} from '@vegaprotocol/accounts';
|
||||
|
||||
import { OrderType } from '@vegaprotocol/types';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import {
|
||||
DealTicketType,
|
||||
@@ -71,6 +67,7 @@ import type { OrderFormValues } from '../../hooks/use-form-values';
|
||||
import { useDealTicketFormValues } from '../../hooks/use-form-values';
|
||||
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
||||
import noop from 'lodash/noop';
|
||||
import { isNonPersistentOrder } from '../../utils/time-in-force-persistance';
|
||||
|
||||
export const REDUCE_ONLY_TOOLTIP =
|
||||
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.';
|
||||
@@ -230,8 +227,8 @@ export const DealTicket = ({
|
||||
});
|
||||
const openVolume = useOpenVolume(pubKey, market.id) ?? '0';
|
||||
const orders = activeOrders
|
||||
? activeOrders.map<OrderInfo>((order) => ({
|
||||
isMarketOrder: order.type === OrderType.TYPE_MARKET,
|
||||
? activeOrders.map<Schema.OrderInfo>((order) => ({
|
||||
isMarketOrder: order.type === Schema.OrderType.TYPE_MARKET,
|
||||
price: order.price,
|
||||
remaining: order.remaining,
|
||||
side: order.side,
|
||||
@@ -239,7 +236,7 @@ export const DealTicket = ({
|
||||
: [];
|
||||
if (normalizedOrder) {
|
||||
orders.push({
|
||||
isMarketOrder: normalizedOrder.type === OrderType.TYPE_MARKET,
|
||||
isMarketOrder: normalizedOrder.type === Schema.OrderType.TYPE_MARKET,
|
||||
price: normalizedOrder.price ?? '0',
|
||||
remaining: normalizedOrder.size,
|
||||
side: normalizedOrder.side,
|
||||
@@ -307,12 +304,10 @@ export const DealTicket = ({
|
||||
pubKey,
|
||||
]);
|
||||
|
||||
const disablePostOnlyCheckbox = [
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
].includes(timeInForce);
|
||||
|
||||
const disableReduceOnlyCheckbox = !disablePostOnlyCheckbox;
|
||||
const nonPersistentOrder = isNonPersistentOrder(timeInForce);
|
||||
const disablePostOnlyCheckbox = nonPersistentOrder;
|
||||
const disableReduceOnlyCheckbox = !nonPersistentOrder;
|
||||
const disableIcebergCheckbox = nonPersistentOrder;
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(formValues: OrderFormValues) => {
|
||||
@@ -468,6 +463,8 @@ export const DealTicket = ({
|
||||
value={field.value}
|
||||
orderType={type}
|
||||
onSelect={(value) => {
|
||||
// If GTT is selected and no expiresAt time is set, or its
|
||||
// behind current time then reset the value to current time
|
||||
if (
|
||||
value === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT &&
|
||||
(!expiresAt || new Date(expiresAt).getTime() < Date.now())
|
||||
@@ -476,6 +473,12 @@ export const DealTicket = ({
|
||||
shouldValidate: true,
|
||||
});
|
||||
}
|
||||
|
||||
// iceberg orders must be persistent orders, so if user
|
||||
// switches to to a non persisten tif value, remove iceberg selection
|
||||
if (iceberg && isNonPersistentOrder(value)) {
|
||||
setValue('iceberg', false);
|
||||
}
|
||||
field.onChange(value);
|
||||
}}
|
||||
market={market}
|
||||
@@ -502,7 +505,7 @@ export const DealTicket = ({
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex gap-2 pb-2 justify-between">
|
||||
<div className="flex justify-between pb-2 gap-2">
|
||||
<Controller
|
||||
name="postOnly"
|
||||
control={control}
|
||||
@@ -568,7 +571,7 @@ export const DealTicket = ({
|
||||
</div>
|
||||
{type === Schema.OrderType.TYPE_LIMIT && (
|
||||
<>
|
||||
<div className="flex gap-2 pb-2 justify-between">
|
||||
<div className="flex justify-between pb-2 gap-2">
|
||||
<Controller
|
||||
name="iceberg"
|
||||
control={control}
|
||||
@@ -577,6 +580,7 @@ export const DealTicket = ({
|
||||
name="iceberg"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={disableIcebergCheckbox}
|
||||
label={
|
||||
<Tooltip
|
||||
description={
|
||||
|
||||
@@ -18,6 +18,8 @@ export const ExpirySelector = ({
|
||||
onSelect,
|
||||
errorMessage,
|
||||
}: ExpirySelectorProps) => {
|
||||
const minDateRef = useRef(new Date());
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<TradingFormGroup
|
||||
@@ -31,7 +33,7 @@ export const ExpirySelector = ({
|
||||
type="datetime-local"
|
||||
value={value && formatForInput(new Date(value))}
|
||||
onChange={(e) => onSelect(e.target.value)}
|
||||
min={formatForInput(useRef(new Date()).current)}
|
||||
min={formatForInput(minDateRef.current)}
|
||||
hasError={!!errorMessage}
|
||||
/>
|
||||
{errorMessage && (
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
} from '../hooks/use-form-values';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||
import { isPersistentOrder } from './time-in-force-persistance';
|
||||
|
||||
export const mapFormValuesToOrderSubmission = (
|
||||
order: OrderFormValues,
|
||||
@@ -41,11 +42,8 @@ export const mapFormValuesToOrderSubmission = (
|
||||
? false
|
||||
: order.reduceOnly,
|
||||
icebergOpts:
|
||||
(order.type === Schema.OrderType.TYPE_MARKET ||
|
||||
[
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
].includes(order.timeInForce)) &&
|
||||
order.type === Schema.OrderType.TYPE_LIMIT &&
|
||||
isPersistentOrder(order.timeInForce) &&
|
||||
order.iceberg &&
|
||||
order.peakSize &&
|
||||
order.minimumVisibleSize
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { mapFormValuesToOrderSubmission } from './map-form-values-to-submission';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||
import type { OrderFormValues } from '../hooks';
|
||||
|
||||
describe('mapFormValuesToOrderSubmission', () => {
|
||||
it('sets and formats price only for limit orders', () => {
|
||||
@@ -25,7 +27,7 @@ describe('mapFormValuesToOrderSubmission', () => {
|
||||
).toEqual('10000');
|
||||
});
|
||||
|
||||
it('sets and formats expiresAt only for time in force orders', () => {
|
||||
it('sets and formats expiresAt only for GTT orders', () => {
|
||||
expect(
|
||||
mapFormValuesToOrderSubmission(
|
||||
{
|
||||
@@ -49,6 +51,41 @@ describe('mapFormValuesToOrderSubmission', () => {
|
||||
).toEqual('1640995200000000000');
|
||||
});
|
||||
|
||||
it('sets and formats icebergOpts only for persisted orders', () => {
|
||||
expect(
|
||||
mapFormValuesToOrderSubmission(
|
||||
{
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
iceberg: true,
|
||||
peakSize: '10.00',
|
||||
minimumVisibleSize: '10.00',
|
||||
} as OrderFormValues,
|
||||
'marketId',
|
||||
2,
|
||||
2
|
||||
).icebergOpts
|
||||
).toEqual(undefined);
|
||||
|
||||
expect(
|
||||
mapFormValuesToOrderSubmission(
|
||||
{
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
iceberg: true,
|
||||
peakSize: '10.00',
|
||||
minimumVisibleSize: '10.00',
|
||||
} as OrderFormValues,
|
||||
'marketId',
|
||||
2,
|
||||
2
|
||||
).icebergOpts
|
||||
).toEqual({
|
||||
peakSize: '1000',
|
||||
minimumVisibleSize: '1000',
|
||||
});
|
||||
});
|
||||
|
||||
it('formats size', () => {
|
||||
expect(
|
||||
mapFormValuesToOrderSubmission(
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { OrderTimeInForce } from '@vegaprotocol/types';
|
||||
import {
|
||||
isNonPersistentOrder,
|
||||
isPersistentOrder,
|
||||
} from './time-in-force-persistance';
|
||||
|
||||
it('isNonPeristentOrder', () => {
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(true);
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(true);
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(false);
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTT)).toBe(false);
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFA)).toBe(false);
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFN)).toBe(false);
|
||||
});
|
||||
|
||||
it('isPeristentOrder', () => {
|
||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(false);
|
||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(false);
|
||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(true);
|
||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTT)).toBe(true);
|
||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFA)).toBe(true);
|
||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFN)).toBe(true);
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { OrderTimeInForce } from '@vegaprotocol/types';
|
||||
|
||||
export const isNonPersistentOrder = (timeInForce: OrderTimeInForce) => {
|
||||
return [
|
||||
OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
].includes(timeInForce);
|
||||
};
|
||||
|
||||
export const isPersistentOrder = (timeInForce: OrderTimeInForce) => {
|
||||
return !isNonPersistentOrder(timeInForce);
|
||||
};
|
||||
@@ -1,8 +1,16 @@
|
||||
import { isNumeric } from '@vegaprotocol/utils';
|
||||
import { PriceChangeCell } from '@vegaprotocol/datagrid';
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumberPercentage,
|
||||
isNumeric,
|
||||
priceChange,
|
||||
priceChangePercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { PriceChangeCell, signedNumberCssClass } from '@vegaprotocol/datagrid';
|
||||
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useCandles } from '../../hooks/use-candles';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
marketId?: string;
|
||||
@@ -47,10 +55,39 @@ export const Last24hPriceChange = ({
|
||||
if (error || !isNumeric(decimalPlaces)) {
|
||||
return <span>-</span>;
|
||||
}
|
||||
|
||||
const candles = oneDayCandles?.map((c) => c.close) || initialValue || [];
|
||||
const change = priceChange(candles);
|
||||
const changePercentage = priceChangePercentage(candles);
|
||||
|
||||
return (
|
||||
<PriceChangeCell
|
||||
candles={oneDayCandles?.map((c) => c.close) || initialValue || []}
|
||||
decimalPlaces={decimalPlaces}
|
||||
/>
|
||||
<span
|
||||
className={classNames(
|
||||
'flex items-center gap-1',
|
||||
signedNumberCssClass(change)
|
||||
)}
|
||||
>
|
||||
<Arrow value={change} />
|
||||
<span data-testid="price-change-percentage">
|
||||
{formatNumberPercentage(new BigNumber(changePercentage.toString()), 2)}
|
||||
</span>
|
||||
<span data-testid="price-change">
|
||||
{addDecimalsFormatNumber(change.toString(), decimalPlaces ?? 0, 3)}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const Arrow = ({ value }: { value: number | bigint }) => {
|
||||
const size = 10;
|
||||
|
||||
if (value > 0) {
|
||||
return <VegaIcon name={VegaIconNames.ARROW_UP} size={size} />;
|
||||
}
|
||||
|
||||
if (value < 0) {
|
||||
return <VegaIcon name={VegaIconNames.ARROW_DOWN} size={size} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { useTimeToUpgrade } from './use-time-to-upgrade';
|
||||
import {
|
||||
ERR_NO_TIME_UNITS,
|
||||
parseDuration,
|
||||
useTimeToUpgrade,
|
||||
} from './use-time-to-upgrade';
|
||||
|
||||
jest.mock('./__generated__/BlockStatistics', () => ({
|
||||
...jest.requireActual('./__generated__/BlockStatistics'),
|
||||
@@ -8,7 +12,7 @@ jest.mock('./__generated__/BlockStatistics', () => ({
|
||||
data: {
|
||||
statistics: {
|
||||
blockHeight: 1,
|
||||
blockDuration: 500,
|
||||
blockDuration: '500ms',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -30,3 +34,25 @@ describe('useTimeToUpgrade', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseDuration', () => {
|
||||
it.each([
|
||||
['1000000ns', 1],
|
||||
['1000µs', 1],
|
||||
['1ms', 1],
|
||||
['1s', 1000],
|
||||
['1m', 60 * 1000],
|
||||
['1h', 60 * 60 * 1000],
|
||||
// below test cases are from vega
|
||||
['3.3s', 3300],
|
||||
['4m5s', 4 * 60 * 1000 + 5 * 1000],
|
||||
['4m5.001s', 4 * 60 * 1000 + 5001],
|
||||
['5h6m7.001s', 5 * 60 * 60 * 1000 + 6 * 60 * 1000 + 7001],
|
||||
['8m0.000000001s', 8 * 60 * 1000 + 1 / 1000000],
|
||||
])('parses %s to %d milliseconds', (input, output) => {
|
||||
expect(parseDuration(input)).toEqual(output);
|
||||
});
|
||||
it('throws an error when given corrupted data', () => {
|
||||
expect(() => parseDuration('blah')).toThrow(ERR_NO_TIME_UNITS);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,52 @@ const DEFAULT_POLLS = 10;
|
||||
const INTERVAL = 1000;
|
||||
const durations = [] as number[];
|
||||
|
||||
export const ERR_NO_TIME_UNITS = new Error(
|
||||
'could not parse block duration value - no time units detected'
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses block duration value and output a number of milliseconds.
|
||||
* @param input The block duration input from the API, e.g. 4m5.001s
|
||||
* @returns A number of milliseconds
|
||||
*/
|
||||
export const parseDuration = (input: string) => {
|
||||
// h -> 60*60*1000
|
||||
// m -> 60*1000
|
||||
// s -> 1000
|
||||
// ms -> 1
|
||||
// µs -> 1/1000
|
||||
// ns -> 1/1000000
|
||||
let H = 0;
|
||||
let M = 0;
|
||||
let S = 0;
|
||||
const lessThanSecond = /^[0-9.]+[nµm]*s$/gu.test(input);
|
||||
const exp = /(?<hours>[0-9.]+h)?(?<minutes>[0-9.]+m)?(?<seconds>[0-9.]+s)?/gu;
|
||||
const m = exp.exec(input);
|
||||
|
||||
const hours = m?.groups?.['hours'];
|
||||
const minutes = m?.groups?.['minutes'];
|
||||
const seconds = lessThanSecond ? input : m?.groups?.['seconds'];
|
||||
if (!lessThanSecond && !hours && !minutes && !seconds) {
|
||||
throw ERR_NO_TIME_UNITS;
|
||||
}
|
||||
|
||||
if (seconds) {
|
||||
S = parseFloat(seconds);
|
||||
if (seconds.includes('ns')) S /= 1000 * 1000;
|
||||
else if (seconds.includes('µs')) S /= 1000;
|
||||
else if (seconds.includes('ms')) S *= 1;
|
||||
else if (seconds.includes('s')) S *= 1000;
|
||||
}
|
||||
if (minutes && !lessThanSecond) {
|
||||
M = parseFloat(minutes) * 60 * 1000;
|
||||
}
|
||||
if (hours && !lessThanSecond) {
|
||||
H = parseFloat(hours) * 60 * 60 * 1000;
|
||||
}
|
||||
return H + M + S;
|
||||
};
|
||||
|
||||
const useAverageBlockDuration = (polls = DEFAULT_POLLS) => {
|
||||
const [avg, setAvg] = useState<number | undefined>(undefined);
|
||||
const { data, startPolling, stopPolling, error } = useBlockStatisticsQuery({
|
||||
@@ -28,7 +74,11 @@ const useAverageBlockDuration = (polls = DEFAULT_POLLS) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (durations.length < polls && data) {
|
||||
durations.push(parseFloat(data.statistics.blockDuration));
|
||||
try {
|
||||
durations.push(parseDuration(data.statistics.blockDuration)); // ms
|
||||
} catch (err) {
|
||||
// NOOP - do not add unparsed value to AVG
|
||||
}
|
||||
}
|
||||
if (durations.length === polls) {
|
||||
const averageBlockDuration = sum(durations) / durations.length; // ms
|
||||
|
||||
@@ -177,7 +177,21 @@ module.exports = {
|
||||
success: '#00F780',
|
||||
},
|
||||
fontFamily: {
|
||||
mono: ['Roboto Mono', 'monospace'],
|
||||
mono: [
|
||||
'ui-monospace',
|
||||
'Menlo',
|
||||
'Monaco',
|
||||
'Cascadia Mono',
|
||||
'Segoe UI Mono',
|
||||
'Roboto Mono',
|
||||
'Oxygen Mono',
|
||||
'Ubuntu Monospace',
|
||||
'Source Code Pro',
|
||||
'Fira Mono',
|
||||
'Droid Sans Mono',
|
||||
'Courier New',
|
||||
'monospace',
|
||||
],
|
||||
sans: [
|
||||
'"Helvetica Neue", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user