Compare commits

...
39 changed files with 1080 additions and 829 deletions
+2 -2
View File
@@ -23,7 +23,7 @@ NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
#Test configuration variables
CYPRESS_FAIRGROUND=false
@@ -31,4 +31,4 @@ LC_ALL="en_US.UTF-8"
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=true
+2 -2
View File
@@ -20,7 +20,7 @@ NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_VEGA_REST_URL=http://localhost:3008/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
@@ -30,4 +30,4 @@ CYPRESS_FAIRGROUND=false
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
NX_METAMASK_SNAPS=false
+2 -2
View File
@@ -15,11 +15,11 @@ NX_VEGA_REST_URL=https://api.n00.devnet1.vega.xyz/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=true
+1 -1
View File
@@ -15,7 +15,7 @@ NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.vega.community/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
+1 -1
View File
@@ -14,7 +14,7 @@ NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-mirror-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.mainnet-mirror.vega.rocks/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
+1 -1
View File
@@ -11,7 +11,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
+1 -1
View File
@@ -16,7 +16,7 @@ NX_VEGA_REST_URL=https://api.n07.testnet.vega.xyz/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+1 -1
View File
@@ -13,7 +13,7 @@ NX_VEGA_REST_URL=https://api-validators-testnet.vega.rocks/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
@@ -1,475 +0,0 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import type { DataSourceDefinition } from '@vegaprotocol/types';
import {
MarketState,
MarketStateMapping,
PropertyKeyType,
} from '@vegaprotocol/types';
import { addDays, subDays } from 'date-fns';
import {
chainIdQuery,
statisticsQuery,
createDataConnection,
oracleSpecDataConnectionQuery,
createMarketFragment,
marketsQuery,
marketsDataQuery,
createMarketsDataFragment,
assetQuery,
networkParamsQuery,
nodeGuardQuery,
} from '@vegaprotocol/mock';
import {
addDecimalsFormatNumber,
getDateTimeFormat,
} from '@vegaprotocol/utils';
describe('Closed markets', { tags: '@smoke' }, () => {
const settlementDataProperty = 'settlement-data-property';
const settlementDataPropertyKey = {
__typename: 'PropertyKey' as const,
name: settlementDataProperty,
type: PropertyKeyType.TYPE_INTEGER,
numberDecimalPlaces: 2,
};
const settlementDataSourceData: DataSourceDefinition = {
sourceType: {
sourceType: {
filters: [
{
__typename: 'Filter',
key: settlementDataPropertyKey,
},
],
},
},
};
const rowSelector =
'[data-testid="tab-closed-markets"] .ag-center-cols-container .ag-row';
const assetsResult = assetQuery();
// @ts-ignore asset definitely exists
const settlementAsset = assetsResult.assetsConnection.edges[0].node;
const settledMarket = createMarketFragment({
id: '0',
state: MarketState.STATE_SETTLED,
marketTimestamps: {
open: subDays(new Date(), 10).toISOString(),
close: subDays(new Date(), 4).toISOString(),
},
tradableInstrument: {
instrument: {
product: {
dataSourceSpecBinding: {
settlementDataProperty,
},
dataSourceSpecForTradingTermination: {
id: 'market-1-trading-termination-oracle-id',
},
dataSourceSpecForSettlementData: {
id: 'market-1-settlement-data-oracle-id',
data: settlementDataSourceData,
},
settlementAsset,
},
},
},
});
const terminatedMarket = createMarketFragment({
id: '1',
state: MarketState.STATE_TRADING_TERMINATED,
marketTimestamps: {
open: subDays(new Date(), 10).toISOString(),
close: null, // market
},
tradableInstrument: {
instrument: {
metadata: {
tags: [
`settlement-expiry-date:${addDays(new Date(), 4).toISOString()}`,
],
},
product: {
dataSourceSpecBinding: {
settlementDataProperty,
},
dataSourceSpecForSettlementData: {
id: 'market-1-settlement-data-oracle-id',
data: settlementDataSourceData,
},
},
},
},
});
const delayedSettledMarket = createMarketFragment({
id: '2',
state: MarketState.STATE_TRADING_TERMINATED,
marketTimestamps: {
open: subDays(new Date(), 10).toISOString(),
close: null, // market
},
tradableInstrument: {
instrument: {
metadata: {
tags: [
`settlement-expiry-date:${subDays(new Date(), 2).toISOString()}`,
],
},
product: {
dataSourceSpecBinding: {
settlementDataProperty,
},
dataSourceSpecForSettlementData: {
id: 'market-1-settlement-data-oracle-id',
data: settlementDataSourceData,
},
},
},
},
});
const unknownMarket = createMarketFragment({
id: '3',
state: MarketState.STATE_SETTLED,
});
const closedMarketsResult = [
{
node: settledMarket,
},
{
node: terminatedMarket,
},
{
node: delayedSettledMarket,
},
{ node: unknownMarket },
{
node: createMarketFragment({ id: '4', state: MarketState.STATE_PENDING }),
},
{
node: createMarketFragment({ id: '5', state: MarketState.STATE_ACTIVE }),
},
];
const settledMarketData = createMarketsDataFragment({
market: {
id: settledMarket.id,
},
bestBidPrice: '1000',
bestOfferPrice: '2000',
markPrice: '1500',
});
const closedMarketsDataResult = [
{
node: {
data: settledMarketData,
},
},
{
node: {
data: createMarketsDataFragment({
market: {
id: terminatedMarket.id,
},
}),
},
},
{
node: {
data: createMarketsDataFragment({
market: {
id: delayedSettledMarket.id,
},
}),
},
},
{
node: {
data: createMarketsDataFragment({
market: {
id: unknownMarket.id,
},
}),
},
},
];
const specDataConnection = createDataConnection();
before(() => {
cy.setOnBoardingViewed();
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'NodeGuard', nodeGuardQuery());
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
aliasGQLQuery(
req,
'Markets',
marketsQuery({
marketsConnection: {
edges: closedMarketsResult,
},
})
);
aliasGQLQuery(
req,
'MarketsData',
marketsDataQuery({
marketsConnection: {
edges: closedMarketsDataResult,
},
})
);
aliasGQLQuery(
req,
'OracleSpecDataConnection',
oracleSpecDataConnectionQuery()
);
});
cy.mockSubscription();
cy.visit('/#/markets/all');
cy.get('[data-testid="Closed markets"]').click();
});
it('renders a settled market', () => {
const expectedMarkets = closedMarketsResult.filter((edge) => {
return [
MarketState.STATE_SETTLED,
MarketState.STATE_TRADING_TERMINATED,
].includes(edge.node.state);
});
const product = settledMarket.tradableInstrument.instrument.product;
// rows should be filtered to only include settled/terminated markets
cy.get(rowSelector).should('have.length', expectedMarkets.length);
// check each column in the first row renders correctly
// 6001-MARK-001
cy.get(rowSelector)
.first()
.find('[col-id="code"]')
.find('[data-testid="market-code"]')
.should('have.text', settledMarket.tradableInstrument.instrument.code);
// 6001-MARK-071
cy.get(rowSelector)
.first()
.find('[title="Future"]')
.should('have.text', 'Futr');
// 6001-MARK-002
cy.get(rowSelector)
.first()
.find('[col-id="name"]')
.should('have.text', settledMarket.tradableInstrument.instrument.name);
// 6001-MARK-003
cy.get(rowSelector)
.first()
.find('[col-id="state"]')
.should('have.text', MarketStateMapping[settledMarket.state]);
// 6001-MARK-004
// 6001-MARK-005
// 6001-MARK-009
// 6001-MARK-008
// 6001-MARK-010
cy.get(rowSelector)
.first()
.find('[col-id="settlementDate"]')
.find('[data-testid="link"]')
.should(($el) => {
const href = $el.attr('href');
expect(href).to.match(
new RegExp(
`/oracles/${product.dataSourceSpecForTradingTermination.id}`
)
);
})
.should('have.text', '4 days ago')
.should(
'have.attr',
'title',
getDateTimeFormat().format(
new Date(settledMarket.marketTimestamps.close)
)
);
// 6001-MARK-011
cy.get(rowSelector)
.first()
.find('[col-id="bestBidPrice"]')
.should(
'have.text',
addDecimalsFormatNumber(
settledMarketData.bestBidPrice,
settledMarket.decimalPlaces
)
);
// 6001-MARK-012
cy.get(rowSelector)
.first()
.find('[col-id="bestOfferPrice"]')
.should(
'have.text',
addDecimalsFormatNumber(
settledMarketData.bestOfferPrice,
settledMarket.decimalPlaces
)
);
// 6001-MARK-013
cy.get(rowSelector).first().find('[col-id="markPrice"]').should(
'have.text',
addDecimalsFormatNumber(
settledMarketData.markPrice,
settledMarket.decimalPlaces
)
);
// 6001-MARK-014
// 6001-MARK-015
// 6001-MARK-016
cy.get(rowSelector)
.first()
.find('[col-id="settlementDataOracleId"]')
.find('[data-testid="link"]')
.should(($el) => {
const href = $el.attr('href');
expect(href).to.match(
new RegExp(`/oracles/${product.dataSourceSpecForSettlementData.id}`)
);
})
.should(
'have.text',
addDecimalsFormatNumber(
// @ts-ignore cannot deep un-partial
specDataConnection.externalData.data.data[0].value,
settlementDataPropertyKey.numberDecimalPlaces
)
);
// 6001-MARK-018
cy.get(rowSelector)
.first()
.find('[col-id="settlementAsset"]')
.should('have.text', product.settlementAsset.symbol);
// 6001-MARK-020
cy.get('.ag-pinned-right-cols-container')
.find('[col-id="market-actions"]')
.first()
.find('button svg')
.should('exist');
if (Cypress.env('NX_SUCCESSOR_MARKETS')) {
cy.get(rowSelector)
.find('[col-id="successorMarket"]')
.first()
.should('have.text', '-');
}
});
// test market list for market in terminated state
it('renders a terminated market', () => {
cy.get(rowSelector)
.eq(1)
.find('[col-id="state"]')
.should('have.text', MarketStateMapping[terminatedMarket.state]);
// 6001-MARK-006
// 6001-MARK-007
cy.get(rowSelector)
.eq(1)
.find('[col-id="settlementDate"]')
.find('[data-testid="link"]')
.should('have.text', 'Expected in 4 days');
});
it('renders a terminated market which was expected to have settled', () => {
cy.get(rowSelector)
.eq(2)
.find('[col-id="settlementDate"]')
.should('have.class', 'text-danger')
.find('[data-testid="link"]')
.should('have.text', 'Expected 2 days ago');
});
it('renders terminated market which doesnt have settlement date metadata', () => {
cy.get(rowSelector)
.eq(3)
.find('[col-id="settlementDate"]')
.find('[data-testid="link"]')
.should('have.text', 'Unknown');
});
it('can open asset detail dialog', () => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Asset', assetsResult);
});
cy.get(rowSelector)
.first()
.find('[col-id="settlementAsset"]')
.find('button')
.click();
// 6001-MARK-019
cy.get('[data-testid="dialog-title"]').should(
'have.text',
`Asset details - ${settlementAsset.symbol}`
);
cy.get('[data-testid="dialog-close"]').click();
});
it('can open row actions', () => {
cy.get('.ag-pinned-right-cols-container')
.find('[col-id="market-actions"]')
.first()
.find('button')
.click();
const dropdownContent = '[data-testid="market-actions-content"]';
const dropdownContentItem = '[role="menuitem"]';
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(0)
// Cannot click the copy button as it falls back to window.prompt, blocking the test.
.should('have.text', 'Copy Market ID');
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(1)
.find('a')
.then(($el) => {
const href = $el.attr('href');
expect(/\/markets\/0/.test(href || '')).to.equal(true);
})
.should('have.text', 'View on Explorer');
});
});
describe('no closed markets', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all');
cy.get('[data-testid="Closed markets"]').click();
});
it('can see no markets message', () => {
// 6001-MARK-034
cy.getByTestId('tab-closed-markets').should('contain.text', 'No markets');
});
});
@@ -133,11 +133,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
validateMarketDataRow(4, 'Decimals', '5');
validateMarketDataRow(5, 'Quantum', '1');
validateMarketDataRow(6, 'Status', 'Enabled');
validateMarketDataRow(
7,
'Contract address',
'0x0158031158Bb4dF2AD02eAA31e8963E84EA978a4'
);
validateMarketDataRow(7, 'Contract address', '0x0158…78a4');
validateMarketDataRow(8, 'Withdrawal threshold', '0.0005');
validateMarketDataRow(9, 'Lifetime limit', '1,230');
validateMarketDataRow(10, 'Infrastructure fee account balance', '0.00001');
@@ -33,9 +33,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
it('must see the price unit', function () {
// 7002-SORD-018
cy.getByTestId(orderPriceField)
.siblings('label')
.should('have.text', 'Price (DAI)');
cy.getByTestId(orderPriceField).next().should('have.text', 'DAI');
});
it('must see warning when placing an order with expiry date in past', () => {
@@ -43,11 +43,10 @@ describe('ethereum wallet', { tags: '@smoke', testIsolation: true }, () => {
// 0004-EWAL-005
// 0004-EWAL-006
const ethWalletAddress = Cypress.env('ETHEREUM_WALLET_ADDRESS');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
connectEthereumWallet('MetaMask');
cy.getByTestId('ethereum-address').should('have.text', ethWalletAddress);
cy.getByTestId('ethereum-address').should('have.text', '0xEe7D…d94F');
cy.getByTestId('disconnect-ethereum-wallet')
.should('have.text', 'Disconnect')
.click();
+1 -1
View File
@@ -13,7 +13,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
+1 -1
View File
@@ -13,7 +13,7 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_ETH_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supply green clown session"
+1 -1
View File
@@ -14,7 +14,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+1 -1
View File
@@ -14,7 +14,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-mainnet/codfcglpplgmmlokgilfkpcjnmkbfiel
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet-mainnet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet
# TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.20.21-core-0.71.6
+1 -1
View File
@@ -14,7 +14,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.mainnet-mirror.vega.rocks
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-mainnet/codfcglpplgmmlokgilfkpcjnmkbfiel
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet-mainnet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet
# TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.20.19-core-0.71.6
+1 -1
View File
@@ -14,7 +14,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+1 -1
View File
@@ -15,7 +15,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+1 -1
View File
@@ -16,7 +16,7 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
+1 -1
View File
@@ -97,7 +97,7 @@ export const AssetDetailsDialog = ({
}}
>
{content}
<p className="text-sm my-4">
<p className="text-xs my-4">
{t(
'There is 1 unit of the settlement asset (%s) to every 1 quote unit.',
[assetSymbol]
+9 -3
View File
@@ -3,7 +3,11 @@ import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type * as Schema from '@vegaprotocol/types';
import type { KeyValueTableRowProps } from '@vegaprotocol/ui-toolkit';
import { CopyWithTooltip, Icon } from '@vegaprotocol/ui-toolkit';
import {
CopyWithTooltip,
Icon,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import {
KeyValueTable,
KeyValueTableRow,
@@ -56,7 +60,7 @@ export const rows: Rows = [
key: AssetDetail.ID,
label: t('ID'),
tooltip: '',
value: (asset) => asset.id,
value: (asset) => truncateMiddle(asset.id),
},
{
key: AssetDetail.TYPE,
@@ -109,7 +113,9 @@ export const rows: Rows = [
return (
<>
<EtherscanLink address={asset.source.contractAddress} />{' '}
<EtherscanLink address={asset.source.contractAddress}>
{truncateMiddle(asset.source.contractAddress)}
</EtherscanLink>{' '}
<CopyWithTooltip text={asset.source.contractAddress}>
<button title={t('Copy address to clipboard')}>
<Icon size={3} name="duplicate" />
+2 -2
View File
@@ -1,4 +1,4 @@
import { TradingOption } from '@vegaprotocol/ui-toolkit';
import { TradingOption, truncateMiddle } from '@vegaprotocol/ui-toolkit';
import type { AssetFieldsFragment } from './__generated__/Asset';
import classNames from 'classnames';
import { t } from '@vegaprotocol/i18n';
@@ -45,7 +45,7 @@ export const AssetOption = ({ asset, balance }: AssetOptionProps) => {
{balance}
<div className="text-[12px] font-mono w-full text-left break-all">
<span className="text-vega-light-300 dark:text-vega-dark-300">
{asset.id}
{truncateMiddle(asset.id)}
</span>
</div>
</div>
@@ -1,25 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { Side } from '@vegaprotocol/types';
import classNames from 'classnames';
interface Props {
side: Side;
label?: string;
}
export const DealTicketButton = ({ side, label }: Props) => {
const buttonClasses = classNames(
'px-10 py-2 uppercase rounded-md text-white w-full',
{
'bg-market-red': side === Side.SIDE_SELL,
'bg-market-green-550': side === Side.SIDE_BUY,
}
);
return (
<div className="mb-2">
<button type="submit" data-testid="place-order" className={buttonClasses}>
{label || t('Place order')}
</button>
</div>
);
};
@@ -1,7 +1,4 @@
import { useCallback, useState } from 'react';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import classnames from 'classnames';
import type { ReactNode } from 'react';
import { t } from '@vegaprotocol/i18n';
import { FeesBreakdown } from '@vegaprotocol/markets';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
@@ -16,7 +13,6 @@ import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
import {
NOTIONAL_SIZE_TOOLTIP_TEXT,
MARGIN_DIFF_TOOLTIP_TEXT,
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
TOTAL_MARGIN_AVAILABLE,
@@ -25,114 +21,54 @@ import {
MARGIN_ACCOUNT_TOOLTIP_TEXT,
} from '../../constants';
import { useEstimateFees } from '../../hooks';
import { KeyValue } from './key-value';
const emptyValue = '-';
export interface DealTicketFeeDetailPros {
label: string;
value?: string | null | undefined;
symbol: string;
indent?: boolean | undefined;
labelDescription?: ReactNode;
formattedValue?: string;
onClick?: () => void;
}
export const DealTicketFeeDetail = ({
label,
value,
labelDescription,
symbol,
indent,
onClick,
formattedValue,
}: DealTicketFeeDetailPros) => {
const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`;
const valueElement = onClick ? (
<button onClick={onClick} className="text-muted">
{displayValue}
</button>
) : (
<div className="text-muted">{displayValue}</div>
);
return (
<div
data-testid={
'deal-ticket-fee-' + label.toLocaleLowerCase().replace(/\s/g, '-')
}
key={typeof label === 'string' ? label : 'value-dropdown'}
className={classnames(
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
{ 'ml-2': indent }
)}
>
<Tooltip description={labelDescription}>
<div>{label}</div>
</Tooltip>
<Tooltip description={`${value ?? '-'} ${symbol || ''}`}>
{valueElement}
</Tooltip>
</div>
);
};
export interface DealTicketFeeDetailsProps {
assetSymbol: string;
order: OrderSubmissionBody['orderSubmission'];
market: Market;
notionalSize: string | null;
}
export const DealTicketFeeDetails = ({
assetSymbol,
order,
market,
notionalSize,
}: DealTicketFeeDetailsProps) => {
const feeEstimate = useEstimateFees(order);
const { settlementAsset: asset } =
market.tradableInstrument.instrument.product;
const { decimals: assetDecimals, quantum } = asset;
const marketDecimals = market.decimalPlaces;
const quoteName = market.tradableInstrument.instrument.product.quoteName;
return (
<>
<DealTicketFeeDetail
label={t('Notional')}
value={formatValue(notionalSize, marketDecimals)}
formattedValue={formatValue(notionalSize, marketDecimals)}
symbol={quoteName}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
/>
<DealTicketFeeDetail
label={t('Fees')}
value={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
}
formattedValue={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
}
labelDescription={
<>
<span>
{t(
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
)}
</span>
<FeesBreakdown
fees={feeEstimate?.fees}
feeFactors={market.fees.factors}
symbol={assetSymbol}
decimals={assetDecimals}
/>
</>
}
symbol={assetSymbol}
/>
</>
<KeyValue
label={t('Fees')}
value={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
}
formattedValue={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
}
labelDescription={
<>
<span>
{t(
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
)}
</span>
<FeesBreakdown
fees={feeEstimate?.fees}
feeFactors={market.fees.factors}
symbol={assetSymbol}
decimals={assetDecimals}
/>
</>
}
symbol={assetSymbol}
/>
);
};
@@ -209,7 +145,7 @@ export const DealTicketMarginDetails = ({
BigInt(marginAccountBalance);
deductionFromCollateral = (
<DealTicketFeeDetail
<KeyValue
indent
label={t('Deduction from collateral')}
value={formatRange(
@@ -236,7 +172,7 @@ export const DealTicketMarginDetails = ({
/>
);
projectedMargin = (
<DealTicketFeeDetail
<KeyValue
label={t('Projected margin')}
value={formatRange(
marginEstimate?.bestCase.initialLevel,
@@ -308,7 +244,7 @@ export const DealTicketMarginDetails = ({
return (
<>
<DealTicketFeeDetail
<KeyValue
label={t('Margin required')}
value={formatRange(
marginRequiredBestCase,
@@ -324,7 +260,7 @@ export const DealTicketMarginDetails = ({
labelDescription={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}
symbol={assetSymbol}
/>
<DealTicketFeeDetail
<KeyValue
label={t('Total margin available')}
indent
value={formatValue(totalMarginAvailable, assetDecimals)}
@@ -342,7 +278,7 @@ export const DealTicketMarginDetails = ({
)}
/>
{deductionFromCollateral}
<DealTicketFeeDetail
<KeyValue
label={t('Current margin allocation')}
indent
onClick={
@@ -358,7 +294,7 @@ export const DealTicketMarginDetails = ({
)}
/>
{projectedMargin}
<DealTicketFeeDetail
<KeyValue
label={t('Liquidation price estimate')}
value={liquidationPriceEstimate}
formattedValue={liquidationPriceEstimate}
@@ -32,7 +32,7 @@ export const DealTicketSizeIceberg = ({
const renderPeakSizeError = () => {
if (peakSizeError) {
return (
<TradingInputError testId="deal-ticket-peak-error-message-size-limit">
<TradingInputError testId="deal-ticket-peak-error-message">
{peakSizeError}
</TradingInputError>
);
@@ -44,7 +44,7 @@ export const DealTicketSizeIceberg = ({
const renderMinimumSizeError = () => {
if (minimumVisibleSizeError) {
return (
<TradingInputError testId="deal-ticket-minimum-error-message-size-limit">
<TradingInputError testId="deal-ticket-minimum-error-message">
{minimumVisibleSizeError}
</TradingInputError>
);
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { render, screen, waitFor } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { generateMarket } from '../../test-helpers';
import { StopOrder } from './deal-ticket-stop-order';
@@ -12,6 +12,7 @@ import {
useDealTicketFormValues,
} from '../../hooks/use-form-values';
import type { FeatureFlags } from '@vegaprotocol/environment';
import { formatForInput } from '@vegaprotocol/utils';
jest.mock('zustand');
jest.mock('./deal-ticket-fee-details', () => ({
@@ -57,7 +58,7 @@ const orderSideBuy = 'order-side-SIDE_BUY';
const orderSideSell = 'order-side-SIDE_SELL';
const triggerDirectionRisesAbove = 'triggerDirection-risesAbove';
// const triggerDirectionFallsBelow = 'triggerDirection-fallsBelow';
const triggerDirectionFallsBelow = 'triggerDirection-fallsBelow';
const expiryStrategySubmit = 'expiryStrategy-submit';
const expiryStrategyCancel = 'expiryStrategy-cancel';
@@ -65,6 +66,7 @@ const expiryStrategyCancel = 'expiryStrategy-cancel';
const triggerTypePrice = 'triggerType-price';
const triggerTypeTrailingPercentOffset = 'triggerType-trailingPercentOffset';
const oco = 'oco';
const expire = 'expire';
const datePicker = 'date-picker-field';
const timeInForce = 'order-tif';
@@ -76,6 +78,8 @@ const triggerPriceWarningMessage = 'stop-order-warning-message-trigger-price';
const triggerTrailingPercentOffsetErrorMessage =
'stop-order-error-message-trigger-trailing-percent-offset';
const ocoPostfix = (id: string, postfix = true) => (postfix ? `${id}-oco` : id);
describe('StopOrder', () => {
beforeEach(() => {
localStorage.clear();
@@ -107,6 +111,7 @@ describe('StopOrder', () => {
'checked'
);
expect(screen.getByTestId(expire).dataset.state).toEqual('unchecked');
expect(screen.getByTestId(oco).dataset.state).toEqual('unchecked');
await userEvent.click(screen.getByTestId(expire));
await waitFor(() => {
expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual(
@@ -115,6 +120,32 @@ describe('StopOrder', () => {
});
});
it('calculate notional for market limit', async () => {
render(generateJsx());
await userEvent.type(screen.getByTestId(sizeInput), '10');
await userEvent.type(screen.getByTestId(priceInput), '10');
expect(screen.getByTestId('deal-ticket-fee-notional')).toHaveTextContent(
'Notional100.00 BTC'
);
});
it('calculates notional for limit order', async () => {
render(generateJsx());
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeMarket));
await userEvent.type(screen.getByTestId(sizeInput), '10');
// price trigger is selected but it's empty, calculate base on size and marketPrice prop
expect(screen.getByTestId('deal-ticket-fee-notional')).toHaveTextContent(
'Notional20.00 BTC'
);
await userEvent.type(screen.getByTestId(triggerPriceInput), '3');
// calculate base on size and price trigger
expect(screen.getByTestId('deal-ticket-fee-notional')).toHaveTextContent(
'Notional30.00 BTC'
);
});
it('should use local storage state for initial values', async () => {
const values: Partial<StopOrderFormValues> = {
type: Schema.OrderType.TYPE_LIMIT,
@@ -125,6 +156,11 @@ describe('StopOrder', () => {
expire: true,
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS,
expiresAt: '2023-07-27T16:43:27.000',
oco: true,
ocoType: Schema.OrderType.TYPE_LIMIT,
ocoSize: '0.2',
ocoPrice: '300.23',
ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
};
useDealTicketFormValues.setState({
@@ -143,10 +179,22 @@ describe('StopOrder', () => {
expect(screen.getByTestId(sizeInput)).toHaveDisplayValue(
values.size as string
);
expect(screen.getByTestId('order-tif')).toHaveValue(values.timeInForce);
expect(screen.getByTestId(timeInForce)).toHaveValue(values.timeInForce);
expect(screen.getByTestId(priceInput)).toHaveDisplayValue(
values.price as string
);
expect(screen.getByTestId(ocoPostfix(sizeInput))).toHaveDisplayValue(
values.ocoSize as string
);
expect(screen.getByTestId(ocoPostfix(timeInForce))).toHaveValue(
values.ocoTimeInForce
);
expect(screen.getByTestId(ocoPostfix(priceInput))).toHaveDisplayValue(
values.ocoPrice as string
);
expect(screen.getByTestId('ocoTypeLimit').dataset.state).toEqual('checked');
expect(screen.getByTestId(expire).dataset.state).toEqual('checked');
expect(screen.getByTestId(expiryStrategyCancel).dataset.state).toEqual(
'checked'
@@ -154,6 +202,9 @@ describe('StopOrder', () => {
expect(screen.getByTestId(datePicker)).toHaveDisplayValue(
values.expiresAt as string
);
await userEvent.click(screen.getByTestId(orderTypeMarket));
expect(screen.getByTestId(oco).dataset.state).toEqual('unchecked');
});
it('does not submit if no wallet connected', async () => {
@@ -174,145 +225,239 @@ describe('StopOrder', () => {
expect(submit).toBeCalled();
});
it('validates size field', async () => {
it.each([
{ fieldName: 'size', ocoValue: false },
{ fieldName: 'ocoSize', ocoValue: true },
])('validates $fieldName field', async ({ ocoValue }) => {
render(generateJsx());
if (ocoValue) {
await userEvent.click(screen.getByTestId(oco));
}
await userEvent.click(screen.getByTestId(submitButton));
const getByTestId = (id: string) =>
screen.getByTestId(ocoPostfix(id, ocoValue));
const queryByTestId = (id: string) =>
screen.queryByTestId(ocoPostfix(id, ocoValue));
// default value should be invalid
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
expect(getByTestId(sizeErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(screen.getByTestId(sizeInput), '0.01');
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
await userEvent.type(getByTestId(sizeInput), '0.01');
expect(getByTestId(sizeErrorMessage)).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(screen.getByTestId(sizeInput));
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
expect(screen.queryByTestId(sizeErrorMessage)).toBeNull();
await userEvent.clear(getByTestId(sizeInput));
await userEvent.type(getByTestId(sizeInput), '0.1');
expect(queryByTestId(sizeErrorMessage)).toBeNull();
});
it('validates price field', async () => {
it.each([
{ fieldName: 'price', ocoValue: false },
{ fieldName: 'ocoPrice', ocoValue: true },
])('validates $fieldName field', async ({ ocoValue }) => {
render(generateJsx());
if (ocoValue) {
await userEvent.click(screen.getByTestId(oco));
}
await userEvent.click(screen.getByTestId(submitButton));
// price error message should not show if size has error
// expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
// await userEvent.type(screen.getByTestId(sizeInput), '0.1');
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
await userEvent.type(screen.getByTestId(priceInput), '0.001');
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
const getByTestId = (id: string) =>
screen.getByTestId(ocoPostfix(id, ocoValue));
const queryByTestId = (id: string) =>
screen.queryByTestId(ocoPostfix(id, ocoValue));
expect(getByTestId(priceErrorMessage)).toBeInTheDocument();
await userEvent.type(getByTestId(priceInput), '0.001');
expect(getByTestId(priceErrorMessage)).toBeInTheDocument();
// switch to market order type error should disappear
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeMarket));
await userEvent.click(screen.getByTestId(submitButton));
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
expect(queryByTestId(priceErrorMessage)).toBeNull();
// switch back to limit type
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeLimit));
await userEvent.click(screen.getByTestId(submitButton));
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
expect(getByTestId(priceErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(screen.getByTestId(priceInput), '0.001');
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
await userEvent.type(getByTestId(priceInput), '0.001');
expect(getByTestId(priceErrorMessage)).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(screen.getByTestId(priceInput));
await userEvent.type(screen.getByTestId(priceInput), '0.01');
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
await userEvent.clear(getByTestId(priceInput));
await userEvent.type(getByTestId(priceInput), '0.01');
expect(queryByTestId(priceErrorMessage)).toBeNull();
});
it('validates trigger price field', async () => {
it.each([
{ fieldName: 'triggerPrice', ocoValue: false },
{ fieldName: 'ocoTriggerPrice', ocoValue: true },
])('validates $fieldName field', async ({ ocoValue }) => {
render(generateJsx());
if (ocoValue) {
await userEvent.click(screen.getByTestId(oco));
await userEvent.click(screen.getByTestId(triggerDirectionFallsBelow));
}
await userEvent.click(screen.getByTestId(submitButton));
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
const getByTestId = (id: string) =>
screen.getByTestId(ocoPostfix(id, ocoValue));
const queryByTestId = (id: string) =>
screen.queryByTestId(ocoPostfix(id, ocoValue));
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// switch to trailing percentage offset trigger type
await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset));
expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
await userEvent.click(getByTestId(triggerTypeTrailingPercentOffset));
expect(queryByTestId(triggerPriceErrorMessage)).toBeNull();
// switch back to price trigger type
await userEvent.click(screen.getByTestId(triggerTypePrice));
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
await userEvent.click(getByTestId(triggerTypePrice));
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.001');
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
await userEvent.type(getByTestId(triggerPriceInput), '0.001');
expect(getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// clear and fill using value causing immediate trigger
await userEvent.clear(screen.getByTestId(triggerPriceInput));
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.01');
expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
expect(
screen.queryByTestId(triggerPriceWarningMessage)
).toBeInTheDocument();
await userEvent.clear(getByTestId(triggerPriceInput));
await userEvent.type(getByTestId(triggerPriceInput), '0.01');
expect(queryByTestId(triggerPriceErrorMessage)).toBeNull();
expect(queryByTestId(triggerPriceWarningMessage)).toBeInTheDocument();
// change to correct value
await userEvent.type(screen.getByTestId(triggerPriceInput), '2');
expect(screen.queryByTestId(triggerPriceWarningMessage)).toBeNull();
await userEvent.type(getByTestId(triggerPriceInput), '2');
expect(queryByTestId(triggerPriceWarningMessage)).toBeNull();
});
it('validates trigger trailing percentage offset field', async () => {
it.each([
{ fieldName: 'trailingPercentageOffset', ocoValue: false },
{ fieldName: 'ocoTrailingPercentageOffset', ocoValue: true },
])('validates $fieldName field', async ({ ocoValue }) => {
render(generateJsx());
if (ocoValue) {
await userEvent.click(screen.getByTestId(oco));
}
await userEvent.click(screen.getByTestId(submitButton));
const getByTestId = (id: string) =>
screen.getByTestId(ocoPostfix(id, ocoValue));
const queryByTestId = (id: string) =>
screen.queryByTestId(ocoPostfix(id, ocoValue));
// should not show error with default form values
await userEvent.click(screen.getByTestId(submitButton));
expect(
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeNull();
expect(queryByTestId(triggerTrailingPercentOffsetErrorMessage)).toBeNull();
// switch to trailing percentage offset trigger type
await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset));
await userEvent.click(getByTestId(triggerTypeTrailingPercentOffset));
expect(
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
getByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(
screen.getByTestId(triggerTrailingPercentOffsetInput),
getByTestId(triggerTrailingPercentOffsetInput),
'0.09'
);
expect(
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
getByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(
screen.getByTestId(triggerTrailingPercentOffsetInput)
);
await userEvent.type(
screen.getByTestId(triggerTrailingPercentOffsetInput),
'0.1'
);
expect(
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeNull();
await userEvent.clear(getByTestId(triggerTrailingPercentOffsetInput));
await userEvent.type(getByTestId(triggerTrailingPercentOffsetInput), '0.1');
expect(queryByTestId(triggerTrailingPercentOffsetErrorMessage)).toBeNull();
// to big value should be invalid
await userEvent.clear(
screen.getByTestId(triggerTrailingPercentOffsetInput)
);
await userEvent.clear(getByTestId(triggerTrailingPercentOffsetInput));
await userEvent.type(
screen.getByTestId(triggerTrailingPercentOffsetInput),
getByTestId(triggerTrailingPercentOffsetInput),
'99.91'
);
expect(
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
getByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(
screen.getByTestId(triggerTrailingPercentOffsetInput)
);
await userEvent.clear(getByTestId(triggerTrailingPercentOffsetInput));
await userEvent.type(
screen.getByTestId(triggerTrailingPercentOffsetInput),
getByTestId(triggerTrailingPercentOffsetInput),
'99.9'
);
expect(queryByTestId(triggerTrailingPercentOffsetErrorMessage)).toBeNull();
});
it('sync oco trigger', async () => {
render(generateJsx());
await userEvent.click(screen.getByTestId(oco));
expect(
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
).toBeNull();
screen.getByTestId(triggerDirectionRisesAbove).dataset.state
).toEqual('checked');
expect(
screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow)).dataset.state
).toEqual('checked');
await userEvent.click(screen.getByTestId(triggerDirectionFallsBelow));
expect(
screen.getByTestId(triggerDirectionRisesAbove).dataset.state
).toEqual('unchecked');
expect(
screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow)).dataset.state
).toEqual('unchecked');
await userEvent.click(
screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow))
);
expect(
screen.getByTestId(triggerDirectionRisesAbove).dataset.state
).toEqual('checked');
expect(
screen.getByTestId(ocoPostfix(triggerDirectionFallsBelow)).dataset.state
).toEqual('checked');
});
it('disables submit expiry strategy when OCO selected', async () => {
render(generateJsx());
await userEvent.click(screen.getByTestId(expire));
await userEvent.click(screen.getByTestId(expiryStrategySubmit));
await userEvent.click(screen.getByTestId(oco));
expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual(
'unchecked'
);
expect(screen.getByTestId(expiryStrategySubmit)).toBeDisabled();
await userEvent.click(screen.getByTestId(oco));
await userEvent.click(screen.getByTestId(expiryStrategySubmit));
expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual(
'checked'
);
expect(screen.getByTestId(expiryStrategySubmit)).not.toBeDisabled();
});
it('sets expiry time/date to now if expiry is changed to checked', async () => {
const now = Math.round(Date.now() / 1000) * 1000;
render(generateJsx());
jest.spyOn(global.Date, 'now').mockImplementationOnce(() => now);
await userEvent.click(screen.getByTestId(expire));
// expiry time/date was empty it should be set to now
expect(
new Date(screen.getByTestId<HTMLInputElement>(datePicker).value).getTime()
).toEqual(now);
// set to the value in the past (now - 1s)
fireEvent.change(screen.getByTestId<HTMLInputElement>(datePicker), {
target: { value: formatForInput(new Date(now - 1000)) },
});
expect(
new Date(
screen.getByTestId<HTMLInputElement>(datePicker).value
).getTime() + 1000
).toEqual(now);
// switch expiry off and on
await userEvent.click(screen.getByTestId(expire));
await userEvent.click(screen.getByTestId(expire));
// expiry time/date was in the past it should be set to now
expect(
new Date(screen.getByTestId<HTMLInputElement>(datePicker).value).getTime()
).toEqual(now);
});
});
@@ -1,8 +1,12 @@
import { useRef, useCallback, useEffect } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
import type {
OrderSubmissionBody,
StopOrdersSubmission,
} from '@vegaprotocol/wallet';
import {
formatForInput,
formatValue,
removeDecimal,
toDecimal,
validateAmount,
@@ -19,6 +23,9 @@ import {
TradingInputError as InputError,
TradingSelect as Select,
Tooltip,
TradingButton as Button,
Pill,
Intent,
} from '@vegaprotocol/ui-toolkit';
import { getDerivedPrice } from '@vegaprotocol/markets';
import type { Market } from '@vegaprotocol/markets';
@@ -31,6 +38,7 @@ import {
REDUCE_ONLY_TOOLTIP,
stopSubmit,
getNotionalSize,
getAssetUnit,
} from './deal-ticket';
import { TypeToggle } from './type-selector';
import {
@@ -41,9 +49,10 @@ import {
} 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';
import { validateExpiration } from '../../utils';
import { NOTIONAL_SIZE_TOOLTIP_TEXT } from '../../constants';
import { KeyValue } from './key-value';
export interface StopOrderProps {
market: Market;
@@ -78,7 +87,7 @@ const Trigger = ({
control,
watch,
priceStep,
assetSymbol,
quoteName,
oco,
marketPrice,
decimalPlaces,
@@ -86,7 +95,7 @@ const Trigger = ({
control: Control<StopOrderFormValues>;
watch: UseFormWatch<StopOrderFormValues>;
priceStep: string;
assetSymbol: string;
quoteName: string;
oco?: boolean;
marketPrice?: string | null;
decimalPlaces: number;
@@ -181,7 +190,7 @@ const Trigger = ({
data-testid={`triggerPrice${oco ? '-oco' : ''}`}
type="number"
step={priceStep}
appendElement={assetSymbol}
appendElement={<Pill size="xs">{quoteName}</Pill>}
value={value || ''}
hasError={!!fieldState.error}
{...props}
@@ -249,7 +258,7 @@ const Trigger = ({
<Input
type="number"
step={trailingPercentOffsetStep}
appendElement="%"
appendElement={<Pill size="xs">%</Pill>}
data-testid={`triggerTrailingPercentOffset${
oco ? '-oco' : ''
}`}
@@ -311,10 +320,14 @@ const Size = ({
control,
sizeStep,
oco,
isLimitType,
assetUnit,
}: {
control: Control<StopOrderFormValues>;
sizeStep: string;
oco?: boolean;
isLimitType: boolean;
assetUnit?: string;
}) => {
return (
<Controller
@@ -332,7 +345,7 @@ const Size = ({
const { value, ...props } = field;
const id = `order-size${oco ? '-oco' : ''}`;
return (
<div className="mb-4">
<div className={isLimitType ? 'mb-4' : 'mb-2'}>
<FormGroup labelFor={id} label={t(`Size`)} compact>
<Input
id={id}
@@ -341,6 +354,7 @@ const Size = ({
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
appendElement={assetUnit && <Pill size="xs">{assetUnit}</Pill>}
data-testid={id}
value={value || ''}
hasError={!!fieldState.error}
@@ -394,12 +408,8 @@ const Price = ({
const { value, ...props } = field;
const id = `order-price${oco ? '-oco' : ''}`;
return (
<div className="mb-4">
<FormGroup
labelFor={id}
label={t(`Price (${quoteName})`)}
compact={true}
>
<div className="mb-2">
<FormGroup labelFor={id} label={t('Price')} compact={true}>
<Input
id={id}
className="w-full"
@@ -409,6 +419,7 @@ const Price = ({
onWheel={(e) => e.currentTarget.blur()}
value={value || ''}
hasError={!!fieldState.error}
appendElement={<Pill size="xs">{quoteName}</Pill>}
{...props}
/>
</FormGroup>
@@ -434,17 +445,17 @@ const TimeInForce = ({
oco?: boolean;
}) => (
<Controller
name="timeInForce"
name={oco ? 'ocoTimeInForce' : 'timeInForce'}
control={control}
render={({ field, fieldState }) => {
const id = `select-time-in-force${oco ? '-oco' : ''}`;
const id = `order-tif${oco ? '-oco' : ''}`;
return (
<div className="mb-2">
<FormGroup label={t('Time in force')} labelFor={id} compact={true}>
<Select
id={id}
className="w-full"
data-testid="order-tif"
data-testid={id}
hasError={!!fieldState.error}
{...field}
>
@@ -486,6 +497,255 @@ const ReduceOnly = () => (
/>
);
const NotionalAndFees = ({
market,
marketPrice,
side,
size,
price,
timeInForce,
triggerPrice,
triggerType,
type,
}: Pick<
OrderSubmissionBody['orderSubmission'],
'side' | 'size' | 'timeInForce' | 'type' | 'price'
> &
Pick<StopOrderProps, 'market' | 'marketPrice'> &
Pick<StopOrderFormValues, 'triggerType' | 'triggerPrice'>) => {
const { quoteName, settlementAsset: asset } =
market.tradableInstrument.instrument.product;
const isPriceTrigger = triggerType === 'price';
const derivedPrice = getDerivedPrice(
{
type,
price,
},
type === Schema.OrderType.TYPE_MARKET && isPriceTrigger && triggerPrice
? removeDecimal(triggerPrice, market.decimalPlaces)
: marketPrice || '0'
);
const notionalSize = getNotionalSize(
derivedPrice,
size,
market.decimalPlaces,
market.positionDecimalPlaces
);
return (
<div className="mb-4">
<KeyValue
label={t('Notional')}
value={formatValue(notionalSize, market.decimalPlaces)}
formattedValue={formatValue(notionalSize, market.decimalPlaces)}
symbol={quoteName}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
/>
<DealTicketFeeDetails
order={{
marketId: market.id,
price: derivedPrice,
side,
size,
timeInForce,
type,
}}
assetSymbol={asset.symbol}
market={market}
/>
</div>
);
};
const formatSizeAtPrice = ({
assetUnit,
decimalPlaces,
positionDecimalPlaces,
price,
quoteName,
side,
size,
type,
}: Pick<StopOrderFormValues, 'price' | 'side' | 'size' | 'type'> & {
assetUnit?: string;
decimalPlaces: number;
positionDecimalPlaces: number;
quoteName: string;
}) =>
`${formatValue(
removeDecimal(size, positionDecimalPlaces),
positionDecimalPlaces
)} ${assetUnit} @ ${
type === Schema.OrderType.TYPE_MARKET
? 'market'
: `${formatValue(
removeDecimal(price || '0', decimalPlaces),
decimalPlaces
)} ${quoteName}`
}`;
const formatTrigger = ({
decimalPlaces,
triggerDirection,
triggerPrice,
triggerTrailingPercentOffset,
triggerType,
quoteName,
}: Pick<
StopOrderFormValues,
| 'triggerDirection'
| 'triggerType'
| 'triggerPrice'
| 'triggerTrailingPercentOffset'
> & {
decimalPlaces: number;
quoteName: string;
}) =>
`${
triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
? t('above')
: t('below')
} ${
triggerType === 'price'
? `${formatValue(
removeDecimal(triggerPrice || '', decimalPlaces),
decimalPlaces
)} ${quoteName}`
: `${(Number(triggerTrailingPercentOffset) || 0).toFixed(1)}% ${t(
'trailing'
)}`
}`;
const SubmitButton = ({
assetUnit,
market,
oco,
ocoPrice,
ocoSize,
ocoTriggerPrice,
ocoTriggerTrailingPercentOffset,
ocoTriggerType,
ocoType,
price,
side,
size,
triggerDirection,
triggerPrice,
triggerTrailingPercentOffset,
triggerType,
type,
}: Pick<
StopOrderFormValues,
| 'oco'
| 'ocoPrice'
| 'ocoSize'
| 'ocoTriggerPrice'
| 'ocoTriggerTrailingPercentOffset'
| 'ocoTriggerType'
| 'ocoType'
| 'price'
| 'side'
| 'size'
| 'triggerDirection'
| 'triggerPrice'
| 'triggerTrailingPercentOffset'
| 'triggerType'
| 'type'
> &
Pick<StopOrderProps, 'market'> & { assetUnit?: string }) => {
const { quoteName } = market.tradableInstrument.instrument.product;
const risesAbove =
triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE;
const subLabel = oco ? (
<>
{formatSizeAtPrice({
assetUnit,
decimalPlaces: market.decimalPlaces,
positionDecimalPlaces: market.positionDecimalPlaces,
price: risesAbove ? price : ocoPrice,
quoteName,
side,
size: risesAbove ? size : ocoSize,
type,
})}{' '}
{formatTrigger({
decimalPlaces: market.decimalPlaces,
quoteName,
triggerDirection:
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
triggerPrice: risesAbove ? triggerPrice : ocoTriggerPrice,
triggerTrailingPercentOffset: risesAbove
? triggerTrailingPercentOffset
: ocoTriggerTrailingPercentOffset,
triggerType: risesAbove ? triggerType : ocoTriggerType,
})}
<br />
{formatSizeAtPrice({
assetUnit,
decimalPlaces: market.decimalPlaces,
positionDecimalPlaces: market.positionDecimalPlaces,
price: !risesAbove ? price : ocoPrice,
quoteName,
side,
size: !risesAbove ? size : ocoSize,
type: ocoType,
})}{' '}
{formatTrigger({
decimalPlaces: market.decimalPlaces,
quoteName,
triggerDirection:
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW,
triggerPrice: !risesAbove ? triggerPrice : ocoTriggerPrice,
triggerTrailingPercentOffset: !risesAbove
? triggerTrailingPercentOffset
: ocoTriggerTrailingPercentOffset,
triggerType: !risesAbove ? triggerType : ocoTriggerType,
})}
</>
) : (
<>
{formatSizeAtPrice({
assetUnit,
decimalPlaces: market.decimalPlaces,
positionDecimalPlaces: market.positionDecimalPlaces,
price,
quoteName,
side,
size,
type,
})}
<br />
{t('Trigger')}{' '}
{formatTrigger({
decimalPlaces: market.decimalPlaces,
quoteName,
triggerDirection,
triggerPrice,
triggerTrailingPercentOffset,
triggerType,
})}
</>
);
return (
<Button
intent={side === Schema.Side.SIDE_BUY ? Intent.Success : Intent.Danger}
data-testid="place-order"
type="submit"
className="w-full"
subLabel={subLabel}
>
{t(
oco
? 'Place OCO stop order'
: type === Schema.OrderType.TYPE_MARKET
? 'Place market stop order'
: 'Place limit stop order'
)}
</Button>
);
};
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
const { pubKey, isReadOnly } = useVegaWallet();
const setType = useDealTicketFormValues((state) => state.setType);
@@ -521,50 +781,40 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
},
[market.id, market.decimalPlaces, market.positionDecimalPlaces, submit]
);
const side = watch('side');
const expire = watch('expire');
const triggerType = watch('triggerType');
const triggerPrice = watch('triggerPrice');
const timeInForce = watch('timeInForce');
const rawPrice = watch('price');
const rawSize = watch('size');
const oco = watch('oco');
const expiresAt = watch('expiresAt');
useEffect(() => {
const size = storedFormValues?.[dealTicketType]?.size;
if (size && rawSize !== size) {
setValue('size', size);
}
}, [storedFormValues, dealTicketType, rawSize, setValue]);
useEffect(() => {
const price = storedFormValues?.[dealTicketType]?.price;
if (price && rawPrice !== price) {
setValue('price', price);
}
}, [storedFormValues, dealTicketType, rawPrice, setValue]);
const isPriceTrigger = triggerType === 'price';
const size = removeDecimal(rawSize, market.positionDecimalPlaces);
const price =
marketPrice &&
getDerivedPrice(
{
type,
price: rawPrice && removeDecimal(rawPrice, market.decimalPlaces),
},
type === Schema.OrderType.TYPE_MARKET && isPriceTrigger && triggerPrice
? removeDecimal(triggerPrice, market.decimalPlaces)
: marketPrice
);
const notionalSize = getNotionalSize(
price,
size,
market.decimalPlaces,
market.positionDecimalPlaces
const oco = watch('oco');
const ocoPrice = watch('ocoPrice');
const ocoSize = watch('ocoSize');
const ocoTimeInForce = watch('ocoTimeInForce');
const ocoTriggerPrice = watch('ocoTriggerPrice');
const ocoTriggerTrailingPercentOffset = watch(
'ocoTriggerTrailingPercentOffset'
);
const ocoTriggerType = watch('ocoTriggerType');
const ocoType = watch('ocoType');
const price = watch('price');
const side = watch('side');
const size = watch('size');
const timeInForce = watch('timeInForce');
const triggerDirection = watch('triggerDirection');
const triggerPrice = watch('triggerPrice');
const triggerTrailingPercentOffset = watch('triggerTrailingPercentOffset');
const triggerType = watch('triggerType');
useEffect(() => {
const storedSize = storedFormValues?.[dealTicketType]?.size;
if (storedSize && size !== storedSize) {
setValue('size', storedSize);
}
}, [storedFormValues, dealTicketType, size, setValue]);
useEffect(() => {
const storedPrice = storedFormValues?.[dealTicketType]?.price;
if (storedPrice && price !== storedPrice) {
setValue('price', storedPrice);
}
}, [storedFormValues, dealTicketType, price, setValue]);
useEffect(() => {
const subscription = watch((value, { name, type }) => {
@@ -573,8 +823,10 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
return () => subscription.unsubscribe();
}, [watch, market.id, updateStoredFormValues]);
const { quoteName, settlementAsset: asset } =
market.tradableInstrument.instrument.product;
const { quoteName } = market.tradableInstrument.instrument.product;
const assetUnit = getAssetUnit(
market.tradableInstrument.instrument.metadata.tags
);
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const priceStep = toDecimal(market?.decimalPlaces);
@@ -584,6 +836,10 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
control,
});
const normalizedPrice = price && removeDecimal(price, market.decimalPlaces);
const normalizedSize =
size && removeDecimal(size, market.positionDecimalPlaces);
return (
<form
onSubmit={isReadOnly || !pubKey ? stopSubmit : handleSubmit(onSubmit)}
@@ -620,18 +876,34 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
control={control}
watch={watch}
priceStep={priceStep}
assetSymbol={asset.symbol}
quoteName={quoteName}
marketPrice={marketPrice}
decimalPlaces={market.decimalPlaces}
/>
<hr className="mb-4 border-vega-clight-500 dark:border-vega-cdark-500" />
<Size
control={control}
sizeStep={sizeStep}
isLimitType={type === Schema.OrderType.TYPE_LIMIT}
assetUnit={assetUnit}
/>
<Price
control={control}
watch={watch}
priceStep={priceStep}
quoteName={quoteName}
/>
<Size control={control} sizeStep={sizeStep} />
<NotionalAndFees
market={market}
marketPrice={marketPrice}
price={normalizedPrice}
side={side}
size={normalizedSize}
timeInForce={timeInForce}
triggerPrice={triggerPrice}
triggerType={triggerType}
type={type}
/>
<TimeInForce control={control} />
<div className="flex justify-end pb-3 gap-2">
<ReduceOnly />
@@ -682,12 +954,12 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
>
<Radio
value={Schema.OrderType.TYPE_MARKET}
id={`ocoTypeMarket`}
id="ocoTypeMarket"
label={'Market'}
/>
<Radio
value={Schema.OrderType.TYPE_LIMIT}
id={`ocoTypeLimit`}
id="ocoTypeLimit"
label={'Limit'}
/>
</RadioGroup>
@@ -699,12 +971,19 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
control={control}
watch={watch}
priceStep={priceStep}
assetSymbol={asset.symbol}
quoteName={quoteName}
marketPrice={marketPrice}
decimalPlaces={market.decimalPlaces}
oco
/>
<hr className="mb-2 border-vega-clight-500 dark:border-vega-cdark-500" />
<Size
control={control}
sizeStep={sizeStep}
assetUnit={assetUnit}
oco
isLimitType={ocoType === Schema.OrderType.TYPE_LIMIT}
/>
<Price
control={control}
watch={watch}
@@ -712,7 +991,19 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
quoteName={quoteName}
oco
/>
<Size control={control} sizeStep={sizeStep} oco />
<NotionalAndFees
market={market}
marketPrice={marketPrice}
price={ocoPrice && removeDecimal(ocoPrice, market.decimalPlaces)}
side={side}
size={
ocoSize && removeDecimal(ocoSize, market.positionDecimalPlaces)
}
timeInForce={ocoTimeInForce}
triggerPrice={ocoTriggerPrice}
triggerType={ocoTriggerType}
type={ocoType}
/>
<TimeInForce control={control} oco />
<div className="flex justify-end mb-2 gap-2">
<ReduceOnly />
@@ -728,11 +1019,12 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
return (
<Checkbox
onCheckedChange={(value) => {
const now = Date.now();
if (
value &&
(!expiresAt || new Date(expiresAt).getTime() < Date.now())
(!expiresAt || new Date(expiresAt).getTime() < now)
) {
setValue('expiresAt', formatForInput(new Date()), {
setValue('expiresAt', formatForInput(new Date(now)), {
shouldValidate: true,
});
}
@@ -803,19 +1095,24 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
</>
)}
<NoWalletWarning isReadOnly={isReadOnly} />
<DealTicketButton side={side} label={t('Submit Stop Order')} />
<DealTicketFeeDetails
order={{
marketId: market.id,
price: price || undefined,
side,
size,
timeInForce,
type,
}}
notionalSize={notionalSize}
assetSymbol={asset.symbol}
<SubmitButton
assetUnit={assetUnit}
market={market}
oco={oco}
ocoPrice={ocoPrice}
ocoSize={ocoSize}
ocoTriggerPrice={ocoTriggerPrice}
ocoTriggerTrailingPercentOffset={ocoTriggerTrailingPercentOffset}
ocoTriggerType={ocoTriggerType}
ocoType={ocoType}
price={price}
side={side}
size={size}
triggerDirection={triggerDirection}
triggerPrice={triggerPrice}
triggerTrailingPercentOffset={triggerTrailingPercentOffset}
triggerType={triggerType}
type={type}
/>
</form>
);
@@ -1,6 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { act, render, screen, waitFor } from '@testing-library/react';
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { generateMarket, generateMarketData } from '../../test-helpers';
import { DealTicket } from './deal-ticket';
@@ -14,6 +20,7 @@ import {
} from '../../hooks/use-form-values';
import * as positionsTools from '@vegaprotocol/positions';
import { OrdersDocument } from '@vegaprotocol/orders';
import { formatForInput } from '@vegaprotocol/utils';
jest.mock('zustand');
jest.mock('./deal-ticket-fee-details', () => ({
@@ -314,7 +321,7 @@ describe('DealTicket', () => {
expect(screen.getByTestId('iceberg')).toBeChecked();
});
it('should set values for a non-persistent iceberg order and disable post only checkbox', () => {
it('should set values for a non-persistent order and disable post only checkbox', () => {
const expectedOrder = {
marketId: market.id,
type: Schema.OrderType.TYPE_LIMIT,
@@ -357,6 +364,7 @@ describe('DealTicket', () => {
expect(screen.getByTestId('reduce-only')).not.toBeChecked();
expect(screen.getByTestId('post-only')).not.toBeChecked();
expect(screen.getByTestId('iceberg')).not.toBeChecked();
expect(screen.getByTestId('iceberg')).toBeDisabled();
});
// eslint-disable-next-line jest/no-disabled-tests
@@ -473,4 +481,150 @@ describe('DealTicket', () => {
Object.keys(Schema.OrderTimeInForce).length
);
});
it('validates size field', async () => {
render(generateJsx());
const sizeErrorMessage = 'deal-ticket-error-message-size';
const sizeInput = 'order-size';
await userEvent.click(screen.getByTestId('place-order'));
// default value should be invalid
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(screen.getByTestId(sizeInput), '0.01');
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(screen.getByTestId(sizeInput));
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
expect(screen.queryByTestId(sizeErrorMessage)).toBeNull();
});
it('validates price field', async () => {
const priceErrorMessage = 'deal-ticket-error-message-price';
const priceInput = 'order-price';
const submitButton = 'place-order';
const orderTypeMarket = 'order-type-Market';
const orderTypeLimit = 'order-type-Limit';
render(generateJsx());
await userEvent.click(screen.getByTestId(submitButton));
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
await userEvent.type(screen.getByTestId(priceInput), '0.001');
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
// switch to market order type error should disappear
await userEvent.click(screen.getByTestId(orderTypeMarket));
await userEvent.click(screen.getByTestId(submitButton));
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
// switch back to limit type
await userEvent.click(screen.getByTestId(orderTypeLimit));
await userEvent.click(screen.getByTestId(submitButton));
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
// to small value should be invalid
await userEvent.type(screen.getByTestId(priceInput), '0.001');
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
// clear and fill using valid value
await userEvent.clear(screen.getByTestId(priceInput));
await userEvent.type(screen.getByTestId(priceInput), '0.01');
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
});
it('validates iceberg field', async () => {
const peakSizeErrorMessage = 'deal-ticket-peak-error-message';
const minimumSizeErrorMessage = 'deal-ticket-minimum-error-message';
const sizeInput = 'order-size';
const peakSizeInput = 'order-peak-size';
const minimumSizeInput = 'order-minimum-size';
const submitButton = 'place-order';
render(generateJsx());
await userEvent.selectOptions(
screen.getByTestId('order-tif'),
Schema.OrderTimeInForce.TIME_IN_FORCE_GFA
);
await userEvent.click(screen.getByTestId('iceberg'));
await userEvent.click(screen.getByTestId(submitButton));
// validate empty fields
expect(screen.getByTestId(peakSizeErrorMessage)).toBeInTheDocument();
expect(screen.getByTestId(minimumSizeErrorMessage)).toBeInTheDocument();
await userEvent.type(screen.getByTestId(peakSizeInput), '0.01');
await userEvent.type(screen.getByTestId(minimumSizeInput), '0.01');
// validate value smaller than step
expect(screen.getByTestId(peakSizeErrorMessage)).toBeInTheDocument();
expect(screen.getByTestId(minimumSizeErrorMessage)).toBeInTheDocument();
await userEvent.clear(screen.getByTestId(peakSizeInput));
await userEvent.type(screen.getByTestId(peakSizeInput), '0.5');
await userEvent.clear(screen.getByTestId(minimumSizeInput));
await userEvent.type(screen.getByTestId(minimumSizeInput), '0.7');
await userEvent.clear(screen.getByTestId(sizeInput));
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
// validate value higher than size
expect(screen.getByTestId(peakSizeErrorMessage)).toBeInTheDocument();
expect(screen.getByTestId(minimumSizeErrorMessage)).toBeInTheDocument();
await userEvent.clear(screen.getByTestId(sizeInput));
await userEvent.type(screen.getByTestId(sizeInput), '1');
// validate peak higher than minimum
expect(screen.queryByTestId(peakSizeErrorMessage)).toBeNull();
expect(screen.getByTestId(minimumSizeErrorMessage)).toBeInTheDocument();
await userEvent.clear(screen.getByTestId(peakSizeInput));
await userEvent.type(screen.getByTestId(peakSizeInput), '1');
await userEvent.clear(screen.getByTestId(minimumSizeInput));
await userEvent.type(screen.getByTestId(minimumSizeInput), '1');
// validate correct values
expect(screen.queryByTestId(peakSizeErrorMessage)).toBeNull();
expect(screen.queryByTestId(minimumSizeErrorMessage)).toBeNull();
});
it('sets expiry time/date to now if expiry is changed to checked', async () => {
const datePicker = 'date-picker-field';
const now = Math.round(Date.now() / 1000) * 1000;
render(generateJsx());
jest.spyOn(global.Date, 'now').mockImplementationOnce(() => now);
await userEvent.selectOptions(
screen.getByTestId('order-tif'),
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
);
// expiry time/date was empty it should be set to now
expect(
new Date(screen.getByTestId<HTMLInputElement>(datePicker).value).getTime()
).toEqual(now);
// set to the value in the past (now - 1s)
fireEvent.change(screen.getByTestId<HTMLInputElement>(datePicker), {
target: { value: formatForInput(new Date(now - 1000)) },
});
expect(
new Date(
screen.getByTestId<HTMLInputElement>(datePicker).value
).getTime() + 1000
).toEqual(now);
// switch expiry off and on
await userEvent.selectOptions(
screen.getByTestId('order-tif'),
Schema.OrderTimeInForce.TIME_IN_FORCE_GFA
);
await userEvent.selectOptions(
screen.getByTestId('order-tif'),
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
);
// expiry time/date was in the past it should be set to now
expect(
new Date(screen.getByTestId<HTMLInputElement>(datePicker).value).getTime()
).toEqual(now);
});
});
@@ -3,7 +3,6 @@ import * as Schema from '@vegaprotocol/types';
import type { FormEventHandler } from 'react';
import { memo, useCallback, useEffect, useRef, useMemo } from 'react';
import { Controller, useController, useForm } from 'react-hook-form';
import { DealTicketButton } from './deal-ticket-button';
import {
DealTicketFeeDetails,
DealTicketMarginDetails,
@@ -23,6 +22,8 @@ import {
Intent,
Notification,
Tooltip,
TradingButton as Button,
Pill,
} from '@vegaprotocol/ui-toolkit';
import {
@@ -35,6 +36,7 @@ import {
validateAmount,
toDecimal,
formatForInput,
formatValue,
} from '@vegaprotocol/utils';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import { getDerivedPrice } from '@vegaprotocol/markets';
@@ -46,7 +48,10 @@ import {
validateType,
} from '../../utils';
import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error';
import { SummaryValidationType } from '../../constants';
import {
NOTIONAL_SIZE_TOOLTIP_TEXT,
SummaryValidationType,
} from '../../constants';
import type {
Market,
MarketData,
@@ -68,6 +73,7 @@ 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';
import { KeyValue } from './key-value';
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.';
@@ -118,6 +124,11 @@ const getDefaultValues = (
...storedValues,
});
export const getAssetUnit = (tags?: string[] | null) =>
tags
?.find((tag) => tag.startsWith('base:') || tag.startsWith('ticker:'))
?.replace(/^[^:]*:/, '');
export const DealTicket = ({
market,
onMarketClick,
@@ -257,6 +268,10 @@ export const DealTicket = ({
const assetSymbol =
market.tradableInstrument.instrument.product.settlementAsset.symbol;
const assetUnit = getAssetUnit(
market.tradableInstrument.instrument.metadata.tags
);
const summaryError = useMemo(() => {
if (!pubKey) {
return {
@@ -338,6 +353,7 @@ export const DealTicket = ({
const priceStep = toDecimal(market?.decimalPlaces);
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const quoteName = market.tradableInstrument.instrument.product.quoteName;
const isLimitType = type === Schema.OrderType.TYPE_LIMIT;
return (
<form
@@ -384,18 +400,16 @@ export const DealTicket = ({
message: t('Size cannot be lower than ' + sizeStep),
},
validate: validateAmount(sizeStep, 'Size'),
deps: ['peakSize', 'minimumVisibleSize'],
}}
render={({ field, fieldState }) => (
<div className="mb-4">
<FormGroup
label={t('Size')}
labelFor="input-order-size-limit"
compact
>
<div className={isLimitType ? 'mb-4' : 'mb-2'}>
<FormGroup label={t('Size')} labelFor="order-size" compact>
<Input
id="input-order-size-limit"
id="order-size"
className="w-full"
type="number"
appendElement={assetUnit && <Pill size="xs">{assetUnit}</Pill>}
step={sizeStep}
min={sizeStep}
data-testid="order-size"
@@ -411,7 +425,7 @@ export const DealTicket = ({
</div>
)}
/>
{type === Schema.OrderType.TYPE_LIMIT && (
{isLimitType && (
<Controller
name="price"
control={control}
@@ -424,14 +438,15 @@ export const DealTicket = ({
validate: validateAmount(priceStep, 'Price'),
}}
render={({ field, fieldState }) => (
<div className="mb-4">
<div className="mb-2">
<FormGroup
labelFor="input-price-quote"
label={t(`Price (${quoteName})`)}
label={t('Price')}
compact
>
<Input
id="input-price-quote"
appendElement={<Pill size="xs">{quoteName}</Pill>}
className="w-full"
type="number"
step={priceStep}
@@ -449,6 +464,22 @@ export const DealTicket = ({
)}
/>
)}
<div className="mb-4">
<KeyValue
label={t('Notional')}
value={formatValue(notionalSize, market.decimalPlaces)}
formattedValue={formatValue(notionalSize, market.decimalPlaces)}
symbol={quoteName}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
/>
<DealTicketFeeDetails
order={
normalizedOrder && { ...normalizedOrder, price: price || undefined }
}
assetSymbol={assetSymbol}
market={market}
/>
</div>
<Controller
name="timeInForce"
control={control}
@@ -475,7 +506,7 @@ export const DealTicket = ({
}
// iceberg orders must be persistent orders, so if user
// switches to to a non persisten tif value, remove iceberg selection
// switches to a non persistent tif value, remove iceberg selection
if (iceberg && isNonPersistentOrder(value)) {
setValue('iceberg', false);
}
@@ -487,7 +518,7 @@ export const DealTicket = ({
/>
)}
/>
{type === Schema.OrderType.TYPE_LIMIT &&
{isLimitType &&
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
<Controller
name="expiresAt"
@@ -569,7 +600,7 @@ export const DealTicket = ({
)}
/>
</div>
{type === Schema.OrderType.TYPE_LIMIT && (
{isLimitType && (
<>
<div className="flex justify-between pb-2 gap-2">
<Controller
@@ -624,15 +655,29 @@ export const DealTicket = ({
pubKey={pubKey}
onDeposit={onDeposit}
/>
<DealTicketButton side={side} />
<DealTicketFeeDetails
order={
normalizedOrder && { ...normalizedOrder, price: price || undefined }
}
notionalSize={notionalSize}
assetSymbol={assetSymbol}
market={market}
/>
<Button
data-testid="place-order"
type="submit"
className="w-full"
intent={side === Schema.Side.SIDE_BUY ? Intent.Success : Intent.Danger}
subLabel={`${formatValue(
normalizedOrder.size,
market.positionDecimalPlaces
)} ${assetUnit} @ ${
type === Schema.OrderType.TYPE_MARKET
? 'market'
: `${formatValue(
normalizedOrder.price,
market.decimalPlaces
)} ${quoteName}`
}`}
>
{t(
type === Schema.OrderType.TYPE_MARKET
? 'Place market order'
: 'Place limit order'
)}
</Button>
<DealTicketMarginDetails
onMarketClick={onMarketClick}
assetSymbol={assetSymbol}
@@ -0,0 +1,51 @@
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import classnames from 'classnames';
import type { ReactNode } from 'react';
export interface KeyValuePros {
label: string;
value?: string | null | undefined;
symbol: string;
indent?: boolean | undefined;
labelDescription?: ReactNode;
formattedValue?: string;
onClick?: () => void;
}
export const KeyValue = ({
label,
value,
labelDescription,
symbol,
indent,
onClick,
formattedValue,
}: KeyValuePros) => {
const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`;
const valueElement = onClick ? (
<button onClick={onClick} className="text-muted">
{displayValue}
</button>
) : (
<div className="text-muted">{displayValue}</div>
);
return (
<div
data-testid={
'deal-ticket-fee-' + label.toLocaleLowerCase().replace(/\s/g, '-')
}
key={typeof label === 'string' ? label : 'value-dropdown'}
className={classnames(
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
{ 'ml-2': indent }
)}
>
<Tooltip description={labelDescription}>
<div>{label}</div>
</Tooltip>
<Tooltip description={`${value ?? '-'} ${symbol || ''}`}>
{valueElement}
</Tooltip>
</div>
);
};
+15 -2
View File
@@ -2,7 +2,11 @@ import type { Asset } from '@vegaprotocol/assets';
import { EtherscanLink } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { Intent, Notification } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
import {
formatNumber,
getUnlimitedThreshold,
quantumDecimalPlaces,
} from '@vegaprotocol/utils';
import type { EthStoredTxState } from '@vegaprotocol/web3';
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
@@ -188,6 +192,15 @@ const ApprovalTxFeedback = ({
}
if (tx.status === EthTxStatus.Confirmed) {
const approvedAllowanceValue = (
allowance || new BigNumber(0)
).isGreaterThan(getUnlimitedThreshold(selectedAsset.decimals))
? '∞'
: formatNumber(
allowance?.toString() || 0,
quantumDecimalPlaces(selectedAsset.quantum, selectedAsset.decimals)
);
return (
<div className="mb-4">
<Notification
@@ -198,7 +211,7 @@ const ApprovalTxFeedback = ({
<p>
{t('You approved deposits of up to %s %s.', [
selectedAsset?.symbol,
formatNumber(allowance?.toString() || 0),
approvedAllowanceValue,
])}
</p>
{txLink && <p>{txLink}</p>}
+3 -2
View File
@@ -15,6 +15,7 @@ import { useWeb3ConnectStore } from '@vegaprotocol/web3';
import { useWeb3React } from '@web3-react/core';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import type { DepositBalances } from './use-deposit-balances';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
jest.mock('@vegaprotocol/wallet');
jest.mock('@vegaprotocol/web3');
@@ -90,7 +91,7 @@ describe('Deposit form', () => {
// Assert default values (including) from/to provided by useVegaWallet and useWeb3React
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
MOCK_ETH_ADDRESS
truncateMiddle(MOCK_ETH_ADDRESS)
);
expect(screen.getByLabelText('Asset')).toHaveValue('');
expect(screen.getByLabelText('To (Vega key)')).toHaveValue('');
@@ -353,7 +354,7 @@ describe('Deposit form', () => {
).not.toBeInTheDocument();
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
MOCK_ETH_ADDRESS
truncateMiddle(MOCK_ETH_ADDRESS)
);
});
+2 -1
View File
@@ -22,6 +22,7 @@ import {
Intent,
ButtonLink,
TradingSelect,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core';
@@ -173,7 +174,7 @@ export const DepositForm = ({
return (
<div className="text-sm" aria-describedby="ethereum-address">
<p className="mb-1 break-all" data-testid="ethereum-address">
{account}
{truncateMiddle(account)}
</p>
<DisconnectEthereumButton
onDisconnect={() => {
+10 -4
View File
@@ -7,7 +7,7 @@ import {
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import type BigNumber from 'bignumber.js';
import { formatNumber } from '@vegaprotocol/utils';
import { formatNumber, quantumDecimalPlaces } from '@vegaprotocol/utils';
// Note: all of the values here are with correct asset's decimals
// See `libs/deposits/src/lib/use-deposit-balances.ts`
@@ -35,7 +35,10 @@ export const DepositLimits = ({
label: t('Balance available'),
rawValue: balance,
value: balance ? (
<CompactNumber number={balance} decimals={asset.decimals} />
<CompactNumber
number={balance}
decimals={quantumDecimalPlaces(asset.quantum, asset.decimals)}
/>
) : (
'-'
),
@@ -73,7 +76,7 @@ export const DepositLimits = ({
value: !exempt ? (
<CompactNumber
number={max.minus(deposited)}
decimals={asset.decimals}
decimals={quantumDecimalPlaces(asset.quantum, asset.decimals)}
/>
) : (
<div data-testid="exempt">{t('Exempt')}</div>
@@ -97,7 +100,10 @@ export const DepositLimits = ({
),
rawValue: allowance,
value: allowance ? (
<CompactNumber number={allowance} decimals={asset.decimals} />
<CompactNumber
number={allowance}
decimals={quantumDecimalPlaces(asset.quantum, asset.decimals)}
/>
) : (
'-'
),
+10 -1
View File
@@ -1,9 +1,14 @@
import { BigNumber } from 'bignumber.js';
import { getUserLocale, formatNumber } from '@vegaprotocol/utils';
import {
getUserLocale,
formatNumber,
getUnlimitedThreshold,
} from '@vegaprotocol/utils';
const INFINITY = '∞';
const DEFAULT_COMPACT_ABOVE = 1_000_000;
const DEFAULT_COMPACT_CAP = new BigNumber(1e24);
/**
* Compacts given number to human readable format.
* @param number
@@ -35,6 +40,10 @@ export const CompactNumber = ({
const decimalPlaces =
(decimals === 'infer' ? number.decimalPlaces() : decimals) || 0;
if (number.isGreaterThan(getUnlimitedThreshold(decimalPlaces))) {
return <span data-testid={testId}>{INFINITY}</span>;
}
if (number.isLessThan(DEFAULT_COMPACT_ABOVE)) {
return (
<span data-testid={testId}>{formatNumber(number, decimalPlaces)}</span>
@@ -80,13 +80,14 @@ const getAffixElement = ({
appendIconName,
appendIconDescription,
}: Pick<TradingInputProps, keyof AffixProps>) => {
const position = prependIconName || prependElement ? 'pre' : 'post';
const className = classNames(
['fill-black dark:fill-white', 'absolute', 'z-10'],
'absolute z-10 top-0 bottom-0 flex items-center',
{
'left-3': position === 'pre',
'right-3': position === 'post',
'fill-black dark:fill-white': prependIconName || appendIconName,
'left-3': prependIconName,
'right-3': appendIconName,
'left-1': prependElement,
'right-1': appendElement,
}
);
@@ -161,7 +162,7 @@ export const TradingInput = forwardRef<HTMLInputElement, TradingInputProps>(
if (element) {
return (
<div className="flex items-center relative">
<div className="relative">
{hasPrepended && element}
{input}
{hasAppended && element}
+62
View File
@@ -5,7 +5,9 @@ import {
addDecimalsFormatNumberQuantum,
formatNumber,
formatNumberPercentage,
getUnlimitedThreshold,
isNumeric,
quantumDecimalPlaces,
toDecimal,
toNumberParts,
} from './number';
@@ -156,3 +158,63 @@ describe('number utils', () => {
});
});
});
describe('quantumDecimalPlaces', () => {
it.each([
['1', 1, 3],
['10', 1, 2],
['100', 1, 1],
['1000', 1, 0],
['1', 2, 4],
['10', 2, 3],
['100', 2, 2],
['1000', 2, 1],
['1', 3, 5],
['10', 3, 4],
['100', 3, 3],
['1000', 3, 2],
['1', 18, 20],
['1000000000', 18, 11],
['5000000000', 18, 11],
['1000000000000000000', 18, 2],
])(
'converts quantum %s of %d decimal places to %d quant. decimal places',
(quantum, decimals, output) => {
expect(quantumDecimalPlaces(quantum, decimals)).toEqual(output);
}
);
});
describe('getUnlimitedThreshold', () => {
it.each([
[
0,
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+76',
],
[
1,
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+75',
],
[
2,
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+74',
],
[
3,
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+73',
],
[
10,
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+66',
],
[
18,
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+58',
],
])(
'given %d decimal places it returns unlimited threshold %s',
(decimals, output) => {
expect(getUnlimitedThreshold(decimals).toString()).toEqual(output);
}
);
});
+31
View File
@@ -4,6 +4,18 @@ import memoize from 'lodash/memoize';
import { getUserLocale } from '../get-user-locale';
/**
* A raw unformatted value greater than this is considered and displayed
* as UNLIMITED.
*/
export const UNLIMITED_THRESHOLD = new BigNumber(2).pow(256).times(0.8);
/**
* Gets the unlimited threshold value for given decimal places.
* @param decimalPlaces the asset's decimal places
*/
export const getUnlimitedThreshold = (decimalPlaces: number) =>
UNLIMITED_THRESHOLD.dividedBy(Math.pow(10, decimalPlaces));
const MIN_FRACTION_DIGITS = 2;
const MAX_FRACTION_DIGITS = 20;
@@ -90,6 +102,25 @@ export const formatNumberFixed = (
return getFixedNumberFormat(formatDecimals).format(Number(rawValue));
};
export const quantumDecimalPlaces = (
/** Raw asset's quantum value */
rawQuantum: number | string,
/** Asset's decimal places */
decimalPlaces: number
) => {
// if raw quantum value is an empty string then it'll evaluate to 0
// this check ignores NaNs and zeroes
const formatDecimals =
isNaN(Number(rawQuantum)) || Number(rawQuantum) === 0
? decimalPlaces
: Math.max(
0,
Math.log10(100 / Number(addDecimal(rawQuantum, decimalPlaces)))
);
return Math.ceil(formatDecimals);
};
export const addDecimalsFormatNumberQuantum = (
rawValue: string | number,
decimalPlaces: number,