Compare commits

...
Author SHA1 Message Date
Matthew Russell 52481ccaf5 chore: fix bad grammar in comment 2023-04-10 10:46:14 -07:00
Matthew Russell 483345942d chore: actually use poll interval in test 2023-04-10 10:45:45 -07:00
Matthew Russell d71871566b chore: prevent polling if node switcher query fails 2023-04-10 09:49:58 -07:00
dexturr 10afa3c1d4 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-10 12:10:31 +00:00
Maciek ecd362615e chore(deposits): impove UX on deposit dialog (#3401) 2023-04-10 12:59:02 +01:00
dexturr 0d9bd67465 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-10 06:07:18 +00:00
dexturr b5aa92f007 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-10 00:14:34 +00:00
dexturr 2955790be1 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-09 18:09:21 +00:00
dexturr 89ff9309e9 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-09 12:08:56 +00:00
dexturr e1df3489f6 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-09 06:09:36 +00:00
dexturr f0fa9ca5e1 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-09 00:17:04 +00:00
dexturr 9d92477c2a chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-08 18:08:30 +00:00
dexturr b1a198be85 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-08 12:09:49 +00:00
dexturr 5fb385d43c chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-08 06:07:03 +00:00
dexturr 8014b279fe chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-08 00:14:19 +00:00
dexturr f4bde57294 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-07 18:09:32 +00:00
John Walley 2e5ff07824 chore(candles-chart): upgrade pennant to v1.8.1 (#3399) 2023-04-07 10:35:10 -07:00
Bartłomiej Głownia 7accb42793 fix(market-depth): make market data up to date, use fixed number of decimals in cumulative vol (#3361) 2023-04-07 10:34:51 -07:00
Matthew Russell 9229e7f686 chore(trading): update approval amount to be max uint256 (#3395) 2023-04-07 10:33:53 -07:00
daro-maj b570b9c4c1 test(trading): tests for post and reduce functionality (#3400) 2023-04-07 13:03:15 -04:00
m.ray ed78261aad fix(trading): order tx toasts title and intent should be set by status (#3368) 2023-04-07 13:01:12 -04:00
m.ray 363ca9c6e1 fix(trading): add liquidity provision docs (#3403) 2023-04-07 13:00:57 -04:00
m.ray bafd3c384c feat(trading): include fees in transfer amount (#3345) 2023-04-07 13:00:11 -04:00
34 changed files with 1681 additions and 1408 deletions
File diff suppressed because it is too large Load Diff
@@ -87,7 +87,10 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
.clear()
.type('850')
.next(`[data-testid="${formFieldError}"]`)
.should('have.text', 'Insufficient amount in Ethereum wallet');
.should(
'have.text',
"You can't deposit more than you have in your Ethereum wallet, 800 tEURO"
);
});
});
@@ -1,5 +1,5 @@
import * as Schema from '@vegaprotocol/types';
import { aliasGQLQuery, mockConnectWallet } from '@vegaprotocol/cypress';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { testOrderSubmission } from '../support/order-validation';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import {
@@ -13,8 +13,6 @@ const orderSizeField = 'order-size';
const orderPriceField = 'order-price';
const orderTIFDropDown = 'order-tif';
const placeOrderBtn = 'place-order';
const toggleShort = 'order-side-SIDE_SELL';
const toggleLong = 'order-side-SIDE_BUY';
const toggleLimit = 'order-type-TYPE_LIMIT';
const toggleMarket = 'order-type-TYPE_MARKET';
@@ -32,34 +30,6 @@ const displayTomorrow = () => {
return tomorrow.toISOString().substring(0, 16);
};
describe('time in force default values', () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must have market order set up to IOC by default', function () {
// 7002-SORD-031
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'IOC')[0].text
);
});
it('must have time in force set to GTC for limit order', function () {
// 7002-SORD-031
cy.getByTestId(toggleLimit).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'GTC')[0].text
);
});
});
describe('must submit order', { tags: '@smoke' }, () => {
// 7002-SORD-039
before(() => {
@@ -393,227 +363,6 @@ describe(
}
);
describe('deal ticket validation', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must show place order button and connect wallet if wallet is not connected', () => {
// 0003-WTXN-001
cy.getByTestId('connect-vega-wallet'); // Not connected
cy.getByTestId('order-connect-wallet').should('exist');
cy.getByTestId(placeOrderBtn).should('exist');
cy.getByTestId('deal-ticket-connect-wallet').should('exist');
});
it('must be able to select order direction - long/short', function () {
// 7002-SORD-004
cy.getByTestId(toggleShort).click().children('input').should('be.checked');
cy.getByTestId(toggleLong).click().children('input').should('be.checked');
});
it('must be able to select order type - limit/market', function () {
// 7002-SORD-005
// 7002-SORD-006
// 7002-SORD-007
cy.getByTestId(toggleLimit).click().children('input').should('be.checked');
cy.getByTestId(toggleMarket).click().children('input').should('be.checked');
});
it('order connect vega wallet button should connect', () => {
mockConnectWallet();
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('101');
cy.getByTestId('order-connect-wallet').click();
cy.getByTestId('dialog-content').should('be.visible');
cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]')
.click();
cy.wait('@walletReq');
cy.getByTestId(placeOrderBtn).should('be.visible');
cy.getByTestId(toggleLimit).children('input').should('be.checked');
cy.getByTestId(orderPriceField).should('have.value', '101');
});
});
describe('deal ticket size validation', { tags: '@smoke' }, function () {
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must warn if order size input has too many digits after the decimal place', function () {
// 7002-SORD-016
cy.getByTestId('order-type-TYPE_MARKET').click();
cy.getByTestId(orderSizeField).clear().type('1.234');
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Size must be whole numbers for this market'
);
});
it('must warn if order size is set to 0', function () {
cy.getByTestId('order-type-TYPE_MARKET').click();
cy.getByTestId(orderSizeField).clear().type('0');
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Size cannot be lower than 1'
);
});
});
describe('limit order validations', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId(toggleLimit).click();
});
beforeEach(() => {
cy.setVegaWallet();
});
it('must see the price unit', function () {
// 7002-SORD-018
cy.getByTestId(orderPriceField)
.siblings('label')
.should('have.text', 'Price (DAI)');
});
it('must see warning when placing an order with expiry date in past', () => {
const expiresAt = new Date(Date.now() - 24 * 60 * 60 * 1000);
const expiresAtInputValue = expiresAt.toISOString().substring(0, 16);
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('0.1');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
cy.log('choosing yesterday');
cy.getByTestId('date-picker-field').type(expiresAtInputValue);
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('dealticket-error-message-expiry').should(
'have.text',
'The expiry date that you have entered appears to be in the past'
);
});
it('must see warning if price has too many digits after decimal place', function () {
// 7002-SORD-059
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderPriceField).clear().type('1.123456');
cy.getByTestId('dealticket-error-message-price-limit').should(
'have.text',
'Price accepts up to 5 decimal places'
);
});
describe('time in force validations', function () {
const validTIF = TIFlist;
validTIF.forEach((tif) => {
// 7002-SORD-023
// 7002-SORD-024
// 7002-SORD-025
// 7002-SORD-026
// 7002-SORD-027
// 7002-SORD-028
it(`must be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
});
});
it('selections should be remembered', () => {
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'IOC')[0].text
);
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_FOK');
cy.getByTestId(toggleLimit).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'GTT')[0].text
);
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'FOK')[0].text
);
});
});
});
describe('market order validations', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId(toggleMarket).click();
});
beforeEach(() => {
cy.setVegaWallet();
});
it('must not see the price unit', function () {
// 7002-SORD-019
cy.getByTestId(orderPriceField).should('not.exist');
});
describe('time in force validations', function () {
const validTIF = TIFlist.filter((tif) => ['FOK', 'IOC'].includes(tif.code));
const invalidTIF = TIFlist.filter(
(tif) => !['FOK', 'IOC'].includes(tif.code)
);
validTIF.forEach((tif) => {
// 7002-SORD-025
// 7002-SORD-026
it(`must be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
});
});
invalidTIF.forEach((tif) => {
// 7002-SORD-023
// 7002-SORD-024
// 7002-SORD-027
// 7002-SORD-028
it(`must not be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).should('not.contain', tif.text);
});
});
});
});
describe('suspended market validation', { tags: '@regression' }, () => {
before(() => {
cy.setVegaWallet();
@@ -0,0 +1,329 @@
import * as Schema from '@vegaprotocol/types';
import { mockConnectWallet } from '@vegaprotocol/cypress';
const orderSizeField = 'order-size';
const orderPriceField = 'order-price';
const orderTIFDropDown = 'order-tif';
const placeOrderBtn = 'place-order';
const toggleShort = 'order-side-SIDE_SELL';
const toggleLong = 'order-side-SIDE_BUY';
const toggleLimit = 'order-type-TYPE_LIMIT';
const toggleMarket = 'order-type-TYPE_MARKET';
const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
return {
code: Schema.OrderTimeInForceCode[value],
value,
text: Schema.OrderTimeInForceMapping[value],
};
});
describe('time in force default values', () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must have market order set up to IOC by default', function () {
// 7002-SORD-031
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'IOC')[0].text
);
});
it('must have time in force set to GTC for limit order', function () {
// 7002-SORD-031
cy.getByTestId(toggleLimit).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'GTC')[0].text
);
});
});
describe('deal ticket validation', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must show place order button and connect wallet if wallet is not connected', () => {
// 0003-WTXN-001
cy.getByTestId('connect-vega-wallet'); // Not connected
cy.getByTestId('order-connect-wallet').should('exist');
cy.getByTestId(placeOrderBtn).should('exist');
cy.getByTestId('deal-ticket-connect-wallet').should('exist');
});
it('must be able to select order direction - long/short', function () {
// 7002-SORD-004
cy.getByTestId(toggleShort).click().children('input').should('be.checked');
cy.getByTestId(toggleLong).click().children('input').should('be.checked');
});
it('must be able to select order type - limit/market', function () {
// 7002-SORD-005
// 7002-SORD-006
// 7002-SORD-007
cy.getByTestId(toggleLimit).click().children('input').should('be.checked');
cy.getByTestId(toggleMarket).click().children('input').should('be.checked');
});
it('order connect vega wallet button should connect', () => {
mockConnectWallet();
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('101');
cy.getByTestId('order-connect-wallet').click();
cy.getByTestId('dialog-content').should('be.visible');
cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]')
.click();
cy.wait('@walletReq');
cy.getByTestId(placeOrderBtn).should('be.visible');
cy.getByTestId(toggleLimit).children('input').should('be.checked');
cy.getByTestId(orderPriceField).should('have.value', '101');
});
});
describe('deal ticket size validation', { tags: '@smoke' }, function () {
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must warn if order size input has too many digits after the decimal place', function () {
// 7002-SORD-016
cy.getByTestId('order-type-TYPE_MARKET').click();
cy.getByTestId(orderSizeField).clear().type('1.234');
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Size must be whole numbers for this market'
);
});
it('must warn if order size is set to 0', function () {
cy.getByTestId('order-type-TYPE_MARKET').click();
cy.getByTestId(orderSizeField).clear().type('0');
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Size cannot be lower than 1'
);
});
});
describe('limit order validations', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId(toggleLimit).click();
});
beforeEach(() => {
cy.setVegaWallet();
});
it('must see the price unit', function () {
// 7002-SORD-018
cy.getByTestId(orderPriceField)
.siblings('label')
.should('have.text', 'Price (DAI)');
});
it('must see warning when placing an order with expiry date in past', () => {
const expiresAt = new Date(Date.now() - 24 * 60 * 60 * 1000);
const expiresAtInputValue = expiresAt.toISOString().substring(0, 16);
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('0.1');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
cy.log('choosing yesterday');
cy.getByTestId('date-picker-field').type(expiresAtInputValue);
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('dealticket-error-message-expiry').should(
'have.text',
'The expiry date that you have entered appears to be in the past'
);
});
it('must see warning if price has too many digits after decimal place', function () {
// 7002-SORD-059
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderPriceField).clear().type('1.123456');
cy.getByTestId('dealticket-error-message-price-limit').should(
'have.text',
'Price accepts up to 5 decimal places'
);
});
describe('time in force validations', function () {
const validTIF = TIFlist;
validTIF.forEach((tif) => {
// 7002-SORD-023
// 7002-SORD-024
// 7002-SORD-025
// 7002-SORD-026
// 7002-SORD-027
// 7002-SORD-028
it(`must be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
});
});
it('selections should be remembered', () => {
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'IOC')[0].text
);
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_FOK');
cy.getByTestId(toggleLimit).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'GTT')[0].text
);
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'FOK')[0].text
);
});
});
});
describe('market order validations', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId(toggleMarket).click();
});
beforeEach(() => {
cy.setVegaWallet();
});
it('must not see the price unit', function () {
// 7002-SORD-019
cy.getByTestId(orderPriceField).should('not.exist');
});
describe('time in force validations', function () {
const validTIF = TIFlist.filter((tif) => ['FOK', 'IOC'].includes(tif.code));
const invalidTIF = TIFlist.filter(
(tif) => !['FOK', 'IOC'].includes(tif.code)
);
validTIF.forEach((tif) => {
// 7002-SORD-025
// 7002-SORD-026
it(`must be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
});
});
invalidTIF.forEach((tif) => {
// 7002-SORD-023
// 7002-SORD-024
// 7002-SORD-027
// 7002-SORD-028
it(`must not be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).should('not.contain', tif.text);
});
});
});
});
describe('post and reduce order validations', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId(toggleMarket).click();
});
beforeEach(() => {
cy.setVegaWallet();
});
const validTIF = TIFlist.filter((tif) => ['FOK', 'IOC'].includes(tif.code));
validTIF.forEach((tif) => {
// 7002-SORD-025
// 7002-SORD-026
it(`post and reduce order market for ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId('post-only').should('be.disabled');
cy.getByTestId('reduce-only').should('be.enabled');
});
});
validTIF.forEach((tif) => {
it(`post and reduce order limit for ${tif.code}`, function () {
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId('post-only').should('be.disabled');
cy.getByTestId('reduce-only').should('be.enabled');
});
});
const validTIFLimit = TIFlist.filter((tif) =>
['GFA', 'GFN', 'GTC', 'GTT'].includes(tif.code)
);
validTIFLimit.forEach((tif) => {
it(`post and reduce order for limit ${tif.code}`, function () {
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId('post-only').should('be.enabled');
cy.getByTestId('reduce-only').should('be.disabled');
});
});
});
@@ -8,6 +8,7 @@ import {
import { tooltipMapping } from '@vegaprotocol/market-info';
import {
addDecimalsFormatNumber,
createDocsLinks,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
@@ -23,6 +24,7 @@ import {
Tabs,
Link as UiToolkitLink,
Indicator,
ExternalLink,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
@@ -37,6 +39,7 @@ import { Link, useParams } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import { useMarket, useStaticMarketData } from '@vegaprotocol/market-list';
import { useEnvironment } from '@vegaprotocol/environment';
const enum LiquidityTabs {
Active = 'active',
@@ -144,6 +147,7 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
market?.tradableInstrument.instrument.product.settlementAsset.decimals || 0;
const symbol =
market?.tradableInstrument.instrument.product.settlementAsset.symbol;
const { VEGA_DOCS_URL } = useEnvironment();
const { params } = useNetworkParams([
NetworkParams.market_liquidity_stakeToCcyVolume,
@@ -211,6 +215,13 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
<HeaderStat heading={t('Market ID')}>
<div className="break-word">{marketId}</div>
</HeaderStat>
<HeaderStat heading={t('Learn more')}>
{VEGA_DOCS_URL && (
<ExternalLink href={createDocsLinks(VEGA_DOCS_URL).LIQUIDITY}>
{t('Providing liquidity')}
</ExternalLink>
)}
</HeaderStat>
</Header>
);
});
@@ -301,7 +301,7 @@ export const AccountHistoryChart = ({
asset: AssetFieldsFragment;
}) => {
const { theme } = useThemeSwitcher();
const values: { cols: string[]; rows: [Date, ...number[]][] } | null =
const values: { cols: [string, string]; rows: [Date, number][] } | null =
useMemo(() => {
if (!data?.balanceChanges.edges.length) {
return null;
@@ -8,6 +8,7 @@ import type { MarketData } from '@vegaprotocol/market-list';
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
import { HeaderStat } from '../header';
import {
ExternalLink,
Indicator,
KeyValueTable,
KeyValueTableRow,
@@ -18,9 +19,11 @@ import { useCheckLiquidityStatus } from '@vegaprotocol/liquidity';
import { AuctionTrigger, MarketTradingMode } from '@vegaprotocol/types';
import {
addDecimalsFormatNumber,
createDocsLinks,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useEnvironment } from '@vegaprotocol/environment';
interface Props {
marketId?: string;
@@ -44,6 +47,8 @@ export const MarketLiquiditySupplied = ({
params.market_liquidity_targetstake_triggering_ratio
);
const { VEGA_DOCS_URL } = useEnvironment();
const variables = useMemo(
() => ({
marketId: marketId || '',
@@ -126,6 +131,14 @@ export const MarketLiquiditySupplied = ({
<Link href={`/#/liquidity/${marketId}`} data-testid="view-liquidity-link">
{t('View liquidity provision table')}
</Link>
{VEGA_DOCS_URL && (
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).LIQUIDITY}
className="mt-2"
>
{t('Learn about providing liquidity')}
</ExternalLink>
)}
{showMessage && (
<p className="mt-4">
{t(
@@ -39,10 +39,14 @@ import { t } from '@vegaprotocol/i18n';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment';
import { getRejectionReason, useOrderByIdQuery } from '@vegaprotocol/orders';
import {
getOrderToastIntent,
getOrderToastTitle,
getRejectionReason,
useOrderByIdQuery,
} from '@vegaprotocol/orders';
import { useMarketList } from '@vegaprotocol/market-list';
import type { Side } from '@vegaprotocol/types';
import { OrderStatus } from '@vegaprotocol/types';
import { OrderStatusMapping } from '@vegaprotocol/types';
import { Size } from '@vegaprotocol/react-helpers';
@@ -478,12 +482,10 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
getRejectionReason(tx.order) || tx.order.rejectionReason || '';
return (
<>
<ToastHeading>{t('Order rejected')}</ToastHeading>
{rejectionReason || tx.order.rejectionReason ? (
<ToastHeading>{getOrderToastTitle(tx.order.status)}</ToastHeading>
{rejectionReason ? (
<p>
{t('Your order has been rejected because: %s', [
rejectionReason || tx.order.rejectionReason,
])}
{t('Your order has been rejected because: %s', [rejectionReason])}
</p>
) : (
<p>{t('Your order has been rejected.')}</p>
@@ -506,7 +508,7 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
if (isOrderSubmissionTransaction(tx.body) && tx.order?.rejectionReason) {
return (
<div>
<h3 className="font-bold">{t('Order rejected')}</h3>
<h3 className="font-bold">{getOrderToastTitle(tx.order.status)}</h3>
<p>{t('Your order was rejected.')}</p>
{tx.txHash && (
<p className="break-all">
@@ -580,7 +582,7 @@ const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
tx.error instanceof WalletError &&
walletNoConnectionCodes.includes(tx.error.code);
if (orderRejection) {
label = t('Order rejected');
label = getOrderToastTitle(tx.order?.status) || t('Order rejected');
errorMessage = t('Your order has been rejected because: %s', [
orderRejection || tx.order?.rejectionReason || ' ',
]);
@@ -649,13 +651,8 @@ export const useVegaTransactionToasts = () => {
// Transaction can be successful but the order can be rejected by the network
const intent =
(tx.order &&
[OrderStatus.STATUS_REJECTED, OrderStatus.STATUS_STOPPED].includes(
tx.order.status
)) ||
tx.order?.rejectionReason
? Intent.Danger
: intentMap[tx.status];
(tx.order && getOrderToastIntent(tx.order.status)) ||
intentMap[tx.status];
return {
id: `vega-${tx.id}`,
+15
View File
@@ -34,6 +34,18 @@ html [data-theme='dark'] {
/* sell candles only use stroke as the candle is solid (without border) */
--pennant-color-sell-stroke: theme('colors.vega.pink.500');
/* studies */
--pennant-color-eldar-ray-bear-power: theme('colors.vega.pink.500');
--pennant-color-eldar-ray-bull-power: theme('colors.vega.green.650');
--pennant-color-macd-divergence-buy: theme('colors.vega.green.650');
--pennant-color-macd-divergence-sell: theme('colors.vega.pink.500');
--pennant-color-macd-signal: theme('colors.vega.blue.500');
--pennant-color-macd-macd: theme('colors.vega.yellow.500');
--pennant-color-volume-buy: theme('colors.vega.green.650');
--pennant-color-volume-sell: theme('colors.vega.pink.500');
/* depth chart */
--pennant-color-depth-buy-fill: theme('colors.vega.green.650');
--pennant-color-depth-buy-stroke: theme('colors.vega.green.500');
@@ -50,6 +62,9 @@ html [data-theme='light'] {
/* sell candles only use stroke as the candle is solid (without border) */
--pennant-color-sell-stroke: theme('colors.vega.pink.400');
--pennant-color-volume-buy: theme('colors.vega.green.400');
--pennant-color-volume-sell: theme('colors.vega.pink.500');
/* depth chart */
--pennant-color-depth-buy-fill: theme('colors.vega.green.400');
--pennant-color-depth-buy-stroke: theme('colors.vega.green.550');
+221 -72
View File
@@ -1,4 +1,10 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import BigNumber from 'bignumber.js';
import { AddressField, TransferFee, TransferForm } from './transfer-form';
import { AccountType } from '@vegaprotocol/types';
@@ -27,6 +33,34 @@ describe('TransferForm', () => {
submitTransfer: jest.fn(),
};
it('validates a manually entered address', async () => {
render(<TransferForm {...props} />);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
const toggle = screen.getByText('Enter manually');
fireEvent.click(toggle);
// has switched to input
expect(toggle).toHaveTextContent('Select from wallet');
expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: 'invalid-address' },
});
await waitFor(() => {
const errors = screen.getAllByTestId('input-error-text');
expect(errors[0]).toHaveTextContent('Invalid Vega key');
});
// same pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: pubKey },
});
await waitFor(() => {
const errors = screen.getAllByTestId('input-error-text');
expect(errors[0]).toHaveTextContent('Vega key is the same');
});
});
it('validates fields and submits', async () => {
render(<TransferForm {...props} />);
@@ -62,15 +96,17 @@ describe('TransferForm', () => {
formatNumber(asset.balance, asset.decimals)
);
const amountInput = screen.getByLabelText('Amount');
// Test amount validation
fireEvent.change(screen.getByLabelText('Amount'), {
fireEvent.change(amountInput, {
target: { value: '0.00000001' },
});
expect(
await screen.findByText('Value is below minimum')
).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('Amount'), {
fireEvent.change(amountInput, {
target: { value: '9999999' },
});
expect(
@@ -78,7 +114,7 @@ describe('TransferForm', () => {
).toBeInTheDocument();
// set valid amount
fireEvent.change(screen.getByLabelText('Amount'), {
fireEvent.change(amountInput, {
target: { value: amount },
});
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
@@ -100,78 +136,191 @@ describe('TransferForm', () => {
});
});
it('validates a manually entered address', async () => {
render(<TransferForm {...props} />);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
const toggle = screen.getByText('Enter manually');
fireEvent.click(toggle);
// has switched to input
expect(toggle).toHaveTextContent('Select from wallet');
expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: 'invalid-address' },
});
await waitFor(() => {
const errors = screen.getAllByTestId('input-error-text');
expect(errors[0]).toHaveTextContent('Invalid Vega key');
describe('IncludeFeesCheckbox', () => {
it('validates fields and submits when checkbox is checked', async () => {
render(<TransferForm {...props} />);
// check current pubkey not shown
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
expect(keySelect.children).toHaveLength(2);
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
'',
props.pubKeys[1],
]);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
// Select a pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: props.pubKeys[1] },
});
// Select asset
fireEvent.change(
// Bypass RichSelect and target hidden native select
// eslint-disable-next-line
document.querySelector('select[name="asset"]')!,
{ target: { value: asset.id } }
);
// assert rich select as updated
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
asset.name
);
expect(screen.getByTestId('asset-balance')).toHaveTextContent(
formatNumber(asset.balance, asset.decimals)
);
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
expect(checkbox).not.toBeChecked();
act(() => {
/* fire events that update state */
// set valid amount
fireEvent.change(amountInput, {
target: { value: amount },
});
// check include fees checkbox
fireEvent.click(checkbox);
});
expect(checkbox).toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
.toFixed();
const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
expectedAmount
);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
amount
);
submit();
await waitFor(() => {
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
expect(props.submitTransfer).toHaveBeenCalledWith({
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: props.pubKeys[1],
asset: asset.id,
amount: removeDecimal(amount, asset.decimals),
oneOff: {},
});
});
});
// same pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: pubKey },
it('validates fields when checkbox is not checked', async () => {
render(<TransferForm {...props} />);
// check current pubkey not shown
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
expect(keySelect.children).toHaveLength(2);
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
'',
props.pubKeys[1],
]);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
// Select a pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: props.pubKeys[1] },
});
// Select asset
fireEvent.change(
// Bypass RichSelect and target hidden native select
// eslint-disable-next-line
document.querySelector('select[name="asset"]')!,
{ target: { value: asset.id } }
);
// assert rich select as updated
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
asset.name
);
expect(screen.getByTestId('asset-balance')).toHaveTextContent(
formatNumber(asset.balance, asset.decimals)
);
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
expect(checkbox).not.toBeChecked();
act(() => {
/* fire events that update state */
// set valid amount
fireEvent.change(amountInput, {
target: { value: amount },
});
});
expect(checkbox).not.toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
.toFixed();
const total = new BigNumber(amount).plus(expectedFee).toFixed();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
});
});
describe('AddressField', () => {
const props = {
pubKeys: ['pubkey-1', 'pubkey-2'],
select: <div>select</div>,
input: <div>input</div>,
onChange: jest.fn(),
};
it('toggles content and calls onChange', async () => {
const mockOnChange = jest.fn();
render(<AddressField {...props} onChange={mockOnChange} />);
// select should be shown as multiple pubkeys provided
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
fireEvent.click(screen.getByText('Enter manually'));
expect(screen.queryByText('select')).not.toBeInTheDocument();
expect(screen.getByText('input')).toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByText('Select from wallet'));
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledTimes(2);
});
await waitFor(() => {
const errors = screen.getAllByTestId('input-error-text');
expect(errors[0]).toHaveTextContent('Vega key is the same');
it('Does not provide select option if there is only a single key', () => {
render(<AddressField {...props} pubKeys={['single-pubKey']} />);
expect(screen.getByText('input')).toBeInTheDocument();
expect(screen.queryByText('Select from wallet')).not.toBeInTheDocument();
});
});
describe('TransferFee', () => {
const props = {
amount: '200',
feeFactor: '0.001',
fee: '0.2',
transferAmount: '200',
decimals: 8,
};
it('calculates and renders the transfer fee', () => {
render(<TransferFee {...props} />);
const expected = new BigNumber(props.amount)
.times(props.feeFactor)
.toFixed();
const total = new BigNumber(props.amount).plus(expected).toFixed();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
props.amount
);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
});
});
});
describe('AddressField', () => {
const props = {
pubKeys: ['pubkey-1', 'pubkey-2'],
select: <div>select</div>,
input: <div>input</div>,
onChange: jest.fn(),
};
it('toggles content and calls onChange', async () => {
const mockOnChange = jest.fn();
render(<AddressField {...props} onChange={mockOnChange} />);
// select should be shown as multiple pubkeys provided
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
fireEvent.click(screen.getByText('Enter manually'));
expect(screen.queryByText('select')).not.toBeInTheDocument();
expect(screen.getByText('input')).toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByText('Select from wallet'));
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledTimes(2);
});
it('Does not provide select option if there is only a single key', () => {
render(<AddressField {...props} pubKeys={['single-pubKey']} />);
expect(screen.getByText('input')).toBeInTheDocument();
expect(screen.queryByText('Select from wallet')).not.toBeInTheDocument();
});
});
describe('TransferFee', () => {
const props = {
amount: '200',
feeFactor: '0.001',
};
it('calculates and renders the transfer fee', () => {
render(<TransferFee {...props} />);
const expected = new BigNumber(props.amount)
.times(props.feeFactor)
.toFixed();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
});
});
+102 -16
View File
@@ -16,6 +16,7 @@ import {
RichSelect,
Select,
Tooltip,
Checkbox,
} from '@vegaprotocol/ui-toolkit';
import type { Transfer } from '@vegaprotocol/wallet';
import { normalizeTransfer } from '@vegaprotocol/wallet';
@@ -63,6 +64,26 @@ export const TransferForm = ({
const amount = watch('amount');
const assetId = watch('asset');
const [includeFee, setIncludeFee] = useState(false);
const transferAmount = useMemo(() => {
if (!amount) return undefined;
if (includeFee && feeFactor) {
return new BigNumber(1).minus(feeFactor).times(amount).toString();
}
return amount;
}, [amount, includeFee, feeFactor]);
const fee = useMemo(() => {
if (!transferAmount) return undefined;
if (includeFee) {
return new BigNumber(amount).minus(transferAmount).toString();
}
return (
feeFactor && new BigNumber(feeFactor).times(transferAmount).toString()
);
}, [amount, includeFee, transferAmount, feeFactor]);
const asset = useMemo(() => {
return assets.find((a) => a.id === assetId);
}, [assets, assetId]);
@@ -72,13 +93,16 @@ export const TransferForm = ({
if (!asset) {
throw new Error('Submitted transfer with no asset selected');
}
const transfer = normalizeTransfer(fields.toAddress, fields.amount, {
if (!transferAmount) {
throw new Error('Submitted transfer with no amount selected');
}
const transfer = normalizeTransfer(fields.toAddress, transferAmount, {
id: asset.id,
decimals: asset.decimals,
});
submitTransfer(transfer);
},
[asset, submitTransfer]
[asset, submitTransfer, transferAmount]
);
const min = useMemo(() => {
@@ -213,7 +237,32 @@ export const TransferForm = ({
<InputError forInput="amount">{errors.amount.message}</InputError>
)}
</FormGroup>
<TransferFee amount={amount} feeFactor={feeFactor} />
<div className="mb-4">
<Checkbox
name="include-transfer-fee"
disabled={!transferAmount}
label={
<Tooltip
description={t(
`The fee will be taken from the amount you are transferring.`
)}
>
<div>{t('Include transfer fee')}</div>
</Tooltip>
}
checked={includeFee}
onCheckedChange={() => setIncludeFee(!includeFee)}
/>
</div>
{transferAmount && fee && (
<TransferFee
amount={transferAmount}
transferAmount={transferAmount}
feeFactor={feeFactor}
fee={fee}
decimals={asset?.decimals}
/>
)}
<Button type="submit" variant="primary" fill={true}>
{t('Confirm transfer')}
</Button>
@@ -223,34 +272,71 @@ export const TransferForm = ({
export const TransferFee = ({
amount,
transferAmount,
feeFactor,
fee,
decimals,
}: {
amount: string;
transferAmount: string;
feeFactor: string | null;
fee?: string;
decimals?: number;
}) => {
if (!feeFactor || !amount) return null;
if (!feeFactor || !amount || !transferAmount || !fee) return null;
// using toFixed without an argument will always return a
// number in normal notation without rounding, formatting functions
// arent working in a way which won't round the decimal places
const value = new BigNumber(amount).times(feeFactor).toFixed();
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
return (
<div className="mb-4 flex justify-between items-center gap-4 flex-wrap">
<div>
<div className="mb-4 flex flex-col gap-2 text-xs">
<div className="flex justify-between gap-1 items-center flex-wrap">
<Tooltip
description={t(
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to ${feeFactor}`
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to %s`,
[feeFactor]
)}
>
<div>{t('Transfer fee')}</div>
</Tooltip>
<div
data-testid="transfer-fee"
className="text-neutral-500 dark:text-neutral-300"
>
{formatNumber(fee, decimals)}
</div>
</div>
<div
data-testid="transfer-fee"
className="text-neutral-500 dark:text-neutral-300"
>
{value}
<div className="flex justify-between gap-1 items-center flex-wrap">
<Tooltip
description={t(
`The total amount to be transferred (without the fee)`
)}
>
<div>{t('Amount to be transferred')}</div>
</Tooltip>
<div
data-testid="transfer-amount"
className="text-neutral-500 dark:text-neutral-300"
>
{formatNumber(amount, decimals)}
</div>
</div>
<div className="flex justify-between gap-1 items-center flex-wrap">
<Tooltip
description={t(
`The total amount taken from your account. The amount to be transferred plus the fee.`
)}
>
<div>{t('Total amount (with fee)')}</div>
</Tooltip>
<div
data-testid="total-transfer-fee"
className="text-neutral-500 dark:text-neutral-300"
>
{formatNumber(totalValue, decimals)}
</div>
</div>
</div>
);
+5
View File
@@ -1 +1,6 @@
import '@testing-library/jest-dom';
import ResizeObserver from 'resize-observer-polyfill';
import { defaultFallbackInView } from 'react-intersection-observer';
defaultFallbackInView(true);
global.ResizeObserver = ResizeObserver;
+2 -2
View File
@@ -1,6 +1,6 @@
import 'pennant/dist/style.css';
import {
Chart,
CandlestickChart,
ChartType,
Interval,
Overlay,
@@ -234,7 +234,7 @@ export const CandlesChartContainer = ({
</DropdownMenu>
</div>
<div className="flex-1">
<Chart
<CandlestickChart
dataSource={dataSource}
options={{
chartType: chartType,
@@ -1,6 +1,6 @@
import { memo } from 'react';
import { BID_COLOR, ASK_COLOR } from './vol-cell';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { addDecimalsFixedFormatNumber } from '@vegaprotocol/utils';
import { NumericCell } from './numeric-cell';
export interface CumulativeVolProps {
@@ -55,7 +55,7 @@ export const CumulativeVol = memo(
(
<NumericCell
value={Number(indicativeVolume)}
valueFormatted={addDecimalsFormatNumber(
valueFormatted={addDecimalsFixedFormatNumber(
indicativeVolume,
positionDecimalPlaces ?? 0
)}
@@ -67,7 +67,7 @@ export const CumulativeVol = memo(
{ask ? (
<NumericCell
value={ask}
valueFormatted={addDecimalsFormatNumber(
valueFormatted={addDecimalsFixedFormatNumber(
ask,
positionDecimalPlaces ?? 0
)}
@@ -77,7 +77,7 @@ export const CumulativeVol = memo(
{bid ? (
<NumericCell
value={ask}
valueFormatted={addDecimalsFormatNumber(
valueFormatted={addDecimalsFixedFormatNumber(
bid,
positionDecimalPlaces ?? 0
)}
+14 -15
View File
@@ -51,11 +51,12 @@ export const ApproveNotification = ({
intent={intent}
testId="approve-default"
message={t(
`Before you can make a deposit of your chosen asset, ${selectedAsset?.symbol}, you need to approve its use in your Ethereum wallet`
'Before you can make a deposit of your chosen asset, %s, you need to approve its use in your Ethereum wallet',
selectedAsset?.symbol
)}
buttonProps={{
size: 'sm',
text: `Approve ${selectedAsset?.symbol}`,
text: t('Approve %s', selectedAsset?.symbol),
action: onApprove,
dataTestId: 'approve-submit',
}}
@@ -68,13 +69,12 @@ export const ApproveNotification = ({
intent={intent}
testId="reapprove-default"
message={t(
`Approve again to deposit more than ${formatNumber(
balances.allowance.toString()
)}`
'Approve again to deposit more than %s',
formatNumber(balances.allowance.toString())
)}
buttonProps={{
size: 'sm',
text: `Approve ${selectedAsset?.symbol}`,
text: t('Approve %s', selectedAsset?.symbol),
action: onApprove,
dataTestId: 'reapprove-submit',
}}
@@ -157,7 +157,8 @@ const ApprovalTxFeedback = ({
intent={Intent.Warning}
testId="approve-requested"
message={t(
`Go to your Ethereum wallet and approve the transaction to enable the use of ${selectedAsset?.symbol}`
'Go to your Ethereum wallet and approve the transaction to enable the use of %s',
selectedAsset?.symbol
)}
/>
</div>
@@ -174,7 +175,8 @@ const ApprovalTxFeedback = ({
<>
<p>
{t(
`Your ${selectedAsset?.symbol} approval is being confirmed by the Ethereum network. When this is complete, you can continue your deposit`
'Your %s approval is being confirmed by the Ethereum network. When this is complete, you can continue your deposit',
selectedAsset?.symbol
)}{' '}
</p>
{txLink && <p>{txLink}</p>}
@@ -194,13 +196,10 @@ const ApprovalTxFeedback = ({
message={
<>
<p>
{t(
`You can now make deposits in ${
selectedAsset?.symbol
}, up to a maximum of ${formatNumber(
allowance?.toString() || 0
)}`
)}
{t('You approved deposits of up to %s %s.', [
selectedAsset?.symbol,
formatNumber(allowance?.toString() || 0),
])}
</p>
{txLink && <p>{txLink}</p>}
</>
+46 -7
View File
@@ -1,4 +1,11 @@
import { waitFor, fireEvent, render, screen } from '@testing-library/react';
import {
waitFor,
fireEvent,
render,
screen,
act,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import BigNumber from 'bignumber.js';
import type { DepositFormProps } from './deposit-form';
import { DepositForm } from './deposit-form';
@@ -141,12 +148,14 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Insufficient amount in Ethereum wallet')
await screen.findByText(
"You can't deposit more than you have in your Ethereum wallet, 5"
)
).toBeInTheDocument();
});
it('fails when submitted amount is more than the maximum limit', async () => {
render(<DepositForm {...props} />);
render(<DepositForm {...props} selectedAsset={asset} />);
const amountMoreThanLimit = '21';
fireEvent.change(screen.getByLabelText('Amount'), {
@@ -155,7 +164,9 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Amount is above lifetime deposit limit')
await screen.findByText(
"You can't deposit more than your remaining deposit allowance, 10 asset-symbol"
)
).toBeInTheDocument();
});
@@ -179,7 +190,9 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Amount is above approved amount')
await screen.findByText(
"You can't deposit more than your approved deposit amount, 30"
)
).toBeInTheDocument();
});
@@ -283,8 +296,6 @@ describe('Deposit form', () => {
expect(screen.getByTestId('BALANCE_AVAILABLE_value')).toHaveTextContent(
'50'
);
expect(screen.getByTestId('MAX_LIMIT_value')).toHaveTextContent('20');
expect(screen.getByTestId('DEPOSITED_value')).toHaveTextContent('10');
expect(screen.getByTestId('REMAINING_value')).toHaveTextContent('10');
fireEvent.change(screen.getByLabelText('Amount'), {
@@ -365,4 +376,32 @@ describe('Deposit form', () => {
/this app only works on/i
);
});
it('Remaining deposit allowance tooltip should be rendered', async () => {
render(<DepositForm {...props} selectedAsset={asset} />);
await act(async () => {
await userEvent.hover(screen.getByText('Remaining deposit allowance'));
});
await waitFor(async () => {
await expect(
screen.getByRole('tooltip', {
name: /VEGA has a lifetime deposit limit of 20 asset-symbol per address/,
})
).toBeInTheDocument();
});
});
it('Ethereum deposit cap tooltip should be rendered', async () => {
render(<DepositForm {...props} selectedAsset={asset} />);
await act(async () => {
await userEvent.hover(screen.getByText('Ethereum deposit cap'));
});
await waitFor(async () => {
await expect(
screen.getByRole('tooltip', {
name: /The deposit cap is set when you approve an asset for use with this app/,
})
).toBeInTheDocument();
});
});
});
+65 -50
View File
@@ -8,6 +8,7 @@ import {
maxSafe,
addDecimal,
isAssetTypeERC20,
formatNumber,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
@@ -133,11 +134,8 @@ export const DepositForm = ({
return _pubKeys ? _pubKeys.map((pk) => pk.publicKey) : [];
}, [_pubKeys]);
const approved = balances
? balances.allowance.isGreaterThan(0)
? true
: false
: false;
const approved =
balances && balances.allowance.isGreaterThan(0) ? true : false;
return (
<form
@@ -194,6 +192,44 @@ export const DepositForm = ({
<InputError intent="danger">{errors.from.message}</InputError>
)}
</FormGroup>
<FormGroup label={t('To (Vega key)')} labelFor="to">
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('to', '')}
select={
<Select {...register('to')} id="to" defaultValue="">
<option value="" disabled>
{t('Please select')}
</option>
{pubKeys?.length &&
pubKeys.map((pk) => (
<option key={pk} value={pk}>
{pk}
</option>
))}
</Select>
}
input={
<Input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to"
type="text"
{...register('to', {
validate: {
required,
vegaPublicKey,
},
})}
/>
}
/>
{errors.to?.message && (
<InputError intent="danger" forInput="to">
{errors.to.message}
</InputError>
)}
</FormGroup>
<FormGroup label={t('Asset')} labelFor="asset">
<Controller
control={control}
@@ -250,45 +286,7 @@ export const DepositForm = ({
selectedAsset={selectedAsset}
faucetTxId={faucetTxId}
/>
<FormGroup label={t('To (Vega key)')} labelFor="to">
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('to', '')}
select={
<Select {...register('to')} id="to" defaultValue="">
<option value="" disabled={true}>
{t('Please select')}
</option>
{pubKeys?.length &&
pubKeys.map((pk) => (
<option key={pk} value={pk}>
{pk}
</option>
))}
</Select>
}
input={
<Input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to"
type="text"
{...register('to', {
validate: {
required,
vegaPublicKey,
},
})}
/>
}
/>
{errors.to?.message && (
<InputError intent="danger" forInput="to">
{errors.to.message}
</InputError>
)}
</FormGroup>
{selectedAsset && balances && (
{approved && selectedAsset && balances && (
<div className="mb-6">
<DepositLimits {...balances} asset={selectedAsset} />
</div>
@@ -305,8 +303,15 @@ export const DepositForm = ({
minSafe: (value) => minSafe(new BigNumber(min))(value),
approved: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(balances?.allowance || 0)) {
return t('Amount is above approved amount');
const allowance = new BigNumber(balances?.allowance || 0);
if (value.isGreaterThan(allowance)) {
return t(
"You can't deposit more than your approved deposit amount, %s %s",
[
formatNumber(allowance.toString()),
selectedAsset?.symbol || ' ',
]
);
}
return true;
},
@@ -322,14 +327,24 @@ export const DepositForm = ({
}
if (value.isGreaterThan(lifetimeLimit)) {
return t('Amount is above lifetime deposit limit');
return t(
"You can't deposit more than your remaining deposit allowance, %s %s",
[
formatNumber(lifetimeLimit.toString()),
selectedAsset?.symbol || ' ',
]
);
}
return true;
},
balance: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(balances?.balance || 0)) {
return t('Insufficient amount in Ethereum wallet');
const balance = new BigNumber(balances?.balance || 0);
if (value.isGreaterThan(balance)) {
return t(
"You can't deposit more than you have in your Ethereum wallet, %s %s",
[formatNumber(balance), selectedAsset?.symbol || ' ']
);
}
return true;
},
@@ -405,7 +420,7 @@ const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
type="submit"
data-testid="deposit-submit"
variant={isActive ? 'primary' : 'default'}
fill={true}
fill
disabled={invalidChain}
>
{t('Deposit')}
+47 -15
View File
@@ -1,8 +1,13 @@
import type { Asset } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n';
import { CompactNumber } from '@vegaprotocol/react-helpers';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
import {
KeyValueTable,
KeyValueTableRow,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import type BigNumber from 'bignumber.js';
import { formatNumber } from '@vegaprotocol/utils';
// Note: all of the values here are with correct asset's decimals
// See `libs/deposits/src/lib/use-deposit-balances.ts`
@@ -33,21 +38,35 @@ export const DepositLimits = ({
'-'
),
},
{
key: 'MAX_LIMIT',
label: t('Lifetime deposit allowance'),
rawValue: max,
value: <CompactNumber number={max} decimals={asset.decimals} />,
},
{
key: 'DEPOSITED',
label: t('Deposited'),
rawValue: deposited,
value: <CompactNumber number={deposited} decimals={asset.decimals} />,
},
{
key: 'REMAINING',
label: t('Remaining'),
label: (
<Tooltip
description={
<>
<p>
{t(
'VEGA has a lifetime deposit limit of %s %s per address. This can be changed through governance',
[formatNumber(max.toString()), asset.symbol]
)}
</p>
<p>
{t(
'To date, %s %s has been deposited from this Ethereum address, so you can deposit up to %s %s more.',
[
formatNumber(deposited.toString()),
asset.symbol,
formatNumber(max.minus(deposited).toString()),
asset.symbol,
]
)}
</p>
</>
}
>
<button type="button">{t('Remaining deposit allowance')}</button>
</Tooltip>
),
rawValue: max.minus(deposited),
value: (
<CompactNumber
@@ -58,7 +77,20 @@ export const DepositLimits = ({
},
{
key: 'ALLOWANCE',
label: t('Approved'),
label: (
<Tooltip
description={
<p>
{t(
'The deposit cap is set when you approve an asset for use with this app. To increase this cap, approve %s again and choose a higher cap. Check the documentation for your Ethereum wallet app for details.',
asset.symbol
)}
</p>
}
>
<button type="button">{t('Ethereum deposit cap')}</button>
</Tooltip>
),
rawValue: allowance,
value: allowance ? (
<CompactNumber number={allowance} decimals={asset.decimals} />
+3 -3
View File
@@ -1,4 +1,5 @@
import { isAssetTypeERC20, removeDecimal } from '@vegaprotocol/utils';
import { MaxUint256 } from '@ethersproject/constants';
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
import {
EthTxStatus,
useEthereumConfig,
@@ -37,10 +38,9 @@ export const useSubmitApproval = (
},
perform: () => {
if (!asset || !config) return;
const amount = removeDecimal('1000000', asset.decimals);
const id = createEthTransaction(contract, 'approve', [
config?.collateral_bridge_contract.address,
amount,
MaxUint256.toString(),
]);
setId(id);
},
+3
View File
@@ -1 +1,4 @@
import '@testing-library/jest-dom';
import ResizeObserver from 'resize-observer-polyfill';
global.ResizeObserver = ResizeObserver;
@@ -1,6 +1,6 @@
import type { MockedResponse } from '@apollo/react-testing';
import { MockedProvider } from '@apollo/react-testing';
import { render, screen, waitFor } from '@testing-library/react';
import { act, render, screen, waitFor } from '@testing-library/react';
import { RadioGroup } from '@vegaprotocol/ui-toolkit';
import type {
BlockTimeSubscription,
@@ -9,6 +9,7 @@ import type {
import { BlockTimeDocument } from '../../utils/__generated__/Node';
import { StatisticsDocument } from '../../utils/__generated__/Node';
import type { RowDataProps } from './row-data';
import { POLL_INTERVAL } from './row-data';
import { BLOCK_THRESHOLD, RowData } from './row-data';
import type { HeaderEntry } from '@vegaprotocol/apollo-client';
import { useHeaderStore } from '@vegaprotocol/apollo-client';
@@ -25,7 +26,7 @@ const statsQueryMock: MockedResponse<StatisticsQuery> = {
result: {
data: {
statistics: {
blockHeight: '1234',
blockHeight: '1234', // the actual value used in the component is the value from the header store
vegaTime: new Date().toISOString(),
chainId: 'test-chain-id',
},
@@ -251,4 +252,105 @@ describe('RowData', () => {
expect(mockOnBlockHeight).toHaveBeenCalledWith(blockHeight);
});
it('should poll the query unless an errors is returned', async () => {
jest.useFakeTimers();
const createStatsQueryMock = (
blockHeight: string
): MockedResponse<StatisticsQuery> => {
return {
request: {
query: StatisticsDocument,
},
result: {
data: {
statistics: {
blockHeight,
vegaTime: new Date().toISOString(),
chainId: 'test-chain-id',
},
},
},
};
};
const createFailedStatsQueryMock = (): MockedResponse<StatisticsQuery> => {
return {
request: {
query: StatisticsDocument,
},
result: {
data: undefined,
},
error: new Error('failed'),
};
};
mockHeaders(props.url);
const statsQueryMock1 = createStatsQueryMock('1234');
const statsQueryMock2 = createStatsQueryMock('1235');
const statsQueryMock3 = createFailedStatsQueryMock();
const statsQueryMock4 = createStatsQueryMock('1236');
render(
<MockedProvider
mocks={[
statsQueryMock1,
statsQueryMock2,
statsQueryMock3,
statsQueryMock4,
subMock,
]}
>
<RadioGroup>
{/* Radio group required as radio is being render in isolation */}
<RowData {...props} />
</RadioGroup>
</MockedProvider>
);
expect(screen.getByTestId('block-height-cell')).toHaveTextContent(
'Checking'
);
// statsQueryMock1 should be rendered
await waitFor(() => {
const elem = screen.getByTestId('query-block-height');
expect(elem).toHaveAttribute('data-query-block-height', '1234');
});
await act(async () => {
jest.advanceTimersByTime(POLL_INTERVAL);
});
// statsQueryMock2 should be rendered
await waitFor(() => {
const elem = screen.getByTestId('query-block-height');
expect(elem).toHaveAttribute('data-query-block-height', '1235');
});
await act(async () => {
jest.advanceTimersByTime(POLL_INTERVAL);
});
// statsQueryMock3 should FAIL!
await waitFor(() => {
const elem = screen.getByTestId('query-block-height');
expect(elem).toHaveAttribute('data-query-block-height', 'failed');
});
// run the timer again, but statsQueryMock4's result should not be
// rendered even though its successful, because the poll
// should have been stopped
await act(async () => {
jest.advanceTimersByTime(POLL_INTERVAL);
});
// should still render the result of statsQueryMock3
await waitFor(() => {
const elem = screen.getByTestId('query-block-height');
expect(elem).toHaveAttribute('data-query-block-height', 'failed');
});
jest.useRealTimers();
});
});
@@ -11,7 +11,7 @@ import {
} from '../../utils/__generated__/Node';
import { LayoutCell } from './layout-cell';
const POLL_INTERVAL = 1000;
export const POLL_INTERVAL = 1000;
export const BLOCK_THRESHOLD = 3;
export interface RowDataProps {
@@ -60,18 +60,22 @@ export const RowData = ({
// handle polling
useEffect(() => {
const handleStartPoll = () => startPolling(POLL_INTERVAL);
const handleStartPoll = () => {
if (error) return;
startPolling(POLL_INTERVAL);
};
const handleStopPoll = () => stopPolling();
// start polling on mount, but only if there is no error
if (error) {
handleStopPoll();
} else {
handleStartPoll();
}
window.addEventListener('blur', handleStopPoll);
window.addEventListener('focus', handleStartPoll);
handleStartPoll();
if (error) {
stopPolling();
}
return () => {
window.removeEventListener('blur', handleStopPoll);
window.removeEventListener('focus', handleStartPoll);
@@ -81,6 +85,7 @@ export const RowData = ({
// measure response time
useEffect(() => {
if (!isValidUrl(url)) return;
if (typeof window.performance.getEntriesByName !== 'function') return; // protection for test environment
// every time we get data measure response speed
const requestUrl = new URL(url);
const requests = window.performance.getEntriesByName(requestUrl.href);
@@ -177,7 +182,14 @@ export const RowData = ({
hasError={getHasError()}
dataTestId="block-height-cell"
>
{getBlockDisplayValue(headers?.blockHeight, error)}
<span
data-testid="query-block-height"
data-query-block-height={
error ? 'failed' : data?.statistics.blockHeight
}
>
{getBlockDisplayValue(headers?.blockHeight, error)}
</span>
</LayoutCell>
<LayoutCell
label={t('Subscription')}
+13 -15
View File
@@ -48,12 +48,12 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
throttle(() => {
dataRef.current = {
...marketDataRef.current,
indicativePrice: marketDataRef.current?.indicativePrice
? getPriceLevel(
marketDataRef.current.indicativePrice,
resolutionRef.current
)
: undefined,
indicativePrice:
marketDataRef.current?.indicativePrice &&
getPriceLevel(
marketDataRef.current.indicativePrice,
resolutionRef.current
),
midPrice: getMidPrice(
rawDataRef.current?.depth.sell,
rawDataRef.current?.depth.buy,
@@ -148,13 +148,12 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
variables,
});
marketDataRef.current = marketData;
if (!marketDataRef.current && marketData) {
marketDataRef.current = marketData;
}
useEffect(() => {
const throttleRunner = updateOrderbookData.current;
if (!marketDataRef.current) {
return;
}
if (!data) {
dataRef.current = { rows: null };
setOrderbookData(dataRef.current);
@@ -162,10 +161,9 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
}
dataRef.current = {
...marketDataRef.current,
indicativePrice: getPriceLevel(
marketDataRef.current.indicativePrice,
resolution
),
indicativePrice:
marketDataRef.current?.indicativePrice &&
getPriceLevel(marketDataRef.current.indicativePrice, resolution),
midPrice: getMidPrice(data.depth.sell, data.depth.buy, resolution),
rows: compactRows(data.depth.sell, data.depth.buy, resolution),
};
@@ -175,7 +173,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
return () => {
throttleRunner.cancel();
};
}, [data, marketData, resolution]);
}, [data, resolution]);
useEffect(() => {
resolutionRef.current = resolution;
-3
View File
@@ -1,8 +1,5 @@
export * from './__generated__/OrdersSubscription';
export * from './use-has-active-order';
export * from './use-order-cancel';
export * from './use-order-edit';
export * from './use-order-submit';
export * from './use-order-update';
export * from './use-pending-orders-volume';
export * from './use-order-store';
@@ -1,138 +0,0 @@
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import type { ReactNode } from 'react';
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { useOrderCancel } from './use-order-cancel';
import type { OrderSubSubscription } from './';
import { OrderSubDocument } from './';
import * as Schema from '@vegaprotocol/types';
const defaultWalletContext = {
pubKey: null,
pubKeys: [],
isReadOnly: false,
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(),
disconnect: jest.fn(),
selectPubKey: jest.fn(),
connector: null,
};
function setup(context?: Partial<VegaWalletContextShape>) {
const mocks: MockedResponse<OrderSubSubscription> = {
request: {
query: OrderSubDocument,
variables: {
partyId: context?.pubKey || '',
},
},
result: {
data: {
orders: [
{
type: Schema.OrderType.TYPE_LIMIT,
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: Schema.OrderStatus.STATUS_ACTIVE,
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
expiresAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
side: Schema.Side.SIDE_BUY,
marketId: 'market-id',
__typename: 'OrderUpdate',
},
],
},
},
};
const filterMocks: MockedResponse<OrderSubSubscription> = {
request: {
query: OrderSubDocument,
variables: {
partyId: context?.pubKey || '',
},
},
result: {
data: {
orders: [
{
type: Schema.OrderType.TYPE_LIMIT,
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: Schema.OrderStatus.STATUS_ACTIVE,
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
expiresAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
side: Schema.Side.SIDE_BUY,
marketId: 'market-id',
__typename: 'OrderUpdate',
},
],
},
},
};
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={[mocks, filterMocks]}>
<VegaWalletContext.Provider
value={{ ...defaultWalletContext, ...context }}
>
{children}
</VegaWalletContext.Provider>
</MockedProvider>
);
return renderHook(() => useOrderCancel(), { wrapper });
}
describe('useOrderCancel', () => {
it('has the correct default state', () => {
const { result } = setup();
expect(typeof result.current.cancel).toEqual('function');
expect(typeof result.current.reset).toEqual('function');
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
expect(result.current.transaction.txHash).toEqual(null);
expect(result.current.transaction.error).toEqual(null);
});
it('should not sendTx if no keypair', () => {
const mockSendTx = jest.fn();
const { result } = setup({
sendTx: mockSendTx,
pubKeys: [],
pubKey: null,
});
act(() => {
result.current.cancel({ orderId: 'order-id', marketId: 'market-id' });
});
expect(mockSendTx).not.toHaveBeenCalled();
});
it('should cancel a correctly formatted order', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
const pubKeyObj = { publicKey: '0x123', name: 'test key 1' };
const { result } = setup({
sendTx: mockSendTx,
pubKeys: [pubKeyObj],
pubKey: pubKeyObj.publicKey,
});
const args = {
orderId: 'order-id',
marketId: 'market-id',
};
act(() => {
result.current.cancel(args);
});
expect(mockSendTx).toHaveBeenCalledWith(pubKeyObj.publicKey, {
orderCancellation: args,
});
});
});
@@ -1,83 +0,0 @@
import { useCallback, useState } from 'react';
import {
useVegaWallet,
useVegaTransaction,
useTransactionResult,
} from '@vegaprotocol/wallet';
import type {
OrderCancellationBody,
TransactionResult,
} from '@vegaprotocol/wallet';
import type { OrderSubFieldsFragment } from './';
import * as Sentry from '@sentry/react';
import { useOrderUpdate } from './use-order-update';
export const useOrderCancel = () => {
const { pubKey } = useVegaWallet();
const [cancelledOrder, setCancelledOrder] =
useState<OrderSubFieldsFragment | null>(null);
const [transactionResult, setTransactionResult] =
useState<TransactionResult>();
const {
send,
transaction,
reset: resetTransaction,
setComplete,
Dialog,
} = useVegaTransaction();
const waitForOrderUpdate = useOrderUpdate(transaction);
const waitForTransactionResult = useTransactionResult();
const reset = useCallback(() => {
resetTransaction();
setCancelledOrder(null);
}, [resetTransaction]);
const cancel = useCallback(
async (orderCancellation: OrderCancellationBody['orderCancellation']) => {
if (!pubKey) {
return;
}
setCancelledOrder(null);
try {
const res = await send(pubKey, {
orderCancellation,
});
if (orderCancellation.orderId) {
const cancelledOrder = await waitForOrderUpdate(
orderCancellation.orderId,
pubKey
);
setCancelledOrder(cancelledOrder);
setComplete();
} else if (res) {
const txResult = await waitForTransactionResult(
res.transactionHash,
pubKey
);
setTransactionResult(txResult);
setComplete();
}
return res;
} catch (e) {
Sentry.captureException(e);
return;
}
},
[pubKey, send, setComplete, waitForOrderUpdate, waitForTransactionResult]
);
return {
transaction,
transactionResult,
cancelledOrder,
Dialog,
cancel,
reset,
};
};
@@ -1,179 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
import type { ReactNode } from 'react';
import { useOrderEdit } from './use-order-edit';
import type { OrderSubSubscription } from './__generated__/OrdersSubscription';
import { OrderSubDocument } from './__generated__/OrdersSubscription';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type { Order } from '../components';
import { generateOrder } from '../components';
import * as Schema from '@vegaprotocol/types';
const defaultWalletContext = {
pubKey: null,
pubKeys: [],
isReadOnly: false,
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(),
disconnect: jest.fn(),
selectPubKey: jest.fn(),
connector: null,
};
function setup(order: Order, context?: Partial<VegaWalletContextShape>) {
const mocks: MockedResponse<OrderSubSubscription> = {
request: {
query: OrderSubDocument,
variables: {
partyId: context?.pubKey || '',
},
},
result: {
data: {
orders: [
{
type: Schema.OrderType.TYPE_LIMIT,
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: Schema.OrderStatus.STATUS_ACTIVE,
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
expiresAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
side: Schema.Side.SIDE_BUY,
marketId: 'market-id',
__typename: 'OrderUpdate',
},
],
},
},
};
const filterMocks: MockedResponse<OrderSubSubscription> = {
request: {
query: OrderSubDocument,
variables: {
partyId: context?.pubKey || '',
},
},
result: {
data: {
orders: [
{
type: Schema.OrderType.TYPE_LIMIT,
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: Schema.OrderStatus.STATUS_ACTIVE,
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
expiresAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
side: Schema.Side.SIDE_BUY,
marketId: 'market-id',
__typename: 'OrderUpdate',
},
],
},
},
};
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={[mocks, filterMocks]}>
<VegaWalletContext.Provider
value={{ ...defaultWalletContext, ...context }}
>
{children}
</VegaWalletContext.Provider>
</MockedProvider>
);
return renderHook(() => useOrderEdit(order), { wrapper });
}
describe('useOrderEdit', () => {
it('should edit a correctly formatted order if there is no size', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
const pubKeyObj = { publicKey: '0x123', name: 'test key 1' };
const order = generateOrder({
price: '123456789',
market: { decimalPlaces: 2 },
});
const { result } = setup(order, {
sendTx: mockSendTx,
pubKeys: [pubKeyObj],
pubKey: pubKeyObj.publicKey,
});
act(() => {
result.current.edit({ price: '1234567.89' });
});
expect(mockSendTx).toHaveBeenCalledWith(pubKeyObj.publicKey, {
orderAmendment: {
orderId: order.id,
// eslint-disable-next-line
marketId: order.market!.id,
timeInForce: order.timeInForce,
price: '123456789', // Decimal removed
sizeDelta: 0,
expiresAt: undefined,
},
});
});
it('should edit a correctly formatted order', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
const pubKeyObj = { publicKey: '0x123', name: 'test key 1' };
const order = generateOrder({
price: '123456789',
market: { decimalPlaces: 2 },
});
const { result } = setup(order, {
sendTx: mockSendTx,
pubKeys: [pubKeyObj],
pubKey: pubKeyObj.publicKey,
});
act(() => {
result.current.edit({ price: '1234567.89', size: '20' });
});
expect(mockSendTx).toHaveBeenCalledWith(pubKeyObj.publicKey, {
orderAmendment: {
orderId: order.id,
// eslint-disable-next-line
marketId: order.market!.id,
timeInForce: order.timeInForce,
price: '123456789', // Decimal removed
sizeDelta: 1990,
expiresAt: undefined,
},
});
});
it('has the correct default state', () => {
const order = generateOrder();
const { result } = setup(order);
expect(typeof result.current.edit).toEqual('function');
expect(typeof result.current.reset).toEqual('function');
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
expect(result.current.transaction.txHash).toEqual(null);
expect(result.current.transaction.error).toEqual(null);
});
it('should not sendTx if no keypair', async () => {
const order = generateOrder();
const mockSendTx = jest.fn();
const { result } = setup(order, {
sendTx: mockSendTx,
pubKeys: [],
pubKey: null,
});
await act(async () => {
result.current.edit(order);
});
expect(mockSendTx).not.toHaveBeenCalled();
});
});
@@ -1,82 +0,0 @@
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
import { useState, useCallback } from 'react';
import { useVegaTransaction, useVegaWallet } from '@vegaprotocol/wallet';
import type { OrderSubFieldsFragment } from './';
import * as Sentry from '@sentry/react';
import type { Order } from '../components';
import { useOrderUpdate } from './use-order-update';
import BigNumber from 'bignumber.js';
export interface EditOrderArgs {
price: string;
size?: string;
}
export const useOrderEdit = (order: Order | null) => {
const { pubKey } = useVegaWallet();
const [updatedOrder, setUpdatedOrder] =
useState<OrderSubFieldsFragment | null>(null);
const {
send,
transaction,
reset: resetTransaction,
setComplete,
Dialog,
} = useVegaTransaction();
const waitForOrderUpdate = useOrderUpdate(transaction);
const reset = useCallback(() => {
resetTransaction();
setUpdatedOrder(null);
}, [resetTransaction]);
const edit = useCallback(
async (args: EditOrderArgs) => {
if (!pubKey || !order || !order.market) {
return;
}
setUpdatedOrder(null);
try {
await send(pubKey, {
orderAmendment: {
orderId: order.id,
marketId: order.market.id,
price: removeDecimal(args.price, order.market.decimalPlaces),
timeInForce: order.timeInForce,
sizeDelta: args.size
? new BigNumber(
removeDecimal(args.size, order.market.positionDecimalPlaces)
)
.minus(order.size)
.toNumber()
: 0,
expiresAt: order.expiresAt
? toNanoSeconds(order.expiresAt) // Wallet expects timestamp in nanoseconds
: undefined,
},
});
const updatedOrder = await waitForOrderUpdate(order.id, pubKey);
setUpdatedOrder(updatedOrder);
setComplete();
} catch (e) {
Sentry.captureException(e);
return;
}
},
[pubKey, send, order, setComplete, waitForOrderUpdate]
);
return {
transaction,
updatedOrder,
Dialog,
edit,
reset,
};
};
@@ -1,205 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import type { PubKey, VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
import * as Schema from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useOrderSubmit } from './use-order-submit';
import type { OrderSubSubscription } from './';
import { OrderSubDocument } from './';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
const marketId = 'market-id';
const defaultWalletContext = {
pubKey: null,
pubKeys: [],
isReadOnly: false,
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(),
disconnect: jest.fn(),
selectPubKey: jest.fn(),
connector: null,
};
function setup(context?: Partial<VegaWalletContextShape>) {
const mocks: MockedResponse<OrderSubSubscription> = {
request: {
query: OrderSubDocument,
variables: {
partyId: context?.pubKey || '',
},
},
result: {
data: {
orders: [
{
type: Schema.OrderType.TYPE_LIMIT,
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: Schema.OrderStatus.STATUS_ACTIVE,
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
expiresAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
side: Schema.Side.SIDE_BUY,
marketId: 'market-id',
},
],
},
},
};
const filterMocks: MockedResponse<OrderSubSubscription> = {
request: {
query: OrderSubDocument,
variables: {
partyId: context?.pubKey || '',
},
},
result: {
data: {
orders: [
{
type: Schema.OrderType.TYPE_LIMIT,
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: Schema.OrderStatus.STATUS_ACTIVE,
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
expiresAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
side: Schema.Side.SIDE_BUY,
marketId: 'market-id',
__typename: 'OrderUpdate',
},
],
},
},
};
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={[mocks, filterMocks]}>
<VegaWalletContext.Provider
value={{ ...defaultWalletContext, ...context }}
>
{children}
</VegaWalletContext.Provider>
</MockedProvider>
);
return renderHook(() => useOrderSubmit(), { wrapper });
}
describe('useOrderSubmit', () => {
it('should submit a correctly formatted order on GTT', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
const pubKey = '0x123';
const { result } = setup({
sendTx: mockSendTx,
pubKeys: [{ publicKey: pubKey, name: 'test key 1' }],
pubKey,
});
const order = {
type: Schema.OrderType.TYPE_LIMIT,
size: '10',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
side: Schema.Side.SIDE_BUY,
price: '123456789',
expiresAt: new Date('2022-01-01').toISOString(),
};
await act(async () => {
result.current.submit({ ...order, marketId });
});
expect(mockSendTx).toHaveBeenCalledWith(pubKey, {
orderSubmission: {
type: Schema.OrderType.TYPE_LIMIT,
marketId,
size: '10',
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
price: '123456789',
expiresAt: new Date('2022-01-01').toISOString(),
},
});
});
it('should submit a correctly formatted order on GTC', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
const publicKeyObj: PubKey = {
publicKey: '0x123',
name: 'test key 1',
};
const { result } = setup({
sendTx: mockSendTx,
pubKeys: [publicKeyObj],
pubKey: publicKeyObj.publicKey,
});
const order = {
type: Schema.OrderType.TYPE_LIMIT,
size: '10',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
side: Schema.Side.SIDE_BUY,
price: '123456789',
expiresAt: new Date('2022-01-01').toISOString(),
};
await act(async () => {
result.current.submit({ ...order, marketId });
});
expect(mockSendTx).toHaveBeenCalledWith(publicKeyObj.publicKey, {
orderSubmission: {
type: Schema.OrderType.TYPE_LIMIT,
marketId,
size: '10',
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
price: '123456789',
expiresAt: new Date('2022-01-01').toISOString(),
},
});
});
it('has the correct default state', () => {
const { result } = setup();
expect(typeof result.current.submit).toEqual('function');
expect(typeof result.current.reset).toEqual('function');
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
expect(result.current.transaction.txHash).toEqual(null);
expect(result.current.transaction.error).toEqual(null);
});
it('should not sendTx if no keypair', async () => {
const mockSendTx = jest.fn();
const { result } = setup({
sendTx: mockSendTx,
pubKeys: [],
pubKey: null,
});
await act(async () => {
result.current.submit({} as OrderSubmissionBody['orderSubmission']);
});
expect(mockSendTx).not.toHaveBeenCalled();
});
it('should not sendTx side is not specified', async () => {
const mockSendTx = jest.fn();
const publicKeyObj: PubKey = {
publicKey: '0x123',
name: 'test key 1',
};
const { result } = setup({
sendTx: mockSendTx,
pubKeys: [publicKeyObj],
pubKey: publicKeyObj.publicKey,
});
await act(async () => {
result.current.submit({} as OrderSubmissionBody['orderSubmission']);
});
expect(mockSendTx).not.toHaveBeenCalled();
});
});
@@ -1,142 +0,0 @@
import { useCallback, useState } from 'react';
import type { ReactNode } from 'react';
import type { OrderSubFieldsFragment } from './__generated__/OrdersSubscription';
import {
useVegaWallet,
useVegaTransaction,
determineId,
} from '@vegaprotocol/wallet';
import * as Sentry from '@sentry/react';
import { useOrderUpdate } from './use-order-update';
import * as Schema from '@vegaprotocol/types';
import { Icon, Intent } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
export const getOrderDialogTitle = (
status?: Schema.OrderStatus
): string | undefined => {
if (!status) {
return;
}
switch (status) {
case Schema.OrderStatus.STATUS_ACTIVE:
return t('Order submitted');
case Schema.OrderStatus.STATUS_FILLED:
return t('Order filled');
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
return t('Order partially filled');
case Schema.OrderStatus.STATUS_PARKED:
return t('Order parked');
case Schema.OrderStatus.STATUS_STOPPED:
return t('Order stopped');
case Schema.OrderStatus.STATUS_CANCELLED:
return t('Order cancelled');
case Schema.OrderStatus.STATUS_EXPIRED:
return t('Order expired');
case Schema.OrderStatus.STATUS_REJECTED:
return t('Order rejected');
default:
return t('Submission failed');
}
};
export const getOrderDialogIntent = (
status?: Schema.OrderStatus
): Intent | undefined => {
if (!status) {
return;
}
switch (status) {
case Schema.OrderStatus.STATUS_PARKED:
case Schema.OrderStatus.STATUS_EXPIRED:
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
return Intent.Warning;
case Schema.OrderStatus.STATUS_REJECTED:
case Schema.OrderStatus.STATUS_STOPPED:
case Schema.OrderStatus.STATUS_CANCELLED:
return Intent.Danger;
case Schema.OrderStatus.STATUS_FILLED:
case Schema.OrderStatus.STATUS_ACTIVE:
return Intent.Success;
default:
return;
}
};
export const getOrderDialogIcon = (
status?: Schema.OrderStatus
): ReactNode | undefined => {
if (!status) {
return;
}
switch (status) {
case Schema.OrderStatus.STATUS_PARKED:
case Schema.OrderStatus.STATUS_EXPIRED:
return <Icon name="warning-sign" size={16} />;
case Schema.OrderStatus.STATUS_REJECTED:
case Schema.OrderStatus.STATUS_STOPPED:
case Schema.OrderStatus.STATUS_CANCELLED:
return <Icon name="error" size={16} />;
default:
return;
}
};
export const useOrderSubmit = () => {
const { pubKey } = useVegaWallet();
const {
send,
transaction,
reset: resetTransaction,
setComplete,
Dialog,
} = useVegaTransaction();
const waitForOrderUpdate = useOrderUpdate(transaction);
const [finalizedOrder, setFinalizedOrder] =
useState<OrderSubFieldsFragment | null>(null);
const reset = useCallback(() => {
resetTransaction();
setFinalizedOrder(null);
}, [resetTransaction]);
const submit = useCallback(
async (orderSubmission: OrderSubmissionBody['orderSubmission']) => {
if (!pubKey || !orderSubmission.side) {
return;
}
setFinalizedOrder(null);
try {
const res = await send(pubKey, { orderSubmission });
if (res) {
const orderId = determineId(res.signature);
if (orderId) {
const order = await waitForOrderUpdate(orderId, pubKey);
setFinalizedOrder(order);
setComplete();
}
}
} catch (e) {
Sentry.captureException(e);
}
},
[pubKey, send, setComplete, waitForOrderUpdate]
);
return {
transaction,
finalizedOrder,
Dialog,
submit,
reset,
};
};
+53
View File
@@ -1,6 +1,7 @@
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
import type { OrderSubFieldsFragment } from './order-hooks';
import { Intent } from '@vegaprotocol/ui-toolkit';
// More detail in https://docs.vega.xyz/mainnet/graphql/enums/order-time-in-force
export const timeInForceLabel = (tif: string) => {
@@ -38,3 +39,55 @@ export const getRejectionReason = (
: null;
}
};
export const getOrderToastTitle = (
status?: Schema.OrderStatus
): string | undefined => {
if (!status) {
return;
}
switch (status) {
case Schema.OrderStatus.STATUS_ACTIVE:
return t('Order submitted');
case Schema.OrderStatus.STATUS_FILLED:
return t('Order filled');
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
return t('Order partially filled');
case Schema.OrderStatus.STATUS_PARKED:
return t('Order parked');
case Schema.OrderStatus.STATUS_STOPPED:
return t('Order stopped');
case Schema.OrderStatus.STATUS_CANCELLED:
return t('Order cancelled');
case Schema.OrderStatus.STATUS_EXPIRED:
return t('Order expired');
case Schema.OrderStatus.STATUS_REJECTED:
return t('Order rejected');
default:
return t('Submission failed');
}
};
export const getOrderToastIntent = (
status?: Schema.OrderStatus
): Intent | undefined => {
if (!status) {
return;
}
switch (status) {
case Schema.OrderStatus.STATUS_PARKED:
case Schema.OrderStatus.STATUS_EXPIRED:
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
return Intent.Warning;
case Schema.OrderStatus.STATUS_REJECTED:
case Schema.OrderStatus.STATUS_STOPPED:
case Schema.OrderStatus.STATUS_CANCELLED:
return Intent.Danger;
case Schema.OrderStatus.STATUS_FILLED:
case Schema.OrderStatus.STATUS_ACTIVE:
return Intent.Success;
default:
return;
}
};
+1
View File
@@ -15,6 +15,7 @@ export const createDocsLinks = (docsUrl: string) => ({
NODE_OPERATORS: `${docsUrl}/node-operators`,
LOSS_SOCIALIZATION: `${docsUrl}/concepts/trading-on-vega/market-protections#loss-socialisation`,
POSITION_RESOLUTION: `${docsUrl}/concepts/trading-on-vega/market-protections#position-resolution`,
LIQUIDITY: `${docsUrl}/concepts/liquidity/provision`,
});
export const ExternalLinks = {
+2 -2
View File
@@ -43,7 +43,7 @@
"@web3-react/walletconnect": "^8.0.23-beta.0",
"ag-grid-community": "^27.0.1",
"ag-grid-react": "^27.0.1",
"allotment": "^1.17.1",
"allotment": "1.18.1",
"alpha-lyrae": "vegaprotocol/alpha-lyrae",
"apollo": "^2.33.9",
"apollo-link-timeout": "^4.0.0",
@@ -63,7 +63,7 @@
"js-sha3": "^0.8.0",
"lodash": "^4.17.21",
"next": "12.2.3",
"pennant": "1.6.0",
"pennant": "1.8.1",
"react": "18.2.0",
"react-copy-to-clipboard": "^5.0.4",
"react-dom": "18.2.0",
+16 -16
View File
@@ -8153,13 +8153,13 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.8.0:
require-from-string "^2.0.2"
uri-js "^4.2.2"
allotment@1.17.1, allotment@^1.17.1:
version "1.17.1"
resolved "https://registry.yarnpkg.com/allotment/-/allotment-1.17.1.tgz#1a87fe911e520e6028df3740d9ba397c48f8e8d7"
integrity sha512-3/blHxKZfP441IZRwoOJaaNfDOx9XtvQQaeRYYrn7xjQWF7ZhYZCfR7oNxudwx2ga21tIskvfytbZF2P/ydE3A==
allotment@1.18.1:
version "1.18.1"
resolved "https://registry.yarnpkg.com/allotment/-/allotment-1.18.1.tgz#a31b0cb5b301309334dfa352c7704361437058de"
integrity sha512-ZhmpHoSvxHyPT5XjzTHeCBjzCMIFn1aGdNAzBCZhj1CcRMp31CaJCi+lfweoOLDbbjJqbFZqUk3xf+p7Mb0x5A==
dependencies:
classnames "^2.3.0"
eventemitter3 "^4.0.0"
eventemitter3 "^5.0.0"
lodash.clamp "^4.0.0"
lodash.debounce "^4.0.0"
lodash.isequal "^4.5.0"
@@ -9962,6 +9962,11 @@ chownr@^2.0.0:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
chroma-js@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.4.2.tgz#dffc214ed0c11fa8eefca2c36651d8e57cbfb2b0"
integrity sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==
chrome-trace-event@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
@@ -18460,10 +18465,10 @@ pend@~1.2.0:
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
pennant@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.6.0.tgz#9c7d873527defba689439a18f2ff2f9aa5ababea"
integrity sha512-0eWZmFNaWMCYHj9C/+KgwFy8sf/dEkR2TilYrkoRgOVgziDFakojHSiNIwFrqYZFXH1HincYwS7PjKFtQkvrRA==
pennant@1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.8.1.tgz#bb5f8fd2ae78e0e53ff43bea565765bf7e47b255"
integrity sha512-kUKRLfM8GdE+RSlS45Rz/QMQ6xorbzAqw/ybOVR/2gVO6L3sY16+vrgVSJd89RbR+sI/foWe5buXwFmjwFR7Kg==
dependencies:
"@babel/runtime" "^7.13.10"
"@d3fc/d3fc-technical-indicator" "^8.0.1"
@@ -18484,7 +18489,8 @@ pennant@1.6.0:
"@types/react" "^18.0.14"
"@types/react-dom" "^18.0.5"
"@types/react-virtualized-auto-sizer" "^1.0.0"
allotment "1.17.1"
allotment "1.18.1"
chroma-js "^2.4.2"
classnames "^2.2.6"
d3-array "2.8.0"
d3-axis "^3.0.0"
@@ -18505,7 +18511,6 @@ pennant@1.6.0:
lodash "^4.17.20"
react-virtualized-auto-sizer "^1.0.4"
sass "^1.49.9"
typescript "^4.1.2"
use-resize-observer "^9.0.0"
performance-now@^2.1.0:
@@ -22304,11 +22309,6 @@ typescript@4.7.4:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
typescript@^4.1.2:
version "4.9.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78"
integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==
typescript@^4.4.3:
version "4.8.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"