Compare commits

..
Author SHA1 Message Date
Matthew Russell e0316cbd9a fix: remove hover state from disabled view as button 2023-08-03 18:24:13 +01:00
170 changed files with 3216 additions and 6641 deletions
+1 -1
View File
@@ -137,7 +137,7 @@ jobs:
secrets: inherit
with:
projects: ${{ needs.lint-test-build.outputs.projects-e2e }}
tags: '@smoke'
tags: '@smoke @regression'
publish-dist:
needs: lint-test-build
+3 -3
View File
@@ -10,8 +10,8 @@ on:
jobs:
run-tests:
name: run-tests
runs-on: 8-cores
timeout-minutes: 20
runs-on: ubuntu-22.04
timeout-minutes: 25
steps:
#----------------------------------------------
# check-out frontend-monorepo
@@ -125,7 +125,7 @@ jobs:
#----------------------------------------------
- name: Run tests
working-directory: ./console-test
run: poetry run pytest -s --numprocesses auto
run: poetry run pytest --numprocesses auto
- name: Check files
run: |
ls -al .
-1
View File
@@ -23,7 +23,6 @@ module.exports = defineConfig({
viewportWidth: 1440,
viewportHeight: 900,
testIsolation: false,
experimentalMemoryManagement: true,
},
env: {
environment: 'CUSTOM',
@@ -23,7 +23,7 @@ export const Footer = () => {
);
return (
<footer className="grid grid-cols-[1fr_auto] items-center text-xs md:text-md lg:flex md:col-span-2 px-4 pt-2 pb-3 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
<footer className="grid grid-rows-2 lg:grid-cols-[1fr_auto] text-xs md:text-md lg:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
<div className="flex justify-between gap-2 align-middle">
{GIT_COMMIT_HASH && (
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
@@ -43,12 +43,9 @@ export const Footer = () => {
</div>
)}
<div className="content-center flex pr-4 md:border-r border-neutral-700 dark:border-neutral-300">
<span className="pr-2">{VEGA_URL && <NodeUrl url={VEGA_URL} />}</span>
<Link
className="ml-2 underline-offset-4"
onClick={() => setNodeSwitcherOpen(true)}
>
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
<Link className="ml-2" onClick={() => setNodeSwitcherOpen(true)}>
{t('Change')}
</Link>
</div>
@@ -62,10 +59,7 @@ export const Footer = () => {
) : null}
</div>
<div className="pl-2 align-center lg:align-right lg:flex lg:justify-end gap-2 align-middle lg:max-w-xs lg:ml-auto">
<RouteLink
to={`/${Routes.DISCLAIMER}`}
className="underline underline-offset-4"
>
<RouteLink to={`/${Routes.DISCLAIMER}`} className="underline">
Disclaimer
</RouteLink>
</div>
-1
View File
@@ -28,7 +28,6 @@ module.exports = defineConfig({
numTestsKeptInMemory: 5,
downloadsFolder: 'cypress/downloads',
testIsolation: false,
experimentalMemoryManagement: true,
},
env: {
ethProviderUrl: 'http://localhost:8545/',
@@ -730,7 +730,6 @@ context(
.getByTestId('key-value-table-row')
.contains(heading)
.parent()
.parent()
.siblings();
}
}
@@ -96,8 +96,7 @@ context(
});
});
// eslint-disable-next-line
it.skip(
it(
'Able to withdraw asset: -eth wallet connected -withdraw funds button',
{ tags: '@smoke' },
function () {
@@ -231,7 +231,7 @@ context(
});
// 3009-NTWU-001 3009-NTWU-002 3009-NTWU-006 3009-NTWU-009
it.skip('should display network upgrade banner with estimate', function () {
it('should display network upgrade banner with estimate', function () {
mockNetworkUpgradeProposal();
cy.visit('/');
cy.getByTestId('banners').within(() => {
@@ -1,8 +1,4 @@
import {
VegaConnectDialog,
VegaManageDialog,
ViewAsDialog,
} from '@vegaprotocol/wallet';
import { VegaConnectDialog, VegaManageDialog } from '@vegaprotocol/wallet';
import {
AppStateActionType,
useAppState,
@@ -29,8 +25,6 @@ export const VegaWalletDialogs = () => {
})
}
/>
<ViewAsDialog connector={Connectors.view} />
</>
);
};
@@ -0,0 +1,17 @@
import { Link } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import { ExternalLinks } from '@vegaprotocol/environment';
export const DownloadWalletPrompt = () => {
const { t } = useTranslation();
return (
<>
<h3 className="mt-4 mb-2">{t('getWallet')}</h3>
<p>
<Link className="text-neutral-500" href={ExternalLinks.VEGA_WALLET_URL}>
{t('getWalletLink')}
</Link>
</p>
</>
);
};
@@ -1,25 +0,0 @@
import { ButtonLink, Link } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import { ExternalLinks } from '@vegaprotocol/environment';
import { useViewAsDialog } from '@vegaprotocol/wallet';
export const VegaWalletPrompt = () => {
const { t } = useTranslation();
const setViewAsDialog = useViewAsDialog((state) => state.setOpen);
return (
<>
<h3 className="mt-4 mb-2">{t('getWallet')}</h3>
<div className="flex flex-row gap-4">
<Link className="text-neutral-500" href={ExternalLinks.VEGA_WALLET_URL}>
{t('getWalletLink')}
</Link>
<ButtonLink
className="text-neutral-500"
onClick={() => setViewAsDialog(true)}
>
{t('viewAsParty')}
</ButtonLink>
</div>
</>
);
};
@@ -20,7 +20,7 @@ import {
WalletCardHeader,
WalletCardRow,
} from '../wallet-card';
import { VegaWalletPrompt } from './vega-wallet-prompt';
import { DownloadWalletPrompt } from './download-wallet-prompt';
import { usePollForDelegations } from './hooks';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Button, ButtonLink } from '@vegaprotocol/ui-toolkit';
@@ -85,7 +85,7 @@ const VegaWalletNotConnected = () => {
>
{t('connectVegaWalletToUseAssociated')}
</Button>
<VegaWalletPrompt />
<DownloadWalletPrompt />
</>
);
};
@@ -855,6 +855,5 @@
"Estimated time to upgrade": "Estimated time to upgrade",
"Upgraded at": "Upgraded at",
"dataIsIdentical": "Data is identical",
"updatesToMarket": "Updates to market",
"viewAsParty": "View as party"
"updatesToMarket": "Updates to market"
}
@@ -45,10 +45,8 @@ export const useMarketDataDialogStore = create<MarketDataDialogState>(
export const ProposalMarketData = ({
marketData,
parentMarketData,
}: {
marketData: MarketInfoWithData;
parentMarketData?: MarketInfoWithData;
}) => {
const { t } = useTranslation();
const { isOpen, open, close } = useMarketDataDialogStore();
@@ -60,21 +58,8 @@ export const ProposalMarketData = ({
const settlementData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForSettlementData.data as DataSourceDefinition;
const parentSettlementData =
parentMarketData?.tradableInstrument.instrument?.product
?.dataSourceSpecForSettlementData?.data;
const terminationData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForTradingTermination.data as DataSourceDefinition;
const parentTerminationData =
parentMarketData?.tradableInstrument.instrument?.product
?.dataSourceSpecForTradingTermination?.data;
const isParentSettlementDataEqual =
parentSettlementData !== undefined &&
isEqual(settlementData, parentSettlementData);
const isParentTerminationDataEqual =
parentTerminationData !== undefined &&
isEqual(terminationData, parentTerminationData);
const getSigners = (data: DataSourceDefinition) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
@@ -112,22 +97,12 @@ export const ProposalMarketData = ({
<AccordionItem
itemId="key-details"
title={t('Key details')}
content={
<KeyDetailsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
content={<KeyDetailsInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="instrument"
title={t('Instrument')}
content={
<InstrumentInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
content={<InstrumentInfoPanel market={marketData} />}
/>
{isEqual(
getSigners(settlementData),
@@ -140,11 +115,6 @@ export const ProposalMarketData = ({
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual
? undefined
: parentMarketData
}
/>
}
/>
@@ -157,11 +127,6 @@ export const ProposalMarketData = ({
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual
? undefined
: parentMarketData
}
/>
}
/>
@@ -170,21 +135,11 @@ export const ProposalMarketData = ({
itemId="termination-oracle"
title={t('Termination Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="termination"
parentMarket={
isParentTerminationDataEqual
? undefined
: parentMarketData
}
/>
<OracleInfoPanel market={marketData} type="termination" />
}
/>
</>
)}
{/*Note: successor markets will not differ in their settlement*/}
{/*assets, so no need to pass in parent market data for comparison.*/}
<AccordionItem
itemId="settlement-asset"
title={t('Settlement asset')}
@@ -193,42 +148,22 @@ export const ProposalMarketData = ({
<AccordionItem
itemId="metadata"
title={t('Metadata')}
content={
<MetadataInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
content={<MetadataInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-model"
title={t('Risk model')}
content={
<RiskModelInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
content={<RiskModelInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-parameters"
title={t('Risk parameters')}
content={
<RiskParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
content={<RiskParametersInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-factors"
title={t('Risk factors')}
content={
<RiskFactorsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
content={<RiskFactorsInfoPanel market={marketData} />}
/>
{(
marketData.priceMonitoringSettings?.parameters?.triggers || []
@@ -239,7 +174,6 @@ export const ProposalMarketData = ({
content={
<PriceMonitoringBoundsInfoPanel
market={marketData}
parentMarket={parentMarketData}
triggerIndex={triggerIndex}
/>
}
@@ -249,21 +183,13 @@ export const ProposalMarketData = ({
itemId="liqudity-monitoring-parameters"
title={t('Liquidity monitoring parameters')}
content={
<LiquidityMonitoringParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<LiquidityMonitoringParametersInfoPanel market={marketData} />
}
/>
<AccordionItem
itemId="liquidity-price-range"
title={t('Liquidity price range')}
content={
<LiquidityPriceRangeInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
content={<LiquidityPriceRangeInfoPanel market={marketData} />}
/>
</Accordion>
</div>
@@ -32,7 +32,6 @@ export interface ProposalProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
networkParams: Partial<NetworkParamsResult>;
newMarketData?: MarketInfoWithData | null;
parentMarketData?: MarketInfoWithData | null;
assetData?: AssetQuery | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
restData: any;
@@ -47,7 +46,6 @@ export const Proposal = ({
networkParams,
restData,
newMarketData,
parentMarketData,
assetData,
originalMarketProposalRestData,
mostRecentlyEnactedAssociatedMarketProposal,
@@ -159,10 +157,7 @@ export const Proposal = ({
{newMarketData && (
<div className="mb-4">
<ProposalMarketData
marketData={newMarketData}
parentMarketData={parentMarketData ? parentMarketData : undefined}
/>
<ProposalMarketData marketData={newMarketData} />
</div>
)}
@@ -14,9 +14,6 @@ import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { useParentMarketIdQuery } from '@vegaprotocol/markets';
import { FLAGS } from '@vegaprotocol/environment';
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
export const ProposalContainer = () => {
const [
@@ -57,10 +54,6 @@ export const ProposalContainer = () => {
skip: !params.proposalId,
});
const successor = useSuccessorMarketProposalDetails(params.proposalId);
const isSuccessor = !!successor?.parentMarketId || !!successor.code;
const {
state: {
data: originalMarketProposalRestData,
@@ -103,36 +96,6 @@ export const ProposalContainer = () => {
},
});
const {
data: parentMarketId,
loading: parentMarketIdLoading,
error: parentMarketIdError,
} = useParentMarketIdQuery({
variables: {
marketId: newMarketData?.data?.market?.id || '',
},
skip:
!FLAGS.SUCCESSOR_MARKETS ||
!isSuccessor ||
!newMarketData?.data?.market?.id,
});
const {
data: parentMarketData,
loading: parentMarketLoading,
error: parentMarketError,
} = useDataProvider({
dataProvider: marketInfoWithDataProvider,
skipUpdates: true,
variables: {
marketId: parentMarketId?.market?.parentMarketID || '',
skip:
!FLAGS.SUCCESSOR_MARKETS ||
!isSuccessor ||
!parentMarketId?.market?.parentMarketID,
},
});
const {
data: assetData,
loading: assetLoading,
@@ -197,8 +160,6 @@ export const ProposalContainer = () => {
newMarketLoading ||
assetLoading ||
networkParamsLoading ||
parentMarketIdLoading ||
parentMarketLoading ||
(restLoading ? (restLoading as boolean) : false) ||
(originalMarketProposalRestLoading
? (originalMarketProposalRestLoading as boolean)
@@ -211,18 +172,15 @@ export const ProposalContainer = () => {
error ||
newMarketError ||
assetError ||
networkParamsError ||
parentMarketIdError ||
parentMarketError ||
restError ||
originalMarketProposalRestError ||
previouslyEnactedMarketProposalsRestError
previouslyEnactedMarketProposalsRestError ||
networkParamsError
}
data={{
...data,
...networkParams,
...(newMarketData ? { newMarketData } : {}),
...(parentMarketData ? { parentMarketData } : {}),
...(assetData ? { assetData } : {}),
...(restData ? { restData } : {}),
...(originalMarketProposalRestData
@@ -239,7 +197,6 @@ export const ProposalContainer = () => {
networkParams={networkParams}
restData={restData}
newMarketData={newMarketData}
parentMarketData={parentMarketData}
assetData={assetData}
originalMarketProposalRestData={originalMarketProposalRestData}
mostRecentlyEnactedAssociatedMarketProposal={
-1
View File
@@ -26,7 +26,6 @@ module.exports = defineConfig({
requestTimeout: 20000,
retries: 1,
testIsolation: false,
experimentalMemoryManagement: true,
},
env: {
ETHERSCAN_URL: 'https://sepolia.etherscan.io',
@@ -5,11 +5,12 @@ describe('charts', { tags: '@smoke' }, () => {
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId('Depth').click();
});
it('can see market depth chart', () => {
// 6006-DEPC-001
cy.getByTestId('Depth').click();
cy.getByTestId('tab-depth').should('be.visible');
cy.get('.depth-chart-module_canvas__260De').should('be.visible');
});
});
@@ -104,11 +104,11 @@ describe('deposit actions', { tags: '@smoke' }, () => {
cy.visit('/#/markets/market-1');
});
it('Deposit to trade is visible', () => {
it('Deposit to trade is visble', () => {
cy.getByTestId('Collateral').click();
cy.get('[row-id="asset-id"]').contains('tEURO').should('be.visible');
cy.contains('[data-testid="deposit"]', 'Deposit').should('be.visible');
cy.contains('[data-testid="deposit"]', 'Deposit').click();
cy.contains('[data-testid="deposit"]', 'Deposit')
.should('be.visible')
.click();
cy.getByTestId('deposit-submit').should('be.visible');
});
});
@@ -155,10 +155,7 @@ describe(
{ name: 'Volume', infoText: 'Volume: 55,000' },
];
cy.get(indicatorInfo).eq(1).realHover();
cy.get('.chart__wrapper [data-testid="split-view-view"]')
.last()
.find('[role="button"][title="Close"]')
.click({ force: true });
cy.get('.close-button-module_closeButton__2ifkl').click({ force: true });
cy.get(indicatorInfo).should('have.length', 1);
checkMenuItemCheckbox('Studies', studyInfo);
@@ -22,8 +22,9 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
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('get-started-button').should('exist');
cy.getByTestId('deal-ticket-connect-wallet').should('exist');
});
it('must be able to select order direction - long/short', function () {
@@ -44,7 +45,7 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
mockConnectWallet();
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('101');
cy.getByTestId('get-started-button').click();
cy.getByTestId('order-connect-wallet').click();
cy.getByTestId('dialog-content').should('be.visible');
cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]')
@@ -63,7 +63,6 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderPriceField).clear().type('1.123456');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('deal-ticket-error-message-price-limit').should(
'have.text',
'Price accepts up to 5 decimal places'
@@ -74,7 +73,6 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
describe('market order', () => {
before(() => {
cy.getByTestId(toggleMarket).click();
cy.getByTestId(placeOrderBtn).click();
});
it('must not see the price unit', function () {
@@ -48,7 +48,6 @@ describe('suspended market validation', { tags: '@regression' }, () => {
cy.getByTestId(orderPriceField).clear().type('0.1');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('deal-ticket-warning-auction').should(
'have.text',
'Any orders placed now will not trade until the auction ends'
@@ -61,7 +60,6 @@ describe('suspended market validation', { tags: '@regression' }, () => {
TIFlist.filter((item) => item.code === 'FOK')[0].value
);
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('deal-ticket-error-message-tif').should(
'have.text',
'This market is in auction until it reaches sufficient liquidity. Until the auction ends, you can only place GFA, GTT, or GTC limit orders'
@@ -53,7 +53,7 @@ describe('fills', { tags: '@regression' }, () => {
cy.getByTestId(tabFills).contains('Market');
cy.getByTestId(tabFills)
.get(
'[role="gridcell"][col-id="market.tradableInstrument.instrument.code"]'
'[role="gridcell"][col-id="market.tradableInstrument.instrument.name"]'
)
.each(($marketSymbol) => {
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
@@ -27,8 +27,7 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
validatePositionsDisplayed();
});
// TODO: move this to sim, its flakey
it.skip('renders positions on portfolio page', () => {
it('renders positions on portfolio page', () => {
cy.mockGQL((req) => {
const positions = positionsQuery();
if (positions.positions?.edges) {
@@ -143,22 +142,22 @@ describe('positions', { tags: '@regression', testIsolation: true }, () => {
it('sorting by Market', () => {
visitAndClickPositions();
const marketsSortedDefault = [
'AAPL.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'SOLUSD',
'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)',
'ETHBTC Quarterly (30 Jun 2022)',
'SUSPENDED MARKET',
];
const marketsSortedAsc = [
'AAPL.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'SOLUSD',
'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)',
'ETHBTC Quarterly (30 Jun 2022)',
'SUSPENDED MARKET',
];
const marketsSortedDesc = [
'SOLUSD',
'ETHBTC.QM21',
'BTCUSD.MF21',
'AAPL.MF21',
'SUSPENDED MARKET',
'ETHBTC Quarterly (30 Jun 2022)',
'Apple Monthly (30 Jun 2022)',
'ACTIVE MARKET',
];
cy.getByTestId(positions).click();
// 7004-POSI-003
@@ -231,7 +230,7 @@ describe('positions', { tags: '@regression', testIsolation: true }, () => {
deltaX: 500,
});
// 7004-POSI-004
cy.get('[col-id="unrealisedPNL"]').should('be.visible');
cy.get('[col-id="updatedAt"]').should('be.visible');
});
it('Drag and drop columns', () => {
@@ -252,13 +251,11 @@ describe('positions', { tags: '@regression', testIsolation: true }, () => {
it('I can see warnings', () => {
visitAndClickPositions();
cy.get('[col-id="openVolume"]')
.eq(3)
.within(() => {
cy.get('[aria-label="warning-sign icon"]')
.should('be.visible')
.realHover();
});
cy.get('[col-id="openVolume"]').within(() => {
cy.get('[aria-label="warning-sign icon"]')
.should('be.visible')
.realHover();
});
// 7004-POSI-011
cy.getByTestId(tooltipContent).should('be.visible');
});
@@ -292,11 +289,9 @@ describe('positions', { tags: '@regression', testIsolation: true }, () => {
it('View settlement asset', () => {
visitAndClickPositions();
cy.get('[col-id="asset"]')
.eq(3)
.within(() => {
cy.get('button[type="button"]').click();
});
cy.get('[col-id="asset"]').within(() => {
cy.get('button[type="button"]').first().click();
});
// 7004-POSI-008
cy.getByTestId(dialogContent).should('be.visible');
cy.getByTestId(dialogCloseX).click();
@@ -311,7 +306,7 @@ function validatePositionsDisplayed(multiKey = false) {
cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions')
.get('.ag-center-cols-container .ag-row')
.eq(multiKey ? 3 : 1)
.first()
.within(() => {
cy.get('[col-id="marketName"]')
.should('be.visible')
@@ -59,7 +59,8 @@ describe('trades', { tags: '@smoke' }, () => {
cy.getByTestId(tradesTable) // order table shares identical col id
.find(`${colIdCreatedAt} ${colHeader}`)
.should('have.text', 'Created at');
const dateTimeRegex = /(\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
cy.getByTestId(tradesTable)
.get(`.ag-center-cols-container ${colIdCreatedAt}`)
.each(($tradeDateTime) => {
@@ -86,7 +87,6 @@ describe('trades', { tags: '@smoke' }, () => {
});
it('copy price to deal ticket form', () => {
cy.getByTestId('Order').click();
// 6005-THIS-007
cy.get(colIdPrice).last().should('be.visible').click();
cy.getByTestId('order-price').should('have.value', '171.16898');
@@ -1,119 +0,0 @@
import { selectAsset } from '../support/helpers';
const amountField = 'input[name="amount"]';
const includeTransferFeeRadioBtn = 'include-transfer-fee';
const manageVegaWallet = 'manage-vega-wallet';
const toAddressField = '[name="toAddress"]';
const totalTransferfee = 'total-transfer-fee';
const transferAmount = 'transfer-amount';
const transferForm = 'transfer-form';
const transferFee = 'transfer-fee';
const walletTransfer = 'wallet-transfer';
const ASSET_SEPOLIA_TBTC = 2;
describe.skip(
'transfer fees',
{ tags: '@regression', testIsolation: true },
() => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Assets');
cy.wait('@Accounts');
cy.mockVegaWalletTransaction();
});
it('transfer fees tooltips', () => {
// 1003-TRAN-015
// 1003-TRAN-016
// 1003-TRAN-017
// 1003-TRAN-018
// 1003-TRAN-019
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type(
'7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535'
);
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
/// Check Include Transfer Fee tooltip
cy.get('label[for="include-transfer-fee"] div').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Transfer Fee tooltip
cy.contains('div', 'Transfer fee').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Amount to be transferred tooltip
cy.contains('div', 'Amount to be transferred').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Total amount (with fee) tooltip
cy.contains('div', 'Total amount (with fee)').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
});
it('transfer fees', () => {
// 1003-TRAN-020
// 1003-TRAN-021
// 1003-TRAN-022
// 1003-TRAN-023
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type(
'7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535'
);
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(includeTransferFeeRadioBtn).should('be.disabled');
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '1.00');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.01');
cy.getByTestId(includeTransferFeeRadioBtn).click();
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '0.99');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.00');
});
}
);
@@ -4,16 +4,118 @@ const amountField = 'input[name="amount"]';
const transferText = 'transfer-intro-text';
const errorText = 'input-error-text';
const formFieldError = 'input-error-text';
const includeTransferFeeRadioBtn = 'include-transfer-fee';
const keyID = `[data-testid="${transferText}"] > .rounded-md`;
const manageVegaWallet = 'manage-vega-wallet';
const submitTransferBtn = '[type="submit"]';
const toAddressField = '[name="toAddress"]';
const totalTransferfee = 'total-transfer-fee';
const transferAmount = 'transfer-amount';
const transferForm = 'transfer-form';
const transferFee = 'transfer-fee';
const walletTransfer = 'wallet-transfer';
const ASSET_EURO = 1;
const ASSET_SEPOLIA_TBTC = 2;
describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Assets');
cy.wait('@Accounts');
cy.mockVegaWalletTransaction();
});
it('transfer fees tooltips', () => {
// 1003-TRAN-015
// 1003-TRAN-016
// 1003-TRAN-017
// 1003-TRAN-018
// 1003-TRAN-019
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
/// Check Include Transfer Fee tooltip
cy.get('label[for="include-transfer-fee"] div').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Transfer Fee tooltip
cy.contains('div', 'Transfer fee').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Amount to be transferred tooltip
cy.contains('div', 'Amount to be transferred').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Total amount (with fee) tooltip
cy.contains('div', 'Total amount (with fee)').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
});
it('transfer fees', () => {
// 1003-TRAN-020
// 1003-TRAN-021
// 1003-TRAN-022
// 1003-TRAN-023
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(includeTransferFeeRadioBtn).should('be.disabled');
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '1.00');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.01');
cy.getByTestId(includeTransferFeeRadioBtn).click();
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '0.99');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.00');
});
});
describe(
'transfer form validation',
{ tags: '@regression', testIsolation: true },
@@ -30,8 +132,6 @@ describe(
cy.wait('@Accounts');
cy.wait('@Assets');
cy.mockVegaWalletTransaction();
});
it('transfer Text', () => {
@@ -44,16 +44,16 @@ const MainGrid = memo(
return (
<ResizableGrid vertical onChange={handleOnLayoutChange}>
<ResizableGridPanel
preferredSize={sizes[0]}
priority={LayoutPriority.High}
minSize={200}
>
<ResizableGrid onChange={handleOnMiddleLayoutChange}>
<ResizableGridPanel minSize={75} priority={LayoutPriority.High}>
<ResizableGrid
proportionalLayout={false}
minSize={200}
onChange={handleOnMiddleLayoutChange}
>
<ResizableGridPanel
priority={LayoutPriority.High}
minSize={200}
preferredSize={sizesMiddle[0] || '75%'}
preferredSize={sizesMiddle[1] || '50%'}
>
<TradeGridChild>
<Tabs storageKey="console-trade-grid-main-left">
@@ -74,8 +74,8 @@ const MainGrid = memo(
</TradeGridChild>
</ResizableGridPanel>
<ResizableGridPanel
preferredSize={sizesMiddle[2] || 300}
minSize={200}
preferredSize={sizesMiddle[1] || 300}
>
<TradeGridChild>
<Tabs storageKey="console-trade-grid-main-right">
@@ -91,9 +91,9 @@ const MainGrid = memo(
</ResizableGrid>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize={sizes[1] || '25%'}
minSize={50}
priority={LayoutPriority.Low}
>
<TradeGridChild>
<Tabs storageKey="console-trade-grid-bottom">
@@ -1,5 +1,6 @@
import type { ComponentProps } from 'react';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { TradesContainer } from '@vegaprotocol/trades';
import { DepthChartContainer } from '@vegaprotocol/market-depth';
import {
CandlesChartContainer,
@@ -7,7 +8,6 @@ import {
} from '@vegaprotocol/candles-chart';
import { Filter } from '@vegaprotocol/orders';
import { NO_MARKET } from './constants';
import { TradesContainer } from '../../components/trades-container';
import { OrderbookContainer } from '../../components/orderbook-container';
import { FillsContainer } from '../../components/fills-container';
import { PositionsContainer } from '../../components/positions-container';
@@ -276,9 +276,15 @@ const ClosedMarketsDataGrid = ({
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
rowData={rowData}
columnDefs={colDefs}
getRowId={({ data }) => data.id}
defaultColDef={{
resizable: true,
minWidth: 100,
flex: 1,
}}
components={{ SuccessorMarketRenderer }}
overlayNoRowsTemplate={error ? error.message : t('No markets')}
/>
@@ -4,9 +4,12 @@ import { depositsProvider } from '@vegaprotocol/deposits';
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { useSidebar, ViewType } from '../../components/sidebar';
export const DepositsContainer = () => {
const gridRef = useRef<AgGridReact | null>(null);
const { pubKey, isReadOnly } = useVegaWallet();
const { data, error } = useDataProvider({
dataProvider: depositsProvider,
@@ -21,6 +24,7 @@ export const DepositsContainer = () => {
<div className="h-full">
<DepositsTable
rowData={data}
ref={gridRef}
overlayNoRowsTemplate={error ? error.message : t('No deposits')}
/>
{!isReadOnly && (
@@ -1,15 +1,27 @@
import { OrderbookManager } from '@vegaprotocol/market-depth';
import { useCreateOrderStore } from '@vegaprotocol/orders';
import { ViewType, useSidebar } from '../sidebar';
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
import { useStopOrderFormValues } from '@vegaprotocol/deal-ticket';
export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
const update = useDealTicketFormValues((state) => state.updateAll);
const useOrderStoreRef = useCreateOrderStore();
const updateOrder = useOrderStoreRef((store) => store.update);
const updateStoredFormValues = useStopOrderFormValues(
(state) => state.update
);
const setView = useSidebar((store) => store.setView);
return (
<OrderbookManager
marketId={marketId}
onClick={(values) => {
update(marketId, values);
onClick={({ price, size }) => {
if (price) {
updateOrder(marketId, { price });
updateStoredFormValues(marketId, { price });
}
if (size) {
updateOrder(marketId, { size });
updateStoredFormValues(marketId, { size });
}
setView({ type: ViewType.Order });
}}
/>
@@ -19,7 +19,9 @@ export const PositionsContainer = ({
const gridStore = usePositionsStore((store) => store.gridStore);
const updateGridStore = usePositionsStore((store) => store.updateGridStore);
const gridStoreCallbacks = useDataGridEvents(gridStore, updateGridStore);
const gridStoreCallbacks = useDataGridEvents(gridStore, (colState) => {
updateGridStore(colState);
});
if (!pubKey) {
return (
@@ -1 +0,0 @@
export * from './trades-container';
@@ -1,24 +0,0 @@
import { TradesManager } from '@vegaprotocol/trades';
import type { DataGridSlice } from '../../stores/datagrid-store-slice';
import { createDataGridSlice } from '../../stores/datagrid-store-slice';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { useDataGridEvents } from '@vegaprotocol/datagrid';
interface TradesContainerProps {
marketId: string;
}
export const TradesContainer = ({ marketId }: TradesContainerProps) => {
const gridStore = useTradesStore((store) => store.gridStore);
const updateGridStore = useTradesStore((store) => store.updateGridStore);
const gridStoreCallbacks = useDataGridEvents(gridStore, updateGridStore);
return <TradesManager marketId={marketId} gridProps={gridStoreCallbacks} />;
};
const useTradesStore = create<DataGridSlice>()(
persist(createDataGridSlice, {
name: 'vega_trades_store',
})
);
@@ -1,31 +0,0 @@
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { GetStarted } from './get-started';
import { render, screen } from '@testing-library/react';
describe('GetStarted', () => {
const renderComponent = (context: Partial<VegaWalletContextShape> = {}) => {
return render(
<VegaWalletContext.Provider value={context as VegaWalletContextShape}>
<GetStarted />
</VegaWalletContext.Provider>
);
};
it('renders full get started content if not connected and no browser wallet detected', () => {
renderComponent();
expect(screen.getByTestId('get-started-banner')).toBeInTheDocument();
});
it('renders connect prompt if no pubKey but wallet installed', () => {
globalThis.window.vega = {} as Vega;
renderComponent();
expect(screen.getByTestId('order-connect-wallet')).toBeInTheDocument();
globalThis.window.vega = undefined as unknown as Vega;
});
it('renders nothing if connected', () => {
const { container } = renderComponent({ pubKey: 'my-pubkey' });
expect(container).toBeEmptyDOMElement();
});
});
@@ -16,6 +16,7 @@ interface Props {
export const GetStarted = ({ lead }: Props) => {
const { pubKey } = useVegaWallet();
const { VEGA_ENV, VEGA_NETWORKS } = useEnvironment();
const CANONICAL_URL = VEGA_NETWORKS[VEGA_ENV] || 'https://console.vega.xyz';
@@ -32,18 +33,18 @@ export const GetStarted = ({ lead }: Props) => {
setOnboardingViewed('true');
};
const wrapperClasses = classNames(
'flex flex-col py-4 px-6 gap-4 rounded',
'bg-vega-blue-300 dark:bg-vega-blue-700',
'border border-vega-blue-350 dark:border-vega-blue-650',
{ 'mt-8': !lead }
);
if (!pubKey && !isBrowserWalletInstalled()) {
return (
<div className={wrapperClasses} data-testid="get-started-banner">
<div
className={classNames(
'flex flex-col bg-vega-blue-300 dark:bg-vega-blue-700 border border-vega-blue-350 dark:border-vega-blue-650 px-6 gap-4',
{ 'py-4': lead },
{ 'mt-8 py-6': !lead }
)}
data-testid="get-started-banner"
>
{lead && <h2>{lead}</h2>}
<h3 className="text-lg">{t('Get started')}</h3>
<h3>{t('Get started')}</h3>
<div>
<ul className="list-decimal list-inside">
<li>{t('Get a Vega wallet')}</li>
@@ -56,6 +57,7 @@ export const GetStarted = ({ lead }: Props) => {
<TradingButton
intent={Intent.Info}
onClick={onButtonClick}
className="block w-full"
data-testid="get-started-button"
>
{t('Get started')}
@@ -80,28 +82,5 @@ export const GetStarted = ({ lead }: Props) => {
</div>
);
}
if (!pubKey) {
return (
<div className={wrapperClasses}>
<p className="text-sm mb-1">
You need a{' '}
<ExternalLink href="https://vega.xyz/wallet">
Vega wallet
</ExternalLink>{' '}
to start trading in this market.
</p>
<TradingButton
onClick={openVegaWalletDialog}
size="small"
data-testid="order-connect-wallet"
intent={Intent.Info}
>
{t('Connect wallet')}
</TradingButton>
</div>
);
}
return null;
};
@@ -31,12 +31,12 @@ export const WelcomeDialogContent = () => {
<ul className="ml-0">
<ListItemContent
icon={<NonCustodialIcon />}
title={t('Non-custodial and pseudonymous')}
title={t('Non-cutodial and pseudonymous')}
text={t('No third party has access to your funds.')}
/>
<ListItemContent
icon={<PurposeBuiltIcon />}
title={t('Purpose built proof of stake blockchain')}
title={t('Purpose built proof of stake blockhain')}
text={t(
'Fully decentralised high performance peer-to-network trading.'
)}
-10
View File
@@ -142,19 +142,12 @@ html [data-theme='dark'] {
border-width: 0;
}
.vega-ag-grid .ag-cell .ag-cell-wrapper {
height: 100%;
}
.vega-ag-grid .ag-header-row {
@apply font-alpha font-normal;
}
/* Light variables */
.ag-theme-balham {
--ag-grid-size: 2px; /* Used for compactness */
--ag-row-height: 36px;
--ag-header-height: 36px;
--ag-background-color: theme(colors.white);
--ag-border-color: theme(colors.vega.clight.600);
--ag-header-background-color: theme(colors.vega.clight.700);
@@ -167,9 +160,6 @@ html [data-theme='dark'] {
/* Dark variables */
.ag-theme-balham-dark {
--ag-grid-size: 2px; /* Used for compactness */
--ag-row-height: 36px;
--ag-header-height: 36px;
--ag-background-color: theme(colors.vega.cdark.900);
--ag-border-color: theme(colors.vega.cdark.600);
--ag-header-background-color: theme(colors.vega.cdark.700);
+1 -1
View File
@@ -7,7 +7,7 @@ module.exports = {
'scope-enum': async (ctx) => [
2,
'always',
['ci', 'docs', 'specs', ...(await utils.getProjects(ctx))],
['ci', 'docs', ...(await utils.getProjects(ctx))],
],
},
};
@@ -118,6 +118,7 @@ export const AccountManager = ({
onMarketClick,
gridProps,
}: AccountManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [breakdownAssetId, setBreakdownAssetId] = useState<string>();
const { data, error } = useDataProvider({
dataProvider: aggregatedAccountsDataProvider,
@@ -137,6 +138,7 @@ export const AccountManager = ({
return (
<div className="relative h-full">
<AccountTable
ref={gridRef}
rowData={data}
onClickAsset={onClickAsset}
onClickDeposit={onClickDeposit}
+245 -239
View File
@@ -1,4 +1,4 @@
import { useMemo, useCallback } from 'react';
import { forwardRef, useMemo, useCallback } from 'react';
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
@@ -21,7 +21,7 @@ import type {
RowHeightParams,
ColDef,
} from 'ag-grid-community';
import type { AgGridReactProps } from 'ag-grid-react';
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
import type { AccountFields } from './accounts-data-provider';
import type { Asset } from '@vegaprotocol/types';
import { CenteredGridCellWrapper } from '@vegaprotocol/datagrid';
@@ -58,12 +58,6 @@ export const accountValuesComparator = (
return valueA > valueB ? 1 : -1;
};
const defaultColDef = {
resizable: true,
sortable: true,
tooltipComponent: TooltipCellComponent,
comparator: accountValuesComparator,
};
export interface GetRowsParams extends Omit<IGetRowsParams, 'successCallback'> {
successCallback(rowsThisBlock: AccountFields[], lastRow?: number): void;
}
@@ -81,244 +75,256 @@ export interface AccountTableProps extends AgGridReactProps {
pinnedAsset?: PinnedAsset;
}
export const AccountTable = ({
onClickAsset,
onClickWithdraw,
onClickDeposit,
onClickBreakdown,
onClickTransfer,
rowData,
isReadOnly,
pinnedAsset,
...props
}: AccountTableProps) => {
const pinnedRow = useMemo(() => {
if (!pinnedAsset) {
return;
}
const currentPinnedAssetRow = rowData?.find(
(row) => row.asset.id === pinnedAsset?.id
);
if (!currentPinnedAssetRow) {
return {
asset: pinnedAsset,
available: '0',
used: '0',
total: '0',
balance: '0',
};
}
return currentPinnedAssetRow;
}, [pinnedAsset, rowData]);
const { getRowHeight } = props;
const getPinnedAssetRowHeight = useCallback(
(params: RowHeightParams) => {
if (
params.node.rowPinned &&
params.data.asset.id === pinnedAsset?.id &&
new BigNumber(params.data.total).isLessThanOrEqualTo(0)
) {
return 32;
}
return getRowHeight ? getRowHeight(params) : undefined;
export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
(
{
onClickAsset,
onClickWithdraw,
onClickDeposit,
onClickBreakdown,
onClickTransfer,
rowData,
isReadOnly,
pinnedAsset,
...props
},
[pinnedAsset?.id, getRowHeight]
);
ref
) => {
const pinnedRow = useMemo(() => {
if (!pinnedAsset) {
return;
}
const currentPinnedAssetRow = rowData?.find(
(row) => row.asset.id === pinnedAsset?.id
);
if (!currentPinnedAssetRow) {
return {
asset: pinnedAsset,
available: '0',
used: '0',
total: '0',
balance: '0',
};
}
return currentPinnedAssetRow;
}, [pinnedAsset, rowData]);
const showDepositButton = pinnedRow?.balance === '0';
const { getRowHeight } = props;
const colDefs = useMemo(() => {
const defs: ColDef[] = [
{
headerName: t('Asset'),
field: 'asset.symbol',
headerTooltip: t(
'Asset is the collateral that is deposited into the Vega protocol.'
),
cellClass: 'underline',
onCellClicked: ({ data }) => {
if (data) {
onClickAsset(data.asset.id);
}
},
const getPinnedAssetRowHeight = useCallback(
(params: RowHeightParams) => {
if (
params.node.rowPinned &&
params.data.asset.id === pinnedAsset?.id &&
new BigNumber(params.data.total).isLessThanOrEqualTo(0)
) {
return 32;
}
return getRowHeight ? getRowHeight(params) : undefined;
},
{
headerName: t('Used'),
type: 'rightAligned',
field: 'used',
headerTooltip: t(
'Currently allocated to a market as margin or bond. Check the breakdown for details.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
[pinnedAsset?.id, getRowHeight]
);
const showDepositButton = pinnedRow?.balance === '0';
const colDefs = useMemo(() => {
const defs: ColDef[] = [
{
headerName: t('Asset'),
field: 'asset.symbol',
headerTooltip: t(
'Asset is the collateral that is deposited into the Vega protocol.'
),
cellClass: 'underline',
onCellClicked: ({ data }) => {
if (data) {
onClickAsset(data.asset.id);
}
},
},
onCellClicked: ({ data }) => {
if (!data || !onClickBreakdown) return;
onClickBreakdown(data.asset.id);
},
cellRenderer: ({
data,
value,
}: VegaICellRendererParams<AccountFields, 'used'>) => {
if (!value || !data) return '-';
const percentageUsed = percentageValue(value, data.total);
const valueFormatted = addDecimalsFormatNumberQuantum(
{
headerName: t('Used'),
type: 'rightAligned',
field: 'used',
headerTooltip: t(
'Currently allocated to a market as margin or bond. Check the breakdown for details.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
onCellClicked: ({ data }) => {
if (!data || !onClickBreakdown) return;
onClickBreakdown(data.asset.id);
},
cellRenderer: ({
data,
value,
data.asset.decimals,
data.asset.quantum
);
return data.breakdown ? (
<>
<span className="underline">{valueFormatted}</span>
<span
className={classNames(
colorClass(percentageUsed),
'ml-1 inline-block w-14'
)}
>
{percentageUsed.toFixed(2)}%
</span>
</>
) : (
<>
<span className="underline">{valueFormatted}</span>
<span className="ml-2 inline-block w-14 text-muted">
{t('0.00%')}
</span>
</>
);
},
},
{
headerName: t('Available'),
field: 'available',
type: 'rightAligned',
headerTooltip: t(
'Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
cellClass: ({ data }) => {
const percentageUsed = percentageValue(data?.used, data?.total);
return colorClass(percentageUsed, true);
},
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<AccountFields, 'available'>) => {
if (!value || !data) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
},
{
headerName: t('Total'),
type: 'rightAligned',
field: 'total',
headerTooltip: t(
'The total amount of each asset on this key. Includes used and available collateral.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<AccountFields, 'total'>) => {
if (!data || !value) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
},
{
colId: 'accounts-actions',
field: 'asset.id',
...COL_DEFS.actions,
minWidth: showDepositButton ? 130 : COL_DEFS.actions.minWidth,
maxWidth: showDepositButton ? 130 : COL_DEFS.actions.maxWidth,
cellRenderer: ({
value: assetId,
node,
}: VegaICellRendererParams<AccountFields, 'asset.id'>) => {
if (!assetId) return null;
if (node.rowPinned && node.data?.total === '0') {
return (
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
<Button
size="xs"
variant="primary"
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(assetId);
}}
>
<VegaIcon name={VegaIconNames.DEPOSIT} /> {t('Deposit')}
</Button>
</CenteredGridCellWrapper>
}: VegaICellRendererParams<AccountFields, 'used'>) => {
if (!value || !data) return '-';
const percentageUsed = percentageValue(value, data.total);
const valueFormatted = addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
}
return isReadOnly ? null : (
<AccountsActionsDropdown
assetId={assetId}
assetContractAddress={
node.data?.asset.source?.__typename === 'ERC20'
? node.data.asset.source.contractAddress
: undefined
}
onClickDeposit={() => {
onClickDeposit && onClickDeposit(assetId);
}}
onClickWithdraw={() => {
onClickWithdraw && onClickWithdraw(assetId);
}}
onClickBreakdown={() => {
onClickBreakdown && onClickBreakdown(assetId);
}}
onClickTransfer={() => {
onClickTransfer && onClickTransfer(assetId);
}}
/>
);
return data.breakdown ? (
<>
<span className="underline">{valueFormatted}</span>
<span
className={classNames(
colorClass(percentageUsed),
'ml-1 inline-block w-14'
)}
>
{percentageUsed.toFixed(2)}%
</span>
</>
) : (
<>
<span className="underline">{valueFormatted}</span>
<span className="ml-2 inline-block w-14 text-muted">
{t('0.00%')}
</span>
</>
);
},
},
{
headerName: t('Available'),
field: 'available',
type: 'rightAligned',
headerTooltip: t(
'Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
cellClass: ({ data }) => {
const percentageUsed = percentageValue(data?.used, data?.total);
return colorClass(percentageUsed, true);
},
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<AccountFields, 'available'>) => {
if (!value || !data) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
},
},
];
return defs;
}, [
onClickAsset,
onClickBreakdown,
onClickDeposit,
onClickWithdraw,
onClickTransfer,
isReadOnly,
showDepositButton,
]);
const data = rowData?.filter((data) => data.asset.id !== pinnedAsset?.id);
{
headerName: t('Total'),
type: 'rightAligned',
field: 'total',
headerTooltip: t(
'The total amount of each asset on this key. Includes used and available collateral.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<AccountFields, 'total'>) => {
if (!data || !value) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
},
{
colId: 'accounts-actions',
field: 'asset.id',
...COL_DEFS.actions,
minWidth: showDepositButton ? 130 : COL_DEFS.actions.minWidth,
maxWidth: showDepositButton ? 130 : COL_DEFS.actions.maxWidth,
cellRenderer: ({
value: assetId,
node,
}: VegaICellRendererParams<AccountFields, 'asset.id'>) => {
if (!assetId) return null;
if (node.rowPinned && node.data?.total === '0') {
return (
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
<Button
size="xs"
variant="primary"
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(assetId);
}}
>
<VegaIcon name={VegaIconNames.DEPOSIT} /> {t('Deposit')}
</Button>
</CenteredGridCellWrapper>
);
}
return isReadOnly ? null : (
<AccountsActionsDropdown
assetId={assetId}
assetContractAddress={
node.data?.asset.source?.__typename === 'ERC20'
? node.data.asset.source.contractAddress
: undefined
}
onClickDeposit={() => {
onClickDeposit && onClickDeposit(assetId);
}}
onClickWithdraw={() => {
onClickWithdraw && onClickWithdraw(assetId);
}}
onClickBreakdown={() => {
onClickBreakdown && onClickBreakdown(assetId);
}}
onClickTransfer={() => {
onClickTransfer && onClickTransfer(assetId);
}}
/>
);
},
},
];
return defs;
}, [
onClickAsset,
onClickBreakdown,
onClickDeposit,
onClickWithdraw,
onClickTransfer,
isReadOnly,
showDepositButton,
]);
return (
<AgGrid
{...props}
getRowId={({ data }: { data: AccountFields }) => data.asset.id}
tooltipShowDelay={500}
rowData={data}
defaultColDef={defaultColDef}
columnDefs={colDefs}
getRowHeight={getPinnedAssetRowHeight}
pinnedTopRowData={pinnedRow ? [pinnedRow] : undefined}
/>
);
};
const data = rowData?.filter((data) => data.asset.id !== pinnedAsset?.id);
return (
<AgGrid
{...props}
style={{ width: '100%', height: '100%' }}
getRowId={({ data }: { data: AccountFields }) => data.asset.id}
ref={ref}
tooltipShowDelay={500}
rowData={data}
defaultColDef={{
resizable: true,
tooltipComponent: TooltipCellComponent,
sortable: true,
comparator: accountValuesComparator,
}}
columnDefs={colDefs}
getRowHeight={getPinnedAssetRowHeight}
pinnedTopRowData={pinnedRow ? [pinnedRow] : undefined}
/>
);
}
);
@@ -23,7 +23,6 @@ const singleRow = {
instrument: {
__typename: 'Instrument',
name: 'BTCUSD Monthly (30 Jun 2022)',
code: 'BTCUSD.MF21',
},
},
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
@@ -58,7 +57,7 @@ describe('BreakdownTable', () => {
});
const cells = await screen.findAllByRole('gridcell');
const expectedValues = [
'BTCUSD.MF21',
'BTCUSD Monthly (30 Jun 2022)',
'Margin',
'1,256.00 (50%)',
'1,256.00',
@@ -119,7 +118,6 @@ describe('BreakdownTable', () => {
instrument: {
__typename: 'Instrument',
name: 'BTCUSD Monthly (30 Jun 2022)',
code: 'BTCUSD.MF21',
},
},
},
+11 -7
View File
@@ -20,10 +20,6 @@ import { MarginHealthChart } from './margin-health-chart';
import { MarketNameCell } from '@vegaprotocol/datagrid';
import { AccountType } from '@vegaprotocol/types';
const defaultColDef = {
resizable: true,
sortable: true,
};
interface BreakdownTableProps extends AgGridReactProps {
data: AccountFields[] | null;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
@@ -35,16 +31,17 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
const defs: ColDef[] = [
{
headerName: t('Market'),
field: 'market.tradableInstrument.instrument.code',
field: 'market.tradableInstrument.instrument.name',
valueFormatter: ({
value,
}: VegaValueFormatterParams<
AccountFields,
'market.tradableInstrument.instrument.code'
'market.tradableInstrument.instrument.name'
>) => {
if (!value) return 'None';
return value;
},
minWidth: 200,
},
{
headerName: t('Account type'),
@@ -61,6 +58,7 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
{
headerName: t('Balance'),
field: 'used',
flex: 2,
maxWidth: 500,
type: 'rightAligned',
tooltipComponent: TooltipCellComponent,
@@ -99,6 +97,7 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
{
headerName: t('Margin health'),
field: 'market.id',
flex: 2,
maxWidth: 500,
sortable: false,
cellRenderer: ({
@@ -119,6 +118,7 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('Collateral not used')}
rowData={data}
getRowId={({ data }: { data: AccountFields }) =>
@@ -128,7 +128,11 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
rowHeight={34}
components={{ PriceCell, MarketNameCell, ProgressBarCell }}
tooltipShowDelay={500}
defaultColDef={defaultColDef}
defaultColDef={{
flex: 1,
resizable: true,
sortable: true,
}}
columnDefs={coldefs}
/>
);
+2 -2
View File
@@ -28,10 +28,10 @@ export const assetProvider = makeDataProvider<
getData,
});
export const useAssetDataProvider = (assetId: string, skip?: boolean) => {
export const useAssetDataProvider = (assetId: string) => {
return useDataProvider({
dataProvider: assetProvider,
variables: { assetId: assetId || '' },
skip: !assetId || skip,
skip: !assetId,
});
};
+1 -17
View File
@@ -4,25 +4,9 @@ export default {
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
'^.+\\.[tj]sx?$': [
'babel-jest',
{
presets: ['@nx/react/babel'],
// required for pennant to work in jest, due to having untranspiled exports
plugins: [
[
'@babel/plugin-proposal-private-methods',
{
loose: true,
},
],
],
},
],
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/candles-chart',
setupFilesAfterEnv: ['./src/setup-tests.ts'],
// dont ignore pennant from transpilation
transformIgnorePatterns: ['<rootDir>/node_modules/pennant'],
};
+1
View File
@@ -21,4 +21,5 @@ export * from './lib/type-helpers';
export * from './lib/cells/grid-progress-bar';
export * from './lib/ag-grid-update';
export * from './lib/use-datagrid-events';
+20
View File
@@ -0,0 +1,20 @@
import type { MutableRefObject, RefObject } from 'react';
import type { AgGridReact } from 'ag-grid-react';
type AnyArray = Array<any> | null; // eslint-disable-line @typescript-eslint/no-explicit-any
export const isXOrWasEmpty = (prev?: AnyArray, curr?: AnyArray) =>
Boolean(Number(!!prev?.length) ^ Number(!!curr?.length));
export const updateGridData = (
dataRef: MutableRefObject<AnyArray>,
data: AnyArray,
gridRef: RefObject<AgGridReact>
) => {
const rerender = isXOrWasEmpty(dataRef.current, data);
dataRef.current = data;
if (gridRef.current?.api?.getModel().getType() === 'infinite') {
gridRef.current?.api?.refreshInfiniteCache();
}
return !rerender;
};
@@ -3,20 +3,6 @@ import { AgGridReact } from 'ag-grid-react';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { t } from '@vegaprotocol/i18n';
import classNames from 'classnames';
import type { ColDef } from 'ag-grid-community';
const defaultProps: AgGridReactProps = {
enableCellTextSelection: true,
overlayLoadingTemplate: t('Loading...'),
overlayNoRowsTemplate: t('No data'),
suppressCellFocus: true,
suppressColumnMoveAnimation: true,
};
const defaultColDef: ColDef = {
resizable: true,
sortable: true,
};
export const AgGridThemed = ({
style,
@@ -27,20 +13,23 @@ export const AgGridThemed = ({
gridRef?: React.ForwardedRef<AgGridReact>;
}) => {
const { theme } = useThemeSwitcher();
const defaultProps = {
rowHeight: 22,
headerHeight: 22,
enableCellTextSelection: true,
overlayLoadingTemplate: t('Loading...'),
overlayNoRowsTemplate: t('No data'),
suppressCellFocus: true,
};
const wrapperClasses = classNames('vega-ag-grid', 'w-full h-full', {
const wrapperClasses = classNames('vega-ag-grid', {
'ag-theme-balham': theme === 'light',
'ag-theme-balham-dark': theme === 'dark',
});
return (
<div className={wrapperClasses}>
<AgGridReact
defaultColDef={defaultColDef}
ref={gridRef}
{...defaultProps}
{...props}
/>
<div className={wrapperClasses} style={style}>
<AgGridReact {...defaultProps} {...props} ref={gridRef} />
</div>
);
};
@@ -29,11 +29,7 @@ export const MarketNameCell = ({
);
if (!value || !data) return null;
return onMarketClick ? (
<button
onClick={handleOnClick}
tabIndex={0}
className="block text-left text-ellipsis overflow-hidden whitespace-nowrap w-full"
>
<button onClick={handleOnClick} tabIndex={0}>
{value}
</button>
) : (
+2 -2
View File
@@ -4,8 +4,8 @@ export const COL_DEFS = {
sortable: false,
resizable: false,
filter: false,
minWidth: 30,
maxWidth: 30,
minWidth: 45,
maxWidth: 45,
type: 'rightAligned',
pinned: 'right' as const,
},
@@ -14,6 +14,7 @@ const gridProps = {
{
field: 'id',
width: 100,
resizable: true,
filter: 'agNumberColumnFilter',
},
],
@@ -49,7 +50,7 @@ describe('useDataGridEvents', () => {
console.warn = originalWarn;
});
it('default state is set and callback is called on filter event', async () => {
it('default state is set and callback is called on column or filter event', async () => {
const callback = jest.fn();
const initialState = {
filterModel: undefined,
@@ -66,6 +67,45 @@ describe('useDataGridEvents', () => {
// no filters set
expect(result.current.api.getFilterModel()).toEqual({});
const newWidth = 400;
// Set col width
await act(async () => {
result.current.columnApi.setColumnWidth('id', newWidth);
});
act(() => {
jest.advanceTimersByTime(GRID_EVENT_DEBOUNCE_TIME);
});
expect(callback).toHaveBeenCalledWith({
columnState: [expect.objectContaining({ colId: 'id', width: newWidth })],
filterModel: {},
});
callback.mockClear();
expect(result.current.columnApi.getColumnState()[0].width).toEqual(
newWidth
);
// Set filter
await act(async () => {
result.current.columnApi.applyColumnState({
state: [{ colId: 'id', sort: 'asc' }],
applyOrder: true,
});
});
act(() => {
jest.advanceTimersByTime(GRID_EVENT_DEBOUNCE_TIME);
});
expect(callback).toHaveBeenCalledWith({
columnState: [expect.objectContaining({ colId: 'id', sort: 'asc' })],
filterModel: {},
});
callback.mockClear();
expect(result.current.columnApi.getColumnState()[0].sort).toEqual('asc');
// Set filter
const idFilter = {
filter: 1,
@@ -83,7 +123,7 @@ describe('useDataGridEvents', () => {
});
expect(callback).toHaveBeenCalledWith({
columnState: undefined,
columnState: expect.any(Object),
filterModel: {
id: idFilter,
},
@@ -98,7 +138,7 @@ describe('useDataGridEvents', () => {
filterType: 'number',
type: 'equals',
};
const colState = { colId: 'id', sort: 'desc' as const };
const colState = { colId: 'id', width: 300, sort: 'desc' as const };
const initialState = {
filterModel: {
id: idFilter,
@@ -116,7 +156,7 @@ describe('useDataGridEvents', () => {
});
});
it('ignores events that were not made via the UI', async () => {
it('debounces events', async () => {
const callback = jest.fn();
const initialState = {
filterModel: undefined,
@@ -130,6 +170,8 @@ describe('useDataGridEvents', () => {
// Set col width multiple times
await act(async () => {
result.current.columnApi.setColumnWidth('id', newWidth);
result.current.columnApi.setColumnWidth('id', newWidth);
result.current.columnApi.setColumnWidth('id', newWidth);
});
expect(callback).not.toHaveBeenCalled();
@@ -138,6 +180,6 @@ describe('useDataGridEvents', () => {
jest.advanceTimersByTime(GRID_EVENT_DEBOUNCE_TIME);
});
expect(callback).toHaveBeenCalledTimes(0);
expect(callback).toHaveBeenCalledTimes(1);
});
});
+23 -67
View File
@@ -1,13 +1,12 @@
import debounce from 'lodash/debounce';
import type {
ColumnMovedEvent,
ColumnResizedEvent,
ColumnState,
ColumnVisibleEvent,
FilterChangedEvent,
FirstDataRenderedEvent,
GridReadyEvent,
SortChangedEvent,
} from 'ag-grid-community';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
type State = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -15,70 +14,30 @@ type State = {
columnState?: ColumnState[];
};
type Event = ColumnResizedEvent | FilterChangedEvent | SortChangedEvent;
export const GRID_EVENT_DEBOUNCE_TIME = 300;
export const useDataGridEvents = (
state: State,
callback: (data: State) => void
) => {
/**
* Callback for filter events
*/
const onFilterChanged = useCallback(
({ api }: FilterChangedEvent) => {
if (!api) return;
const filterModel = api.getFilterModel();
callback({ filterModel });
},
// This function can be called very frequently by the onColumnResized
// grid callback, so its memoized to only update after resizing is finished
const onGridChange = useMemo(
() =>
debounce(({ api, columnApi }: Event) => {
if (!api || !columnApi) return;
const columnState = columnApi.getColumnState();
const filterModel = api.getFilterModel();
callback({ columnState, filterModel });
}, GRID_EVENT_DEBOUNCE_TIME),
[callback]
);
/**
* Callback for column resized and column moved events, which can be
* triggered in quick succession. Uses the finished flag to not call the
* store callback unnecessarily
*/
const onDebouncedColumnChange = useCallback(
({
columnApi,
source,
finished,
}: ColumnResizedEvent | ColumnMovedEvent) => {
if (!finished) return;
// only call back on user interactions, and not events triggered from the api
const permittedEvents = [
'uiColumnResized',
'uiColumnDragged',
'uiColumnMoved',
];
if (!permittedEvents.includes(source)) {
return;
}
const columnState = columnApi.getColumnState();
callback({ columnState });
},
[callback]
);
/**
* Callback for sort and visible events
*/
const onColumnChange = useCallback(
({ columnApi }: SortChangedEvent | ColumnVisibleEvent) => {
const columnState = columnApi.getColumnState();
callback({ columnState });
},
[callback]
);
/**
* Callback for grid startup to apply stored column and filter states.
* State only applied if found, otherwise columns sized to fit available space
*/
// check if we have stored column states or filter models and apply if we do
const onGridReady = useCallback(
({ api, columnApi }: FirstDataRenderedEvent) => {
({ api, columnApi }: GridReadyEvent) => {
if (!api || !columnApi) return;
if (state.columnState) {
@@ -87,6 +46,7 @@ export const useDataGridEvents = (
applyOrder: true,
});
} else {
// ensure columns fit available space if no widths are set
api.sizeColumnsToFit();
}
@@ -99,12 +59,8 @@ export const useDataGridEvents = (
return {
onGridReady,
// these events don't use the 'finished' flag
onFilterChanged,
onSortChanged: onColumnChange,
onColumnVisible: onColumnChange,
// these trigger a lot so this callback uses the 'finished' flag
onColumnMoved: onDebouncedColumnChange,
onColumnResized: onDebouncedColumnChange,
onColumnResized: onGridChange,
onFilterChanged: onGridChange,
onSortChanged: onGridChange,
};
};
@@ -3,25 +3,29 @@ import type { Market, StaticMarketData } from '@vegaprotocol/markets';
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
import * as Schema from '@vegaprotocol/types';
import type { OrderFormValues } from '../../hooks/use-form-values';
import type { OrderObj } from '@vegaprotocol/orders';
import type { OrderFormFields } from '../../hooks/use-order-form';
export interface DealTicketAmountProps {
control: Control<OrderFormValues>;
type: Schema.OrderType;
control: Control<OrderFormFields>;
orderType: Schema.OrderType;
marketData: StaticMarketData;
marketPrice?: string;
market: Market;
sizeError?: string;
priceError?: string;
update: (obj: Partial<OrderObj>) => void;
size: string;
price?: string;
}
export const DealTicketAmount = ({
type,
orderType,
marketData,
marketPrice,
...props
}: DealTicketAmountProps) => {
switch (type) {
switch (orderType) {
case Schema.OrderType.TYPE_MARKET:
return (
<DealTicketMarketAmount
@@ -33,7 +37,7 @@ export const DealTicketAmount = ({
case Schema.OrderType.TYPE_LIMIT:
return <DealTicketLimitAmount {...props} />;
default: {
throw new Error('Invalid ticket type ' + type);
throw new Error('Invalid ticket type');
}
}
};
@@ -1,8 +1,8 @@
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
import {
isStopOrderType,
useDealTicketFormValues,
} from '../../hooks/use-form-values';
DealTicketType,
useDealTicketTypeStore,
} from '../../hooks/use-type-store';
import { StopOrder } from './deal-ticket-stop-order';
import {
useStaticMarketData,
@@ -25,9 +25,7 @@ export const DealTicketContainer = ({
marketId,
...props
}: DealTicketContainerProps) => {
const showStopOrder = useDealTicketFormValues((state) =>
isStopOrderType(state.formValues[marketId]?.type)
);
const type = useDealTicketTypeStore((state) => state.type[marketId]);
const {
data: market,
error: marketError,
@@ -50,7 +48,9 @@ export const DealTicketContainer = ({
reload={reload}
>
{market && marketData ? (
FLAGS.STOP_ORDERS && showStopOrder ? (
FLAGS.STOP_ORDERS &&
(type === DealTicketType.StopLimit ||
type === DealTicketType.StopMarket) ? (
<StopOrder
market={market}
marketPrice={marketPrice}
@@ -5,8 +5,8 @@ import type { DealTicketAmountProps } from './deal-ticket-amount';
import { Controller } from 'react-hook-form';
export type DealTicketLimitAmountProps = Omit<
DealTicketAmountProps,
'marketData' | 'type'
Omit<DealTicketAmountProps, 'marketData'>,
'orderType'
>;
export const DealTicketLimitAmount = ({
@@ -14,6 +14,9 @@ export const DealTicketLimitAmount = ({
market,
sizeError,
priceError,
update,
price,
size,
}: DealTicketLimitAmountProps) => {
const priceStep = toDecimal(market?.decimalPlaces);
const sizeStep = toDecimal(market?.positionDecimalPlaces);
@@ -59,16 +62,17 @@ export const DealTicketLimitAmount = ({
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={({ field }) => (
render={() => (
<Input
id="input-order-size-limit"
className="w-full"
type="number"
value={size}
onChange={(e) => update({ size: e.target.value })}
step={sizeStep}
min={sizeStep}
data-testid="order-size"
onWheel={(e) => e.currentTarget.blur()}
{...field}
/>
)}
/>
@@ -91,17 +95,19 @@ export const DealTicketLimitAmount = ({
value: priceStep,
message: t('Price cannot be lower than ' + priceStep),
},
// @ts-ignore this fulfills the interface but still errors
validate: validateAmount(priceStep, 'Price'),
}}
render={({ field }) => (
render={() => (
<Input
id="input-price-quote"
className="w-full"
type="number"
value={price}
onChange={(e) => update({ price: e.target.value })}
step={priceStep}
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
{...field}
/>
)}
/>
@@ -10,7 +10,10 @@ import type { DealTicketAmountProps } from './deal-ticket-amount';
import { Controller } from 'react-hook-form';
import classNames from 'classnames';
export type DealTicketMarketAmountProps = Omit<DealTicketAmountProps, 'type'>;
export type DealTicketMarketAmountProps = Omit<
DealTicketAmountProps,
'orderType'
>;
export const DealTicketMarketAmount = ({
control,
@@ -18,6 +21,8 @@ export const DealTicketMarketAmount = ({
marketData,
marketPrice,
sizeError,
update,
size,
}: DealTicketMarketAmountProps) => {
const quoteName = market.tradableInstrument.instrument.product.quoteName;
const sizeStep = toDecimal(market?.positionDecimalPlaces);
@@ -45,16 +50,17 @@ export const DealTicketMarketAmount = ({
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={({ field }) => (
render={() => (
<Input
id="input-order-size-market"
className="w-full"
type="number"
value={size}
onChange={(e) => update({ size: e.target.value })}
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
{...field}
/>
)}
/>
@@ -1,6 +1,7 @@
import { Controller, type Control } from 'react-hook-form';
import type { Market } from '@vegaprotocol/markets';
import type { OrderFormValues } from '../../hooks/use-form-values';
import type { OrderObj } from '@vegaprotocol/orders';
import type { OrderFormFields } from '../../hooks/use-order-form';
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
@@ -11,21 +12,25 @@ import {
} from '@vegaprotocol/ui-toolkit';
export interface DealTicketSizeIcebergProps {
control: Control<OrderFormValues>;
control: Control<OrderFormFields>;
market: Market;
peakSizeError?: string;
minimumVisibleSizeError?: string;
update: (obj: Partial<OrderObj>) => void;
peakSize: string;
minimumVisibleSize: string;
size: string;
peakSize?: string;
}
export const DealTicketSizeIceberg = ({
control,
market,
update,
peakSizeError,
minimumVisibleSizeError,
size,
peakSize,
minimumVisibleSize,
size,
}: DealTicketSizeIcebergProps) => {
const sizeStep = toDecimal(market?.positionDecimalPlaces);
@@ -75,7 +80,7 @@ export const DealTicketSizeIceberg = ({
className="!mb-1"
>
<Controller
name="peakSize"
name="icebergOpts.peakSize"
control={control}
rules={{
required: t('You need to provide a peak size'),
@@ -92,17 +97,25 @@ export const DealTicketSizeIceberg = ({
},
validate: validateAmount(sizeStep, 'peakSize'),
}}
render={({ field }) => (
render={() => (
<Input
id="input-order-peak-size"
className="w-full"
type="number"
value={peakSize}
onChange={(e) =>
update({
icebergOpts: {
peakSize: e.target.value,
minimumVisibleSize,
},
})
}
step={sizeStep}
min={sizeStep}
max={size}
data-testid="order-peak-size"
onWheel={(e) => e.currentTarget.blur()}
{...field}
/>
)}
/>
@@ -131,7 +144,7 @@ export const DealTicketSizeIceberg = ({
className="!mb-1"
>
<Controller
name="minimumVisibleSize"
name="icebergOpts.minimumVisibleSize"
control={control}
rules={{
required: t('You need to provide a minimum visible size'),
@@ -141,7 +154,7 @@ export const DealTicketSizeIceberg = ({
'Minimum visible size cannot be lower than ' + sizeStep
),
},
max: peakSize && {
max: {
value: peakSize,
message: t(
'Minimum visible size cannot be greater than the peak size (%s)',
@@ -150,17 +163,25 @@ export const DealTicketSizeIceberg = ({
},
validate: validateAmount(sizeStep, 'minimumVisibleSize'),
}}
render={({ field }) => (
render={() => (
<Input
id="input-order-minimum-size"
className="w-full"
type="number"
value={minimumVisibleSize}
onChange={(e) =>
update({
icebergOpts: {
peakSize,
minimumVisibleSize: e.target.value,
},
})
}
step={sizeStep}
min={sizeStep}
max={peakSize}
data-testid="order-minimum-size"
onWheel={(e) => e.currentTarget.blur()}
{...field}
/>
)}
/>
@@ -6,11 +6,8 @@ import { generateMarket } from '../../test-helpers';
import { StopOrder } from './deal-ticket-stop-order';
import * as Schema from '@vegaprotocol/types';
import { MockedProvider } from '@apollo/client/testing';
import type { StopOrderFormValues } from '../../hooks/use-form-values';
import {
DealTicketType,
useDealTicketFormValues,
} from '../../hooks/use-form-values';
import type { StopOrderFormValues } from '../../hooks/use-stop-order-form-values';
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
import type { FeatureFlags } from '@vegaprotocol/environment';
jest.mock('zustand');
@@ -134,11 +131,9 @@ describe('StopOrder', () => {
expiresAt: '2023-07-27T16:43:27.000',
};
useDealTicketFormValues.setState({
useStopOrderFormValues.setState({
formValues: {
[market.id]: {
[DealTicketType.StopLimit]: values,
},
[market.id]: values,
},
});
@@ -163,13 +158,16 @@ describe('StopOrder', () => {
);
});
it('does not submit if no wallet connected', async () => {
it('shows no wallet warning and do not submit if no wallet connected', async () => {
render(generateJsx(null));
await userEvent.type(screen.getByTestId(sizeInput), '1');
await userEvent.type(screen.getByTestId(priceInput), '1');
await userEvent.type(screen.getByTestId(triggerPriceInput), '1');
await userEvent.click(screen.getByTestId(submitButton));
expect(submit).not.toBeCalled();
expect(
screen.getByTestId('deal-ticket-connect-wallet')
).toBeInTheDocument();
});
it('calls submit if form is valid', async () => {
@@ -212,13 +210,11 @@ describe('StopOrder', () => {
// 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();
// 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();
// to small value should be invalid
@@ -1,3 +1,4 @@
import type { FormEventHandler } from 'react';
import { useRef, useCallback, useEffect } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
@@ -7,7 +8,7 @@ import {
toDecimal,
validateAmount,
} from '@vegaprotocol/utils';
import { useForm, Controller, useController } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import * as Schema from '@vegaprotocol/types';
import {
Radio,
@@ -23,22 +24,22 @@ import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
import { t } from '@vegaprotocol/i18n';
import { ExpirySelector } from './expiry-selector';
import { SideSelector } from './side-selector';
import { timeInForceLabel } from '@vegaprotocol/orders';
import { timeInForceLabel, useOrder } from '@vegaprotocol/orders';
import {
NoWalletWarning,
REDUCE_ONLY_TOOLTIP,
stopSubmit,
getNotionalSize,
useNotionalSize,
} from './deal-ticket';
import { TypeToggle } from './type-selector';
import {
useDealTicketFormValues,
DealTicketType,
useStopOrderFormValues,
type StopOrderFormValues,
dealTicketTypeToOrderType,
isStopOrderType,
} from '../../hooks/use-form-values';
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-submission';
} from '../../hooks/use-stop-order-form-values';
import {
DealTicketType,
useDealTicketTypeStore,
} from '../../hooks/use-type-store';
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-stop-order-submission';
import { DealTicketButton } from './deal-ticket-button';
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
import { validateExpiration } from '../../utils';
@@ -49,36 +50,32 @@ export interface StopOrderProps {
submit: (order: StopOrdersSubmission) => void;
}
const getDefaultValues = (
type: Schema.OrderType,
storedValues?: Partial<StopOrderFormValues>
): StopOrderFormValues => ({
type,
const defaultValues: Partial<StopOrderFormValues> = {
type: Schema.OrderType.TYPE_LIMIT,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
triggerType: 'price',
triggerDirection:
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
expire: false,
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT,
size: '0',
...storedValues,
});
};
const stopSubmit: FormEventHandler = (e) => e.preventDefault();
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
const { pubKey, isReadOnly } = useVegaWallet();
const setType = useDealTicketFormValues((state) => state.setType);
const updateStoredFormValues = useDealTicketFormValues(
(state) => state.updateStopOrder
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
const [, updateOrder] = useOrder(market.id);
const updateStoredFormValues = useStopOrderFormValues(
(state) => state.update
);
const storedFormValues = useDealTicketFormValues(
const storedFormValues = useStopOrderFormValues(
(state) => state.formValues[market.id]
);
const dealTicketType = storedFormValues?.type ?? DealTicketType.StopLimit;
const type = dealTicketTypeToOrderType(dealTicketType);
const { handleSubmit, setValue, watch, control, formState, reset } =
const { handleSubmit, setValue, watch, control, formState } =
useForm<StopOrderFormValues>({
defaultValues: getDefaultValues(type, storedFormValues?.[dealTicketType]),
defaultValues: { ...defaultValues, ...storedFormValues },
});
const { errors } = formState;
const lastSubmitTime = useRef(0);
@@ -105,22 +102,16 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
const triggerType = watch('triggerType');
const triggerPrice = watch('triggerPrice');
const timeInForce = watch('timeInForce');
const type = watch('type');
const rawPrice = watch('price');
const rawSize = watch('size');
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]);
if (storedFormValues?.size && rawSize !== storedFormValues?.size) {
setValue('size', storedFormValues.size);
}
if (storedFormValues?.price && rawPrice !== storedFormValues?.price) {
setValue('price', storedFormValues.price);
}
const isPriceTrigger = triggerType === 'price';
const size = removeDecimal(rawSize, market.positionDecimalPlaces);
@@ -136,7 +127,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
: marketPrice
);
const notionalSize = getNotionalSize(
const notionalSize = useNotionalSize(
price,
size,
market.decimalPlaces,
@@ -162,28 +153,47 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
? formatNumber(triggerPrice, market.decimalPlaces)
: undefined;
useController({
name: 'type',
control,
});
return (
<form
onSubmit={isReadOnly || !pubKey ? stopSubmit : handleSubmit(onSubmit)}
noValidate
>
<TypeToggle
value={dealTicketType}
onValueChange={(dealTicketType) => {
setType(market.id, dealTicketType);
if (isStopOrderType(dealTicketType)) {
reset(
getDefaultValues(
dealTicketTypeToOrderType(dealTicketType),
storedFormValues?.[dealTicketType]
)
);
}
<Controller
name="type"
control={control}
render={({ field }) => {
const { value } = field;
return (
<TypeToggle
value={
value === Schema.OrderType.TYPE_LIMIT
? DealTicketType.StopLimit
: DealTicketType.StopMarket
}
onValueChange={(value) => {
const type = value as DealTicketType;
setDealTicketType(market.id, type);
if (
type === DealTicketType.Limit ||
type === DealTicketType.Market
) {
updateOrder({
type:
type === DealTicketType.Limit
? Schema.OrderType.TYPE_LIMIT
: Schema.OrderType.TYPE_MARKET,
});
return;
}
setValue(
'type',
type === DealTicketType.StopLimit
? Schema.OrderType.TYPE_LIMIT
: Schema.OrderType.TYPE_MARKET
);
}}
/>
);
}}
/>
{errors.type && (
@@ -558,7 +568,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
</div>
</>
)}
<NoWalletWarning isReadOnly={isReadOnly} />
<NoWalletWarning pubKey={pubKey} isReadOnly={isReadOnly} asset={asset} />
<DealTicketButton side={side} label={t('Submit Stop Order')} />
<DealTicketFeeDetails
order={{
@@ -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,
render,
renderHook,
screen,
waitFor,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { generateMarket, generateMarketData } from '../../test-helpers';
import { DealTicket } from './deal-ticket';
@@ -9,10 +15,7 @@ import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { addDecimal } from '@vegaprotocol/utils';
import type { OrdersQuery } from '@vegaprotocol/orders';
import {
DealTicketType,
useDealTicketFormValues,
} from '../../hooks/use-form-values';
import { useCreateOrderStore } from '@vegaprotocol/orders';
import * as positionsTools from '@vegaprotocol/positions';
import { OrdersDocument } from '@vegaprotocol/orders';
@@ -47,6 +50,9 @@ function generateJsx(mocks: MockedResponse[] = []) {
}
describe('DealTicket', () => {
const { result } = renderHook(() => useCreateOrderStore());
const useOrderStore = result.current;
beforeEach(() => {
jest.clearAllMocks();
localStorage.clear();
@@ -160,11 +166,9 @@ describe('DealTicket', () => {
persist: true,
};
useDealTicketFormValues.setState({
formValues: {
[expectedOrder.marketId]: {
[DealTicketType.Limit]: expectedOrder,
},
useOrderStore.setState({
orders: {
[expectedOrder.marketId]: expectedOrder,
},
});
@@ -200,11 +204,9 @@ describe('DealTicket', () => {
reduceOnly: true,
postOnly: false,
};
useDealTicketFormValues.setState({
formValues: {
[expectedOrder.marketId]: {
[DealTicketType.Limit]: expectedOrder,
},
useOrderStore.setState({
orders: {
[expectedOrder.marketId]: expectedOrder,
},
});
@@ -245,11 +247,9 @@ describe('DealTicket', () => {
postOnly: true,
};
useDealTicketFormValues.setState({
formValues: {
[expectedOrder.marketId]: {
[DealTicketType.Limit]: expectedOrder,
},
useOrderStore.setState({
orders: {
[expectedOrder.marketId]: expectedOrder,
},
});
@@ -295,11 +295,9 @@ describe('DealTicket', () => {
},
};
useDealTicketFormValues.setState({
formValues: {
[expectedOrder.marketId]: {
[DealTicketType.Limit]: expectedOrder,
},
useOrderStore.setState({
orders: {
[expectedOrder.marketId]: expectedOrder,
},
});
@@ -341,11 +339,9 @@ describe('DealTicket', () => {
reduceOnly: false,
postOnly: false,
};
useDealTicketFormValues.setState({
formValues: {
[expectedOrder.marketId]: {
[DealTicketType.Limit]: expectedOrder,
},
useOrderStore.setState({
orders: {
[expectedOrder.marketId]: expectedOrder,
},
});
@@ -374,7 +370,6 @@ describe('DealTicket', () => {
expect(screen.getByTestId('iceberg')).not.toBeChecked();
});
// eslint-disable-next-line jest/no-disabled-tests
it('handles TIF select box dependent on order type', async () => {
render(generateJsx());
@@ -1,8 +1,7 @@
import { t } from '@vegaprotocol/i18n';
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 { memo, useCallback, useEffect, useState, useRef, useMemo } from 'react';
import { Controller } from 'react-hook-form';
import { DealTicketAmount } from './deal-ticket-amount';
import { DealTicketButton } from './deal-ticket-button';
import {
@@ -14,10 +13,14 @@ import { SideSelector } from './side-selector';
import { TimeInForceSelector } from './time-in-force-selector';
import { TypeSelector } from './type-selector';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { mapFormValuesToOrderSubmission } from '../../utils/map-form-values-to-submission';
import {
normalizeOrderSubmission,
useVegaWallet,
useVegaWalletDialogStore,
} from '@vegaprotocol/wallet';
import {
Checkbox,
ExternalLink,
InputError,
Intent,
Notification,
@@ -53,15 +56,14 @@ import {
useAccountBalance,
} from '@vegaprotocol/accounts';
import { OrderType } from '@vegaprotocol/types';
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
import { useOrderForm } from '../../hooks/use-order-form';
import { useDataProvider } from '@vegaprotocol/data-provider';
import {
DealTicketType,
dealTicketTypeToOrderType,
isStopOrderType,
} from '../../hooks/use-form-values';
import type { OrderFormValues } from '../../hooks/use-form-values';
import { useDealTicketFormValues } from '../../hooks/use-form-values';
useDealTicketTypeStore,
} from '../../hooks/use-type-store';
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
import noop from 'lodash/noop';
@@ -78,42 +80,23 @@ export interface DealTicketProps {
onDeposit: (assetId: string) => void;
}
export const getNotionalSize = (
export const useNotionalSize = (
price: string | null | undefined,
size: string | undefined,
decimalPlaces: number,
positionDecimalPlaces: number
) => {
if (price && size) {
return removeDecimal(
toBigNum(size, positionDecimalPlaces).multipliedBy(
toBigNum(price, decimalPlaces)
),
decimalPlaces
);
}
return null;
};
export const stopSubmit: FormEventHandler = (e) => e.preventDefault();
const getDefaultValues = (
type: Schema.OrderType,
storedValues?: Partial<OrderFormValues>
): OrderFormValues => ({
type,
side: Schema.Side.SIDE_BUY,
timeInForce:
type === Schema.OrderType.TYPE_LIMIT
? Schema.OrderTimeInForce.TIME_IN_FORCE_GTC
: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
size: '0',
price: '0',
expiresAt: undefined,
postOnly: false,
reduceOnly: false,
...storedValues,
});
) =>
useMemo(() => {
if (price && size) {
return removeDecimal(
toBigNum(size, positionDecimalPlaces).multipliedBy(
toBigNum(price, decimalPlaces)
),
decimalPlaces
);
}
return null;
}, [price, size, decimalPlaces, positionDecimalPlaces]);
export const DealTicket = ({
market,
@@ -125,29 +108,32 @@ export const DealTicket = ({
onDeposit,
}: DealTicketProps) => {
const { pubKey, isReadOnly } = useVegaWallet();
const setType = useDealTicketFormValues((state) => state.setType);
const storedFormValues = useDealTicketFormValues(
(state) => state.formValues[market.id]
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
const updateStopOrderFormValues = useStopOrderFormValues(
(state) => state.update
);
const updateStoredFormValues = useDealTicketFormValues(
(state) => state.updateOrder
);
const dealTicketType = storedFormValues?.type ?? DealTicketType.Limit;
const type = dealTicketTypeToOrderType(dealTicketType);
// store last used tif for market so that when changing OrderType the previous TIF
// selection for that type is used when switching back
const [lastTIF, setLastTIF] = useState({
[OrderType.TYPE_MARKET]: OrderTimeInForce.TIME_IN_FORCE_IOC,
[OrderType.TYPE_LIMIT]: OrderTimeInForce.TIME_IN_FORCE_GTC,
});
const {
control,
reset,
formState: { errors },
errors,
order,
setError,
clearErrors,
update,
handleSubmit,
setValue,
watch,
} = useForm<OrderFormValues>({
defaultValues: getDefaultValues(type, storedFormValues?.[dealTicketType]),
});
} = useOrderForm(market.id);
const lastSubmitTime = useRef(0);
const asset = market.tradableInstrument.instrument.product.settlementAsset;
const {
accountBalance: marginAccountBalance,
loading: loadingMarginAccountBalance,
@@ -163,54 +149,24 @@ export const DealTicket = ({
).toString();
const { marketState, marketTradingMode } = marketData;
const timeInForce = watch('timeInForce');
const side = watch('side');
const rawSize = watch('size');
const rawPrice = watch('price');
const iceberg = watch('iceberg');
const peakSize = watch('peakSize');
const normalizedOrder =
order &&
normalizeOrderSubmission(
order,
market.decimalPlaces,
market.positionDecimalPlaces
);
useEffect(() => {
const size = storedFormValues?.[dealTicketType]?.size;
if (size && rawSize !== size) {
setValue('size', size);
}
}, [storedFormValues, dealTicketType, rawSize, setValue]);
const price = useMemo(() => {
return (
normalizedOrder &&
marketPrice &&
getDerivedPrice(normalizedOrder, marketPrice)
);
}, [normalizedOrder, marketPrice]);
useEffect(() => {
const price = storedFormValues?.[dealTicketType]?.price;
if (price && rawPrice !== price) {
setValue('price', price);
}
}, [storedFormValues, dealTicketType, rawPrice, setValue]);
useEffect(() => {
const subscription = watch((value, { name, type }) => {
updateStoredFormValues(market.id, value);
});
return () => subscription.unsubscribe();
}, [watch, market.id, updateStoredFormValues]);
const normalizedOrder = mapFormValuesToOrderSubmission(
{
price: rawPrice || undefined,
side,
size: rawSize,
timeInForce,
type,
},
market.id,
market.decimalPlaces,
market.positionDecimalPlaces
);
const price =
normalizedOrder &&
marketPrice &&
getDerivedPrice(normalizedOrder, marketPrice);
const notionalSize = getNotionalSize(
const notionalSize = useNotionalSize(
price,
normalizedOrder?.size,
market.decimalPlaces,
@@ -254,20 +210,22 @@ export const DealTicket = ({
const assetSymbol =
market.tradableInstrument.instrument.product.settlementAsset.symbol;
const summaryError = useMemo(() => {
useEffect(() => {
if (!pubKey) {
return {
setError('summary', {
message: t('No public key selected'),
type: SummaryValidationType.NoPubKey,
};
});
return;
}
const marketStateError = validateMarketState(marketState);
if (marketStateError !== true) {
return {
setError('summary', {
message: marketStateError,
type: SummaryValidationType.MarketState,
};
});
return;
}
const hasNoBalance =
@@ -276,21 +234,24 @@ export const DealTicket = ({
hasNoBalance &&
!(loadingMarginAccountBalance || loadingGeneralAccountBalance)
) {
return {
setError('summary', {
message: SummaryValidationType.NoCollateral,
type: SummaryValidationType.NoCollateral,
};
});
return;
}
const marketTradingModeError = validateMarketTradingMode(marketTradingMode);
if (marketTradingModeError !== true) {
return {
setError('summary', {
message: marketTradingModeError,
type: SummaryValidationType.TradingMode,
};
});
return;
}
return undefined;
// No error found above clear the error in case it was active on a previous render
clearErrors('summary');
}, [
marketState,
marketTradingMode,
@@ -299,83 +260,156 @@ export const DealTicket = ({
loadingMarginAccountBalance,
loadingGeneralAccountBalance,
pubKey,
setError,
clearErrors,
]);
const disablePostOnlyCheckbox = [
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
].includes(timeInForce);
const disablePostOnlyCheckbox = useMemo(() => {
const disabled = order
? [
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
].includes(order.timeInForce)
: true;
return disabled;
}, [order]);
const disableReduceOnlyCheckbox = !disablePostOnlyCheckbox;
const disableReduceOnlyCheckbox = useMemo(() => {
const disabled = order
? ![
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
].includes(order.timeInForce)
: true;
return disabled;
}, [order]);
const onSubmit = useCallback(
(formValues: OrderFormValues) => {
(order: OrderSubmission) => {
const now = new Date().getTime();
if (lastSubmitTime.current && now - lastSubmitTime.current < 1000) {
return;
}
submit(
mapFormValuesToOrderSubmission(
formValues,
market.id,
normalizeOrderSubmission(
order,
market.decimalPlaces,
market.positionDecimalPlaces
)
);
lastSubmitTime.current = now;
},
[submit, market.decimalPlaces, market.positionDecimalPlaces, market.id]
[submit, market.decimalPlaces, market.positionDecimalPlaces]
);
useController({
name: 'type',
control,
rules: {
validate: validateType(marketData.marketTradingMode, marketData.trigger),
},
});
// if an order doesn't exist one will be created by the store immediately
if (!order || !normalizedOrder) {
return null;
}
return (
<form
onSubmit={
isReadOnly || !pubKey
? stopSubmit
: handleSubmit(summaryError ? noop : onSubmit)
}
onSubmit={isReadOnly ? noop : handleSubmit(onSubmit)}
noValidate
data-testid="deal-ticket-form"
>
<TypeSelector
value={dealTicketType}
onValueChange={(dealTicketType) => {
setType(market.id, dealTicketType);
if (!isStopOrderType(dealTicketType)) {
reset(
getDefaultValues(
dealTicketTypeToOrderType(dealTicketType),
storedFormValues?.[dealTicketType]
)
);
}
<Controller
name="type"
control={control}
rules={{
validate: validateType(
marketData.marketTradingMode,
marketData.trigger
),
}}
market={market}
marketData={marketData}
errorMessage={errors.type?.message}
render={() => (
<TypeSelector
value={
order.type === OrderType.TYPE_LIMIT
? DealTicketType.Limit
: DealTicketType.Market
}
onValueChange={(dealTicketType) => {
setDealTicketType(market.id, dealTicketType);
if (
dealTicketType !== DealTicketType.Limit &&
dealTicketType !== DealTicketType.Market
) {
updateStopOrderFormValues(market.id, {
type:
dealTicketType === DealTicketType.StopLimit
? OrderType.TYPE_LIMIT
: OrderType.TYPE_MARKET,
});
return;
}
const type =
dealTicketType === DealTicketType.Limit
? OrderType.TYPE_LIMIT
: OrderType.TYPE_MARKET;
update({
type,
// when changing type also update the TIF to what was last used of new type
timeInForce: lastTIF[type] || order.timeInForce,
postOnly:
type === OrderType.TYPE_MARKET ? false : order.postOnly,
iceberg:
type === OrderType.TYPE_MARKET ||
[
OrderTimeInForce.TIME_IN_FORCE_FOK,
OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(lastTIF[type] || order.timeInForce)
? false
: order.iceberg,
icebergOpts:
type === OrderType.TYPE_MARKET ||
[
OrderTimeInForce.TIME_IN_FORCE_FOK,
OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(lastTIF[type] || order.timeInForce)
? undefined
: order.icebergOpts,
reduceOnly:
type === OrderType.TYPE_LIMIT &&
![
OrderTimeInForce.TIME_IN_FORCE_FOK,
OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(lastTIF[type] || order.timeInForce)
? false
: order.postOnly,
expiresAt: undefined,
});
clearErrors(['expiresAt', 'price']);
}}
market={market}
marketData={marketData}
errorMessage={errors.type?.message}
/>
)}
/>
<Controller
name="side"
control={control}
render={({ field }) => (
<SideSelector value={field.value} onValueChange={field.onChange} />
render={() => (
<SideSelector
value={order.side}
onValueChange={(side) => {
update({ side });
}}
/>
)}
/>
<DealTicketAmount
type={type}
control={control}
orderType={order.type}
market={market}
marketData={marketData}
marketPrice={marketPrice || undefined}
sizeError={errors.size?.message}
priceError={errors.price?.message}
update={update}
size={order.size}
price={order.price}
/>
<Controller
name="timeInForce"
@@ -386,29 +420,58 @@ export const DealTicket = ({
marketData.trigger
),
}}
render={({ field }) => (
render={() => (
<TimeInForceSelector
value={field.value}
orderType={type}
onSelect={field.onChange}
value={order.timeInForce}
orderType={order.type}
onSelect={(timeInForce) => {
// Reset post only and reduce only when changing TIF
update({
timeInForce,
postOnly: [
OrderTimeInForce.TIME_IN_FORCE_FOK,
OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(timeInForce)
? false
: order.postOnly,
reduceOnly: ![
OrderTimeInForce.TIME_IN_FORCE_FOK,
OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(timeInForce)
? false
: order.reduceOnly,
});
// Set TIF value for the given order type, so that when switching
// types we know the last used TIF for the given order type
setLastTIF((curr) => ({
...curr,
[order.type]: timeInForce,
expiresAt: undefined,
}));
clearErrors('expiresAt');
}}
market={market}
marketData={marketData}
errorMessage={errors.timeInForce?.message}
/>
)}
/>
{type === Schema.OrderType.TYPE_LIMIT &&
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
{order.type === Schema.OrderType.TYPE_LIMIT &&
order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT && (
<Controller
name="expiresAt"
control={control}
rules={{
validate: validateExpiration,
}}
render={({ field }) => (
render={() => (
<ExpirySelector
value={field.value}
onSelect={(expiresAt) => field.onChange(expiresAt)}
value={order.expiresAt}
onSelect={(expiresAt) =>
update({
expiresAt: expiresAt || undefined,
})
}
errorMessage={errors.expiresAt?.message}
/>
)}
@@ -418,14 +481,13 @@ export const DealTicket = ({
<Controller
name="postOnly"
control={control}
render={({ field }) => (
render={() => (
<Checkbox
name="post-only"
checked={!disablePostOnlyCheckbox && field.value}
checked={order.postOnly}
disabled={disablePostOnlyCheckbox}
onCheckedChange={(postOnly) => {
field.onChange(postOnly);
setValue('reduceOnly', false);
onCheckedChange={() => {
update({ postOnly: !order.postOnly, reduceOnly: false });
}}
label={
<Tooltip
@@ -450,14 +512,13 @@ export const DealTicket = ({
<Controller
name="reduceOnly"
control={control}
render={({ field }) => (
render={() => (
<Checkbox
name="reduce-only"
checked={!disableReduceOnlyCheckbox && field.value}
checked={order.reduceOnly}
disabled={disableReduceOnlyCheckbox}
onCheckedChange={(reduceOnly) => {
field.onChange(reduceOnly);
setValue('postOnly', false);
onCheckedChange={() => {
update({ postOnly: false, reduceOnly: !order.reduceOnly });
}}
label={
<Tooltip
@@ -478,49 +539,53 @@ export const DealTicket = ({
)}
/>
</div>
{type === Schema.OrderType.TYPE_LIMIT && (
<>
<div className="flex gap-2 pb-2 justify-between">
<Controller
name="iceberg"
control={control}
render={({ field }) => (
<Checkbox
name="iceberg"
checked={field.value}
onCheckedChange={field.onChange}
label={
<Tooltip
description={
<p>
{t(`Trade only a fraction of the order size at once.
<div className="flex gap-2 pb-2 justify-between">
{order.type === Schema.OrderType.TYPE_LIMIT && (
<Controller
name="iceberg"
control={control}
render={() => (
<Checkbox
name="iceberg"
checked={order.iceberg}
onCheckedChange={() => {
update({ iceberg: !order.iceberg, icebergOpts: undefined });
}}
label={
<Tooltip
description={
<p>
{t(`Trade only a fraction of the order size at once.
After the peak size of the order has traded, the size is reset. This is repeated until the order is cancelled, expires, or its full volume trades away.
For example, an iceberg order with a size of 1000 and a peak size of 100 will effectively be split into 10 orders with a size of 100 each.
Note that the full volume of the order is not hidden and is still reflected in the order book.`)}
</p>
}
>
<span className="text-xs">{t('Iceberg')}</span>
</Tooltip>
}
/>
)}
/>
</div>
{iceberg && (
<DealTicketSizeIceberg
market={market}
peakSizeError={errors.peakSize?.message}
minimumVisibleSizeError={errors.minimumVisibleSize?.message}
control={control}
size={rawSize}
peakSize={peakSize}
/>
)}
</>
</p>
}
>
<span className="text-xs">{t('Iceberg')}</span>
</Tooltip>
}
/>
)}
/>
)}
</div>
{order.iceberg && (
<DealTicketSizeIceberg
update={update}
market={market}
peakSizeError={errors.icebergOpts?.peakSize?.message}
minimumVisibleSizeError={
errors.icebergOpts?.minimumVisibleSize?.message
}
control={control}
size={order.size}
peakSize={order.icebergOpts?.peakSize || ''}
minimumVisibleSize={order.icebergOpts?.minimumVisibleSize || ''}
/>
)}
<SummaryMessage
error={summaryError}
errorMessage={errors.summary?.message}
asset={asset}
marketTradingMode={marketData.marketTradingMode}
balance={balance}
@@ -533,7 +598,7 @@ export const DealTicket = ({
onClickCollateral={onClickCollateral}
onDeposit={onDeposit}
/>
<DealTicketButton side={side} />
<DealTicketButton side={order.side} />
<DealTicketFeeDetails
order={
normalizedOrder && { ...normalizedOrder, price: price || undefined }
@@ -559,7 +624,7 @@ export const DealTicket = ({
* renders warnings about current state of the market
*/
interface SummaryMessageProps {
error?: { message: string; type: string };
errorMessage?: string;
asset: { id: string; symbol: string; name: string; decimals: number };
marketTradingMode: MarketData['marketTradingMode'];
balance: string;
@@ -572,7 +637,13 @@ interface SummaryMessageProps {
export const NoWalletWarning = ({
isReadOnly,
}: Pick<SummaryMessageProps, 'isReadOnly'>) => {
pubKey,
asset,
}: Pick<SummaryMessageProps, 'isReadOnly' | 'pubKey' | 'asset'>) => {
const assetSymbol = asset.symbol;
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
if (isReadOnly) {
return (
<div className="mb-2">
@@ -584,12 +655,37 @@ export const NoWalletWarning = ({
</div>
);
}
if (!pubKey) {
return (
<div className="mb-2">
<Notification
testId={'deal-ticket-connect-wallet'}
intent={Intent.Warning}
message={
<p className="text-sm pb-2">
You need a{' '}
<ExternalLink href="https://vega.xyz/wallet">
Vega wallet
</ExternalLink>{' '}
with {assetSymbol} to start trading in this market.
</p>
}
buttonProps={{
text: t('Connect wallet'),
action: openVegaWalletDialog,
dataTestId: 'order-connect-wallet',
size: 'small',
}}
/>
</div>
);
}
return null;
};
const SummaryMessage = memo(
({
error,
errorMessage,
asset,
marketTradingMode,
balance,
@@ -602,10 +698,16 @@ const SummaryMessage = memo(
// Specific error UI for if balance is so we can
// render a deposit dialog
if (isReadOnly || !pubKey) {
return <NoWalletWarning isReadOnly={isReadOnly} />;
return (
<NoWalletWarning
isReadOnly={isReadOnly}
asset={asset}
pubKey={pubKey}
/>
);
}
if (error?.type === SummaryValidationType.NoCollateral) {
if (errorMessage === SummaryValidationType.NoCollateral) {
return (
<div className="mb-2">
<ZeroBalanceError
@@ -619,11 +721,11 @@ const SummaryMessage = memo(
// If we have any other full error which prevents
// submission render that first
if (error?.message) {
if (errorMessage) {
return (
<div className="mb-2">
<InputError testId="deal-ticket-error-message-summary">
{error?.message}
{errorMessage}
</InputError>
</div>
);
@@ -21,13 +21,6 @@ interface TimeInForceSelectorProps {
errorMessage?: string;
}
const typeLimitOptions = Object.entries(Schema.OrderTimeInForce);
const typeMarketOptions = typeLimitOptions.filter(
([_, timeInForce]) =>
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_FOK ||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
);
export const TimeInForceSelector = ({
value,
orderType,
@@ -38,8 +31,12 @@ export const TimeInForceSelector = ({
}: TimeInForceSelectorProps) => {
const options =
orderType === Schema.OrderType.TYPE_LIMIT
? typeLimitOptions
: typeMarketOptions;
? Object.entries(Schema.OrderTimeInForce)
: Object.entries(Schema.OrderTimeInForce).filter(
([_, timeInForce]) =>
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_FOK ||
timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
);
const renderError = (errorType: string) => {
if (errorType === MarketModeValidationType.Auction) {
@@ -16,7 +16,7 @@ import { t } from '@vegaprotocol/i18n';
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
import { compileGridData } from '../trading-mode-tooltip';
import { MarketModeValidationType } from '../../constants';
import { DealTicketType } from '../../hooks/use-form-values';
import { DealTicketType } from '../../hooks/use-type-store';
import * as RadioGroup from '@radix-ui/react-radio-group';
import classNames from 'classnames';
import { FLAGS } from '@vegaprotocol/environment';
+2 -1
View File
@@ -1,3 +1,4 @@
export * from './__generated__/EstimateOrder';
export * from './use-estimate-fees';
export * from './use-form-values';
export * from './use-type-store';
export * from './use-stop-order-form-values';
@@ -1,144 +0,0 @@
import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware';
import type { OrderTimeInForce, Side, OrderType } from '@vegaprotocol/types';
import * as Schema from '@vegaprotocol/types';
import { immer } from 'zustand/middleware/immer';
export enum DealTicketType {
Limit = 'Limit',
Market = 'Market',
StopLimit = 'StopLimit',
StopMarket = 'StopMarket',
}
export interface StopOrderFormValues {
side: Side;
triggerDirection: Schema.StopOrderTriggerDirection;
triggerType: 'price' | 'trailingPercentOffset';
triggerPrice?: string;
triggerTrailingPercentOffset?: string;
type: OrderType;
size: string;
timeInForce: OrderTimeInForce;
price?: string;
expire: boolean;
expiryStrategy?: Schema.StopOrderExpiryStrategy;
expiresAt?: string;
}
export type OrderFormValues = {
type: OrderType;
side: Side;
size: string;
timeInForce: OrderTimeInForce;
price?: string;
expiresAt?: string | undefined;
postOnly?: boolean;
reduceOnly?: boolean;
iceberg?: boolean;
peakSize?: string;
minimumVisibleSize?: string;
};
type UpdateOrder = (marketId: string, values: Partial<OrderFormValues>) => void;
type UpdateStopOrder = (
marketId: string,
values: Partial<StopOrderFormValues>
) => void;
type Store = {
updateOrder: UpdateOrder;
updateStopOrder: UpdateStopOrder;
setType: (marketId: string, value: DealTicketType) => void;
updateAll: (
marketId: string,
values: { size?: string; price?: string }
) => void;
formValues: Record<
string,
| {
[DealTicketType.Limit]?: Partial<OrderFormValues>;
[DealTicketType.Market]?: Partial<OrderFormValues>;
[DealTicketType.StopLimit]?: Partial<StopOrderFormValues>;
[DealTicketType.StopMarket]?: Partial<StopOrderFormValues>;
type?: DealTicketType;
}
| undefined
>;
};
export const dealTicketTypeToOrderType = (dealTicketType?: DealTicketType) =>
dealTicketType === DealTicketType.Limit ||
dealTicketType === DealTicketType.StopLimit
? Schema.OrderType.TYPE_LIMIT
: Schema.OrderType.TYPE_MARKET;
export const isStopOrderType = (dealTicketType?: DealTicketType) =>
dealTicketType === DealTicketType.StopLimit ||
dealTicketType === DealTicketType.StopMarket;
export const useDealTicketFormValues = create<Store>()(
immer(
persist(
subscribeWithSelector((set) => ({
formValues: {},
updateStopOrder: (marketId, formValues) => {
set((state) => {
const type =
formValues.type === Schema.OrderType.TYPE_LIMIT
? DealTicketType.StopLimit
: DealTicketType.StopMarket;
const market = state.formValues[marketId] || {};
if (!state.formValues[marketId]) {
state.formValues[marketId] = market;
}
market[type] = Object.assign(market[type] ?? {}, formValues);
});
},
updateOrder: (marketId, formValues) => {
set((state) => {
const type =
formValues.type === Schema.OrderType.TYPE_LIMIT
? DealTicketType.Limit
: DealTicketType.Market;
const market = state.formValues[marketId] || {};
if (!state.formValues[marketId]) {
state.formValues[marketId] = market;
}
market[type] = Object.assign(market[type] ?? {}, formValues);
});
},
updateAll: (
marketId: string,
formValues: { size?: string; price?: string }
) => {
set((state) => {
const market = state.formValues[marketId] || {};
if (!state.formValues[marketId]) {
state.formValues[marketId] = market;
}
for (const type of Object.values(DealTicketType)) {
market[type] = Object.assign(market[type] ?? {}, formValues);
}
});
},
setType: (marketId, type) => {
set((state) => {
state.formValues[marketId] = Object.assign(
state.formValues[marketId] ?? {},
{ type }
);
});
},
})),
{
name: 'vega_deal_ticket_store',
}
)
)
);
@@ -0,0 +1,68 @@
import omit from 'lodash/omit';
import { act, renderHook } from '@testing-library/react';
import { getDefaultOrder, useCreateOrderStore } from '@vegaprotocol/orders';
import { useOrderForm } from './use-order-form';
jest.mock('zustand');
describe('useOrderForm', () => {
const marketId = 'market-id';
const setup = (marketId: string) => {
return renderHook(() => useOrderForm(marketId));
};
const { result } = renderHook(() => useCreateOrderStore());
const useOrderStore = result.current;
it('updates form fields when the order changes', async () => {
const order = getDefaultOrder(marketId);
const { result } = setup(marketId);
// expect default values
expect(result.current.order).toEqual(order);
expect(result.current.getValues()).toEqual(order);
const priceUpdate = {
...order,
price: '100',
size: '22',
};
await act(async () => {
useOrderStore.setState({
orders: {
[marketId]: priceUpdate,
},
});
});
// check order store has updated fields
expect(result.current.order).toEqual(priceUpdate);
// check react-hook-form has updated fields
expect(result.current.getValues()).toEqual(priceUpdate);
});
it('removes persist key on submit', async () => {
const order = {
...getDefaultOrder(marketId),
price: '99',
size: '22',
};
const onSubmit = jest.fn();
const { result } = setup(marketId);
await act(async () => {
useOrderStore.setState({
orders: {
[marketId]: order,
},
});
});
await act(async () => {
result.current.handleSubmit(onSubmit)();
});
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit.mock.calls[0][0]).toEqual(omit(order, 'persist'));
expect(onSubmit.mock.calls[0][0].persist).toBeUndefined();
});
});
@@ -0,0 +1,71 @@
import omit from 'lodash/omit';
import type { OrderObj } from '@vegaprotocol/orders';
import { getDefaultOrder, useOrder } from '@vegaprotocol/orders';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import type { OrderSubmission } from '@vegaprotocol/wallet';
export type OrderFormFields = OrderObj & {
summary: string;
};
/**
* Connects the order store to a react-hook-form instance. Any time a field
* changes in the store the form will be updated so that validation rules
* for those fields are applied
*/
export const useOrderForm = (marketId: string) => {
const [order, update] = useOrder(marketId);
const {
control,
formState: { errors, isSubmitted },
handleSubmit,
setError,
setValue,
clearErrors,
getValues,
} = useForm<OrderFormFields>({
// order can be undefined if there is nothing in the store, it
// will be created but the form still needs some default values
defaultValues: order || getDefaultOrder(marketId),
});
// Keep form fields in sync with the store values,
// inputs are updating the store, fields need updating
// to ensure validation rules are applied
useEffect(() => {
if (!order) return;
const currOrder = getValues();
for (const k in order) {
const key = k as keyof typeof order;
const curr = currOrder[key];
const value = order[key];
if (value !== curr) {
setValue(key, value, {
shouldValidate: isSubmitted, // only apply validation after the form has been submitted and failed
shouldDirty: true,
shouldTouch: true,
});
}
}
}, [order, isSubmitted, getValues, setValue]);
const handleSubmitWrapper = (cb: (o: OrderSubmission) => void) => {
return handleSubmit(() => {
// remove the persist and iceberg key from the order in the store, the wallet will reject
// an order that contains unrecognized additional keys
cb(omit(order, 'persist', 'iceberg'));
});
};
return {
order,
update,
control,
errors,
setError,
clearErrors,
getValues, // returned for test purposes only
handleSubmit: handleSubmitWrapper,
};
};
@@ -0,0 +1,62 @@
import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware';
import type { OrderTimeInForce, Side, OrderType } from '@vegaprotocol/types';
import type * as Schema from '@vegaprotocol/types';
export interface StopOrderFormValues {
side: Side;
triggerDirection: Schema.StopOrderTriggerDirection;
triggerType: 'price' | 'trailingPercentOffset';
triggerPrice: string;
triggerTrailingPercentOffset: string;
type: OrderType;
size: string;
timeInForce: OrderTimeInForce;
price?: string;
expire: boolean;
expiryStrategy?: Schema.StopOrderExpiryStrategy;
expiresAt?: string;
}
type StopOrderFormValuesMap = {
[marketId: string]: Partial<StopOrderFormValues> | undefined;
};
type Update = (
marketId: string,
formValues: Partial<StopOrderFormValues>,
persist?: boolean
) => void;
interface Store {
formValues: StopOrderFormValuesMap;
update: Update;
}
export const useStopOrderFormValues = create<Store>()(
persist(
subscribeWithSelector((set) => ({
formValues: {},
update: (marketId, formValues, persist = true) => {
set((state) => {
return {
formValues: {
...state.formValues,
[marketId]: {
...state.formValues[marketId],
...formValues,
},
},
};
});
},
})),
{
name: 'vega_stop_order_store',
}
)
);
@@ -0,0 +1,28 @@
import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware';
export enum DealTicketType {
Limit = 'Limit',
Market = 'Market',
StopLimit = 'StopLimit',
StopMarket = 'StopMarket',
}
export const useDealTicketTypeStore = create<{
set: (marketId: string, type: DealTicketType) => void;
type: Record<string, DealTicketType>;
}>()(
persist(
subscribeWithSelector((set) => ({
type: {},
set: (marketId: string, type: DealTicketType) =>
set((state) => ({
...state,
type: { ...state.type, [marketId]: type },
})),
})),
{
name: 'deal_ticket_type',
}
)
);
@@ -1,64 +1,12 @@
import type {
OrderSubmission,
StopOrderSetup,
StopOrdersSubmission,
} from '@vegaprotocol/wallet';
import type {
OrderFormValues,
StopOrderFormValues,
} from '../hooks/use-form-values';
import { normalizeOrderSubmission } from '@vegaprotocol/wallet';
import type { StopOrderFormValues } from '../hooks/use-stop-order-form-values';
import * as Schema from '@vegaprotocol/types';
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
export const mapFormValuesToOrderSubmission = (
order: OrderFormValues,
marketId: string,
decimalPlaces: number,
positionDecimalPlaces: number
): OrderSubmission => ({
marketId: marketId,
type: order.type,
side: order.side,
timeInForce: order.timeInForce,
price:
order.type === Schema.OrderType.TYPE_LIMIT && order.price
? removeDecimal(order.price, decimalPlaces)
: undefined,
size: removeDecimal(order.size, positionDecimalPlaces),
expiresAt:
order.expiresAt &&
order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
? toNanoSeconds(order.expiresAt)
: undefined,
postOnly:
order.type === Schema.OrderType.TYPE_MARKET ? false : order.postOnly,
reduceOnly:
order.type === Schema.OrderType.TYPE_LIMIT &&
![
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(order.timeInForce)
? false
: order.reduceOnly,
icebergOpts:
(order.type === Schema.OrderType.TYPE_MARKET ||
[
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(order.timeInForce)) &&
order.iceberg &&
order.peakSize &&
order.minimumVisibleSize
? {
peakSize: removeDecimal(order.peakSize, positionDecimalPlaces),
minimumVisibleSize: removeDecimal(
order.minimumVisibleSize,
positionDecimalPlaces
),
}
: undefined,
});
export const mapFormValuesToStopOrdersSubmission = (
data: StopOrderFormValues,
marketId: string,
@@ -67,8 +15,9 @@ export const mapFormValuesToStopOrdersSubmission = (
): StopOrdersSubmission => {
const submission: StopOrdersSubmission = {};
const stopOrderSetup: StopOrderSetup = {
orderSubmission: mapFormValuesToOrderSubmission(
orderSubmission: normalizeOrderSubmission(
{
marketId,
type: data.type,
side: data.side,
size: data.size,
@@ -76,16 +25,12 @@ export const mapFormValuesToStopOrdersSubmission = (
price: data.price,
reduceOnly: true,
},
marketId,
decimalPlaces,
positionDecimalPlaces
),
};
if (data.triggerType === 'price') {
stopOrderSetup.price = removeDecimal(
data.triggerPrice ?? '',
decimalPlaces
);
stopOrderSetup.price = removeDecimal(data.triggerPrice, decimalPlaces);
} else if (data.triggerType === 'trailingPercentOffset') {
stopOrderSetup.trailingPercentOffset = (
Number(data.triggerTrailingPercentOffset) / 100
@@ -1,64 +0,0 @@
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { mapFormValuesToOrderSubmission } from './map-form-values-to-submission';
import * as Schema from '@vegaprotocol/types';
describe('mapFormValuesToOrderSubmission', () => {
it('sets and formats price only for limit orders', () => {
expect(
mapFormValuesToOrderSubmission(
{ price: '100' } as unknown as OrderSubmissionBody['orderSubmission'],
'marketId',
2,
1
).price
).toBeUndefined();
expect(
mapFormValuesToOrderSubmission(
{
price: '100',
type: Schema.OrderType.TYPE_LIMIT,
} as unknown as OrderSubmissionBody['orderSubmission'],
'marketId',
2,
1
).price
).toEqual('10000');
});
it('sets and formats expiresAt only for time in force orders', () => {
expect(
mapFormValuesToOrderSubmission(
{
expiresAt: '2022-01-01T00:00:00.000Z',
} as OrderSubmissionBody['orderSubmission'],
'marketId',
2,
1
).expiresAt
).toBeUndefined();
expect(
mapFormValuesToOrderSubmission(
{
expiresAt: '2022-01-01T00:00:00.000Z',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
} as OrderSubmissionBody['orderSubmission'],
'marketId',
2,
1
).expiresAt
).toEqual('1640995200000000000');
});
it('formats size', () => {
expect(
mapFormValuesToOrderSubmission(
{
size: '100',
} as OrderSubmissionBody['orderSubmission'],
'marketId',
2,
1
).size
).toEqual('1000');
});
});
+17 -6
View File
@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { forwardRef, useMemo } from 'react';
import {
addDecimalsFormatNumber,
getDateTimeFormat,
@@ -6,6 +6,7 @@ import {
isNumeric,
} from '@vegaprotocol/utils';
import type { ColDef } from 'ag-grid-community';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type {
VegaICellRendererParams,
@@ -16,9 +17,10 @@ import type { DepositFieldsFragment } from './__generated__/Deposit';
import { EtherscanLink } from '@vegaprotocol/environment';
import { DepositStatusMapping } from '@vegaprotocol/types';
export const DepositsTable = (
props: TypedDataAgGrid<DepositFieldsFragment>
) => {
export const DepositsTable = forwardRef<
AgGridReact,
TypedDataAgGrid<DepositFieldsFragment>
>((props, ref) => {
const columnDefs = useMemo<ColDef[]>(
() => [
{ headerName: 'Asset', field: 'asset.symbol' },
@@ -70,9 +72,18 @@ export const DepositsTable = (
</EtherscanLink>
);
},
flex: 1,
},
],
[]
);
return <AgGrid columnDefs={columnDefs} {...props} />;
};
return (
<AgGrid
ref={ref}
defaultColDef={{ flex: 1 }}
columnDefs={columnDefs}
style={{ width: '100%', height: '100%' }}
{...props}
/>
);
});
+4 -4
View File
@@ -20,7 +20,7 @@ describe('FillsTable', () => {
positionDecimalPlaces: 5,
tradableInstrument: {
instrument: {
code: 'test market',
name: 'test market',
product: {
settlementAsset: {
decimals: 2,
@@ -71,7 +71,7 @@ describe('FillsTable', () => {
render(<FillsTable partyId={partyId} rowData={[{ ...buyerFill }]} />);
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
buyerFill.market?.tradableInstrument.instrument.code || '',
buyerFill.market?.tradableInstrument.instrument.name || '',
'+3.00',
'1.00 BTC',
'3.00 BTC',
@@ -106,7 +106,7 @@ describe('FillsTable', () => {
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
buyerFill.market?.tradableInstrument.instrument.code || '',
buyerFill.market?.tradableInstrument.instrument.name || '',
'-3.00',
'1.00 BTC',
'3.00 BTC',
@@ -141,7 +141,7 @@ describe('FillsTable', () => {
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
buyerFill.market?.tradableInstrument.instrument.code || '',
buyerFill.market?.tradableInstrument.instrument.name || '',
'-3.00',
'1.00 BTC',
'3.00 BTC',
+3 -1
View File
@@ -47,7 +47,7 @@ export const FillsTable = forwardRef<AgGridReact, Props>(
() => [
{
headerName: t('Market'),
field: 'market.tradableInstrument.instrument.code',
field: 'market.tradableInstrument.instrument.name',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'market.id', onMarketClick },
},
@@ -124,6 +124,8 @@ export const FillsTable = forwardRef<AgGridReact, Props>(
ref={ref}
columnDefs={columnDefs}
overlayNoRowsTemplate={t('No fills')}
defaultColDef={{ resizable: true }}
style={{ width: '100%', height: '100%' }}
getRowId={({ data }) => data?.id}
tooltipShowDelay={0}
tooltipHideDelay={2000}
+4 -1
View File
@@ -1,7 +1,8 @@
import { t } from '@vegaprotocol/i18n';
import type * as Schema from '@vegaprotocol/types';
import type { FilterChangedEvent } from 'ag-grid-community';
import { useCallback, useState, useMemo } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useRef, useState, useMemo } from 'react';
import { subDays, formatRFC3339 } from 'date-fns';
import { ledgerEntriesProvider } from './ledger-entries-data-provider';
import type { LedgerEntriesQueryVariables } from './__generated__/LedgerEntries';
@@ -31,6 +32,7 @@ export const LedgerManager = ({
partyId: string;
gridProps: ReturnType<typeof useDataGridEvents>;
}) => {
const gridRef = useRef<AgGridReact | null>(null);
const [filter, setFilter] = useState<Filter>(defaultFilter);
const variables = useMemo<LedgerEntriesQueryVariables>(
@@ -62,6 +64,7 @@ export const LedgerManager = ({
return (
<div className="h-full relative">
<LedgerTable
ref={gridRef}
rowData={data}
overlayNoRowsTemplate={error ? error.message : t('No entries')}
{...gridProps}
+155 -151
View File
@@ -14,6 +14,7 @@ import {
DateRangeFilter,
SetFilter,
} from '@vegaprotocol/datagrid';
import type { AgGridReact } from 'ag-grid-react';
import type * as Types from '@vegaprotocol/types';
import type { ColDef } from 'ag-grid-community';
import {
@@ -22,7 +23,7 @@ import {
TransferTypeMapping,
} from '@vegaprotocol/types';
import type { LedgerEntry } from './ledger-entries-data-provider';
import { useMemo } from 'react';
import { forwardRef, useMemo } from 'react';
import { formatRFC3339, subDays } from 'date-fns';
export const TransferTooltipCellComponent = ({
@@ -42,161 +43,164 @@ const dateRangeFilterParams = {
maxNextDays: 0,
defaultValue,
};
const defaultColDef = {
resizable: true,
sortable: true,
tooltipComponent: TransferTooltipCellComponent,
filterParams: {
...dateRangeFilterParams,
buttons: ['reset'],
},
};
type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>;
export const LedgerTable = (props: LedgerEntryProps) => {
const columnDefs = useMemo<ColDef[]>(
() => [
{
headerName: t('Sender'),
field: 'fromAccountPartyId',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountPartyId'>) =>
truncateByChars(value || ''),
},
{
headerName: t('Account type'),
filter: SetFilter,
filterParams: {
set: AccountTypeMapping,
export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
(props, ref) => {
const columnDefs = useMemo<ColDef[]>(
() => [
{
headerName: t('Sender'),
field: 'fromAccountPartyId',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountPartyId'>) =>
truncateByChars(value || ''),
},
field: 'fromAccountType',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountType'>) =>
value ? AccountTypeMapping[value] : '-',
},
{
headerName: t('Market'),
field: 'marketSender.tradableInstrument.instrument.code',
cellRenderer: ({
value,
}: VegaValueFormatterParams<
LedgerEntry,
'marketSender.tradableInstrument.instrument.code'
>) => value || '-',
},
{
headerName: t('Receiver'),
field: 'toAccountPartyId',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountPartyId'>) =>
truncateByChars(value || ''),
},
{
headerName: t('Account type'),
filter: SetFilter,
filterParams: {
set: AccountTypeMapping,
{
headerName: t('Account type'),
filter: SetFilter,
filterParams: {
set: AccountTypeMapping,
},
field: 'fromAccountType',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountType'>) =>
value ? AccountTypeMapping[value] : '-',
},
field: 'toAccountType',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountType'>) =>
value ? AccountTypeMapping[value] : '-',
},
{
headerName: t('Market'),
field: 'marketReceiver.tradableInstrument.instrument.code',
cellRenderer: ({
value,
}: VegaValueFormatterParams<
LedgerEntry,
'marketReceiver.tradableInstrument.instrument.code'
>) => value || '-',
},
{
headerName: t('Transfer type'),
field: 'transferType',
tooltipField: 'transferType',
filter: SetFilter,
filterParams: {
set: TransferTypeMapping,
{
headerName: t('Market'),
field: 'marketSender.tradableInstrument.instrument.code',
cellRenderer: ({
value,
}: VegaValueFormatterParams<
LedgerEntry,
'marketSender.tradableInstrument.instrument.code'
>) => value || '-',
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'transferType'>) =>
value ? TransferTypeMapping[value] : '',
},
{
headerName: t('Quantity'),
field: 'quantity',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'quantity'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
{
headerName: t('Receiver'),
field: 'toAccountPartyId',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountPartyId'>) =>
truncateByChars(value || ''),
},
},
{
headerName: t('Asset'),
field: 'assetId',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'asset'>) =>
data?.asset?.symbol || '',
},
{
headerName: t('Sender account balance'),
field: 'fromAccountBalance',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountBalance'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
{
headerName: t('Account type'),
filter: SetFilter,
filterParams: {
set: AccountTypeMapping,
},
field: 'toAccountType',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountType'>) =>
value ? AccountTypeMapping[value] : '-',
},
},
{
headerName: t('Receiver account balance'),
field: 'toAccountBalance',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountBalance'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
{
headerName: t('Market'),
field: 'marketReceiver.tradableInstrument.instrument.code',
cellRenderer: ({
value,
}: VegaValueFormatterParams<
LedgerEntry,
'marketReceiver.tradableInstrument.instrument.code'
>) => value || '-',
},
},
{
headerName: t('Vega time'),
field: 'vegaTime',
valueFormatter: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'vegaTime'>) =>
value ? getDateTimeFormat().format(fromNanoSeconds(value)) : '-',
filterParams: dateRangeFilterParams,
filter: DateRangeFilter,
},
],
[]
);
return (
<AgGrid
tooltipShowDelay={500}
defaultColDef={defaultColDef}
columnDefs={columnDefs}
{...props}
/>
);
};
{
headerName: t('Transfer type'),
field: 'transferType',
tooltipField: 'transferType',
filter: SetFilter,
filterParams: {
set: TransferTypeMapping,
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'transferType'>) =>
value ? TransferTypeMapping[value] : '',
},
{
headerName: t('Quantity'),
field: 'quantity',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'quantity'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
},
{
headerName: t('Asset'),
field: 'assetId',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'asset'>) =>
data?.asset?.symbol || '',
},
{
headerName: t('Sender account balance'),
field: 'fromAccountBalance',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountBalance'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
},
{
headerName: t('Receiver account balance'),
field: 'toAccountBalance',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountBalance'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
},
{
headerName: t('Vega time'),
field: 'vegaTime',
valueFormatter: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'vegaTime'>) =>
value ? getDateTimeFormat().format(fromNanoSeconds(value)) : '-',
filterParams: dateRangeFilterParams,
filter: DateRangeFilter,
flex: 1,
},
],
[]
);
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
ref={ref}
tooltipShowDelay={500}
defaultColDef={{
resizable: true,
sortable: true,
tooltipComponent: TransferTooltipCellComponent,
filterParams: {
...dateRangeFilterParams,
buttons: ['reset'],
},
}}
columnDefs={columnDefs}
{...props}
/>
);
}
);
+8 -7
View File
@@ -31,12 +31,6 @@ const dateValueFormatter = ({ value }: { value?: string | null }) => {
return getDateTimeFormat().format(new Date(value));
};
const defaultColDef = {
resizable: true,
sortable: true,
tooltipComponent: TooltipCellComponent,
};
export interface LiquidityTableProps
extends TypedDataAgGrid<LiquidityProvisionData> {
symbol?: string;
@@ -130,6 +124,7 @@ export const LiquidityTable = ({
headerTooltip: t(
'The valuation of the market at the time the liquidity commitment was made. Commitments made at a lower valuation earlier in the lifetime of the market would be expected to have a higher equity-like share if the market has grown. If a commitment is amended, value will reflect the average of the market valuations across the lifetime of the commitment.'
),
minWidth: 160,
valueFormatter: assetDecimalsQuantumFormatter,
tooltipValueGetter: assetDecimalsFormatter,
},
@@ -188,10 +183,16 @@ export const LiquidityTable = ({
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No liquidity provisions')}
getRowId={({ data }: { data: LiquidityProvisionData }) => data.id || ''}
tooltipShowDelay={500}
defaultColDef={defaultColDef}
defaultColDef={{
resizable: true,
minWidth: 100,
tooltipComponent: TooltipCellComponent,
sortable: true,
}}
{...props}
columnDefs={colDefs}
/>
+1 -17
View File
@@ -4,25 +4,9 @@ export default {
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
'^.+\\.[tj]sx?$': [
'babel-jest',
{
presets: ['@nx/react/babel'],
// required for pennant to work in jest, due to having untranspiled exports
plugins: [
[
'@babel/plugin-proposal-private-methods',
{
loose: true,
},
],
],
},
],
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/market-depth',
setupFilesAfterEnv: ['./src/setup-tests.ts'],
// dont ignore pennant from transpilation
transformIgnorePatterns: ['<rootDir>/node_modules/pennant'],
};
@@ -34,8 +34,5 @@ query SuccessorMarket($marketId: ID!) {
code
}
}
proposal {
id
}
}
}
+1 -4
View File
@@ -27,7 +27,7 @@ export type SuccessorMarketQueryVariables = Types.Exact<{
}>;
export type SuccessorMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, state: Types.MarketState, tradingMode: Types.MarketTradingMode, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string } }, proposal?: { __typename?: 'Proposal', id?: string | null } | null } | null };
export type SuccessorMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, state: Types.MarketState, tradingMode: Types.MarketTradingMode, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string } } } | null };
export const SuccessorMarketIdDocument = gql`
@@ -153,9 +153,6 @@ export const SuccessorMarketDocument = gql`
code
}
}
proposal {
id
}
}
}
`;
@@ -4,10 +4,8 @@ import {
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
Intent,
KeyValueTable,
KeyValueTableRow,
Lozenge,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import BigNumber from 'bignumber.js';
@@ -24,11 +22,9 @@ interface RowProps {
unformatted?: boolean;
assetSymbol?: string;
noBorder?: boolean;
parentValue?: ReactNode;
hasParentData?: boolean;
}
export const Row = ({
const Row = ({
field,
value,
decimalPlaces,
@@ -36,14 +32,7 @@ export const Row = ({
unformatted,
assetSymbol = '',
noBorder = true,
parentValue,
hasParentData,
}: RowProps) => {
// Note: we need both 'parentValue' and 'hasParentData' to do a conditional
// check to differentiate between when parentData itself is missing and when
// a specific parentValue is missing. These values are only used when we
// have successor market parent data.
const className = 'text-sm';
const getFormattedValue = (value: ReactNode) => {
@@ -66,10 +55,6 @@ export const Row = ({
const formattedValue = getFormattedValue(value);
if (!formattedValue) return null;
const newValueInSuccessorMarket = hasParentData && value && !parentValue;
const valueDiffersFromParentMarket = parentValue && parentValue !== value;
return (
<KeyValueTableRow
key={field}
@@ -78,35 +63,10 @@ export const Row = ({
dtClassName={className}
ddClassName={className}
>
<div className="flex items-center gap-3">
<Tooltip description={tooltipMapping[field]} align="start">
<div tabIndex={-1}>{startCase(t(field))}</div>
</Tooltip>
{valueDiffersFromParentMarket && (
<Lozenge className="py-0" variant={Intent.Primary}>
{t('Updated')}
</Lozenge>
)}
{newValueInSuccessorMarket && (
<Lozenge className="py-0" variant={Intent.Primary}>
{t('Added')}
</Lozenge>
)}
</div>
<div style={{ wordBreak: 'break-word' }}>
{valueDiffersFromParentMarket ? (
<div className="flex items-center gap-3">
<span className="line-through">
{getFormattedValue(parentValue)}
</span>
<span>{formattedValue}</span>
</div>
) : (
formattedValue
)}
</div>
<Tooltip description={tooltipMapping[field]} align="start">
<div tabIndex={-1}>{startCase(t(field))}</div>
</Tooltip>
<span style={{ wordBreak: 'break-word' }}>{formattedValue}</span>
</KeyValueTableRow>
);
};
@@ -119,7 +79,6 @@ export interface MarketInfoTableProps {
children?: ReactNode;
assetSymbol?: string;
noBorder?: boolean;
parentData?: Record<string, ReactNode> | null | undefined;
}
export const MarketInfoTable = ({
@@ -130,33 +89,25 @@ export const MarketInfoTable = ({
children,
assetSymbol,
noBorder,
parentData,
}: MarketInfoTableProps) => {
if (!data || typeof data !== 'object') {
return null;
}
const hasParentData = parentData !== undefined;
return (
<>
<KeyValueTable>
<>
{Object.entries(data).map(([key, value]) => (
<Row
key={key}
field={key}
value={value}
decimalPlaces={decimalPlaces}
assetSymbol={assetSymbol}
asPercentage={asPercentage}
unformatted={unformatted}
noBorder={noBorder}
parentValue={parentData?.[key]}
hasParentData={hasParentData}
/>
))}
</>
{Object.entries(data).map(([key, value]) => (
<Row
key={key}
field={key}
value={value}
decimalPlaces={decimalPlaces}
assetSymbol={assetSymbol}
asPercentage={asPercentage}
unformatted={unformatted}
noBorder={noBorder}
/>
))}
</KeyValueTable>
<div className="flex flex-col gap-2">{children}</div>
</>
@@ -1,8 +1,4 @@
import {
FLAGS,
TokenStaticLinks,
useEnvironment,
} from '@vegaprotocol/environment';
import { TokenStaticLinks, useEnvironment } from '@vegaprotocol/environment';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider';
@@ -38,7 +34,6 @@ import {
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
SuccessionLineInfoPanel,
} from './market-info-panels';
import type { DataSourceDefinition } from '@vegaprotocol/types';
import isEqual from 'lodash/isEqual';
@@ -296,13 +291,6 @@ export const MarketInfoAccordion = ({
</>
}
/>
{FLAGS.SUCCESSOR_MARKETS && (
<AccordionItem
itemId="succession-line"
title={t('Succession line')}
content={<SuccessionLineInfoPanel market={market} />}
/>
)}
</Accordion>
</div>
)}
@@ -1,11 +1,9 @@
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import {
ConditionOperator,
ConditionOperatorMapping,
} from '@vegaprotocol/types';
import { DataSourceProof, SuccessionLineInfoPanel } from './market-info-panels';
import { MockedProvider } from '@apollo/react-testing';
import { SuccessorMarketIdsDocument } from '../../__generated__';
import { DataSourceProof } from './market-info-panels';
jest.mock('../../hooks/use-oracle-markets', () => ({
useOracleMarkets: () => [],
@@ -139,93 +137,3 @@ describe('DataSourceProof', () => {
).toBeInTheDocument();
});
});
describe('SuccessionLineInfoPanel', () => {
const mocks = [
{
request: {
query: SuccessorMarketIdsDocument,
},
result: {
data: {
__typename: 'Query',
marketsConnection: {
__typename: 'MarketConnection',
edges: [
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: 'abc',
successorMarketID: 'def',
parentMarketID: null,
},
},
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: 'def',
successorMarketID: 'ghi',
parentMarketID: 'abc',
},
},
{
__typename: 'MarketEdge',
node: {
__typename: 'Market',
id: 'ghi',
successorMarketID: null,
parentMarketID: 'def',
},
},
],
},
},
},
},
];
it.each([
['abc', 1],
['def', 2],
['ghi', 3],
])(
'renders succession line for %s (current position %d)',
async (id, number) => {
render(
<MockedProvider mocks={mocks}>
<SuccessionLineInfoPanel
market={{
id,
}}
/>
</MockedProvider>
);
await waitFor(() => {
const items = screen.getAllByTestId('succession-line-item');
expect(items.length).toBe(3);
expect(
items[0].querySelector(
'[data-testid="succession-line-item-market-id"]'
)?.textContent
).toBe('abc');
expect(
items[1].querySelector(
'[data-testid="succession-line-item-market-id"]'
)?.textContent
).toBe('def');
expect(
items[2].querySelector(
'[data-testid="succession-line-item-market-id"]'
)?.textContent
).toBe('ghi');
expect(
items[number - 1].querySelector('[data-testid="icon-bullet"]')
).toBeInTheDocument();
});
}
);
});
@@ -1,18 +1,11 @@
import isEqual from 'lodash/isEqual';
import type { ReactNode } from 'react';
import { Fragment, useState } from 'react';
import { useState } from 'react';
import { useMemo } from 'react';
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n';
import { marketDataProvider } from '../../market-data-provider';
import { totalFeesPercentage } from '../../market-utils';
import {
ExternalLink,
Splash,
Tooltip,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
import {
addDecimalsFormatNumber,
formatNumber,
@@ -30,32 +23,17 @@ import BigNumber from 'bignumber.js';
import type { DataSourceDefinition, SignerKind } from '@vegaprotocol/types';
import { ConditionOperatorMapping } from '@vegaprotocol/types';
import { MarketTradingModeMapping } from '@vegaprotocol/types';
import {
DApp,
FLAGS,
TOKEN_PROPOSAL,
useEnvironment,
useLinks,
} from '@vegaprotocol/environment';
import { FLAGS, useEnvironment } from '@vegaprotocol/environment';
import type { Provider } from '../../oracle-schema';
import { OracleBasicProfile } from '../../components/oracle-basic-profile';
import { useOracleProofs } from '../../hooks';
import { OracleDialog } from '../oracle-dialog/oracle-dialog';
import { useDataProvider } from '@vegaprotocol/data-provider';
import {
useParentMarketIdQuery,
useSuccessorMarketIdsQuery,
useSuccessorMarketQuery,
} from '../../__generated__';
import { useParentMarketIdQuery } from '../../__generated__';
import { useSuccessorMarketProposalDetailsQuery } from '@vegaprotocol/proposals';
import type { MarketTradingMode } from '@vegaprotocol/types';
import type { Signer } from '@vegaprotocol/types';
import classNames from 'classnames';
import compact from 'lodash/compact';
type MarketInfoProps = {
market: MarketInfo;
parentMarket?: MarketInfo;
children?: ReactNode;
};
@@ -160,43 +138,21 @@ export const InsurancePoolInfoPanel = ({
);
};
export const KeyDetailsInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => {
const { data: parentMarketIdData } = useParentMarketIdQuery({
export const KeyDetailsInfoPanel = ({ market }: MarketInfoProps) => {
const { data: parentData } = useParentMarketIdQuery({
variables: {
marketId: market.id,
},
skip: !FLAGS.SUCCESSOR_MARKETS,
});
const { data: successorProposalDetails } =
useSuccessorMarketProposalDetailsQuery({
variables: {
proposalId: market.proposal?.id || '',
},
skip: !FLAGS.SUCCESSOR_MARKETS || !market.proposal?.id,
});
// The following queries are needed as the parent market could also have been a successor market.
// Note: the parent market is only passed to this component if the successor markets flag is enabled,
// so that check is not needed in the skip.
const { data: grandparentMarketIdData } = useParentMarketIdQuery({
const { data: successor } = useSuccessorMarketProposalDetailsQuery({
variables: {
marketId: parentMarket?.id || '',
proposalId: market.proposal?.id || '',
},
skip: !parentMarket?.id,
skip: !FLAGS.SUCCESSOR_MARKETS || !market.proposal?.id,
});
const { data: parentSuccessorProposalDetails } =
useSuccessorMarketProposalDetailsQuery({
variables: {
proposalId: parentMarket?.proposal?.id || '',
},
skip: !parentMarket?.proposal?.id,
});
const assetDecimals =
market.tradableInstrument.instrument.product.settlementAsset.decimals;
@@ -207,12 +163,11 @@ export const KeyDetailsInfoPanel = ({
? {
name: market.tradableInstrument.instrument.name,
marketID: market.id,
parentMarketID: parentMarketIdData?.market?.parentMarketID || '-',
parentMarketID: parentData?.market?.parentMarketID || '-',
insurancePoolFraction:
(successorProposalDetails?.proposal?.terms.change.__typename ===
'NewMarket' &&
successorProposalDetails.proposal.terms.change
.successorConfiguration?.insurancePoolFraction) ||
(successor?.proposal?.terms.change.__typename === 'NewMarket' &&
successor.proposal.terms.change.successorConfiguration
?.insurancePoolFraction) ||
'-',
tradingMode:
market.tradingMode &&
@@ -232,156 +187,11 @@ export const KeyDetailsInfoPanel = ({
settlementAssetDecimalPlaces: assetDecimals,
}
}
parentData={
parentMarket && {
name: parentMarket?.tradableInstrument?.instrument?.name,
marketID: parentMarket?.id,
parentMarketID: grandparentMarketIdData?.market?.parentMarketID,
insurancePoolFraction:
parentSuccessorProposalDetails?.proposal?.terms.change
.__typename === 'NewMarket' &&
parentSuccessorProposalDetails.proposal.terms.change
.successorConfiguration?.insurancePoolFraction,
tradingMode:
parentMarket?.tradingMode &&
MarketTradingModeMapping[
parentMarket.tradingMode as MarketTradingMode
],
marketDecimalPlaces: parentMarket?.decimalPlaces,
positionDecimalPlaces: parentMarket?.positionDecimalPlaces,
settlementAssetDecimalPlaces:
parentMarket?.tradableInstrument?.instrument?.product
?.settlementAsset?.decimals,
}
}
/>
);
};
const SuccessionLineItem = ({
marketId,
isCurrent,
}: {
marketId: string;
isCurrent?: boolean;
}) => {
const { data } = useSuccessorMarketQuery({
variables: {
marketId,
},
});
const marketData = data?.market;
const governanceLink = useLinks(DApp.Token);
const proposalLink = marketData?.proposal?.id
? governanceLink(TOKEN_PROPOSAL.replace(':id', marketData?.proposal?.id))
: undefined;
return (
<div
data-testid="succession-line-item"
className={classNames(
'rounded p-2 bg-vega-clight-700 dark:bg-vega-cdark-700',
'font-alpha',
'flex flex-col '
)}
>
<div className="flex justify-between">
<div>
{marketData ? (
proposalLink ? (
<ExternalLink href={proposalLink}>
{marketData.tradableInstrument.instrument.code}
</ExternalLink>
) : (
marketData.tradableInstrument.instrument.code
)
) : (
<span className="block w-20 h-4 mb-1 bg-vega-clight-500 dark:bg-vega-cdark-500 animate-pulse"></span>
)}
</div>
{isCurrent && (
<Tooltip description={t('This market')}>
<div className="text-vega-clight-200 dark:text-vega-cdark-200 cursor-help">
<VegaIcon name={VegaIconNames.BULLET} size={16} />
</div>
</Tooltip>
)}
</div>
<div className="text-xs">
{marketData ? (
marketData.tradableInstrument.instrument.name
) : (
<span className="block w-28 h-4 bg-vega-clight-500 dark:bg-vega-cdark-500 animate-pulse"></span>
)}
</div>
<div
data-testid="succession-line-item-market-id"
className="text-xs truncate mt-1"
>
{marketId}
</div>
</div>
);
};
const SuccessionLink = () => (
<div className="text-center leading-none" aria-hidden>
<VegaIcon name={VegaIconNames.ARROW_DOWN} size={12} />
</div>
);
const buildSuccessionLine = (
all: {
id: string;
successorMarketID?: string | null | undefined;
parentMarketID?: string | null | undefined;
}[],
id: string
) => {
let line = [id];
const find = (id: string, dir?: 'up' | 'down') => {
const item = all.find((a) => a.id === id);
const anc = dir === 'up' && item?.parentMarketID;
const des = dir === 'down' && item?.successorMarketID;
if (anc) {
line = [anc, ...line];
find(anc, 'up');
}
if (des) {
line = [...line, des];
find(des, 'down');
}
};
find(id, 'up');
find(id, 'down');
return line;
};
export const SuccessionLineInfoPanel = ({
market,
}: {
market: Pick<MarketInfo, 'id'>;
}) => {
const { data } = useSuccessorMarketIdsQuery();
const ids = compact(data?.marketsConnection?.edges.map((e) => e.node));
const line = buildSuccessionLine(ids, market.id);
return (
<div className="flex flex-col gap-2">
{line.map((id, i) => (
<Fragment key={i}>
{i > 0 && <SuccessionLink />}
<SuccessionLineItem marketId={id} isCurrent={id === market.id} />
</Fragment>
))}
</div>
);
};
export const InstrumentInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => (
export const InstrumentInfoPanel = ({ market }: MarketInfoProps) => (
<MarketInfoTable
data={{
marketName: market.tradableInstrument.instrument.name,
@@ -389,16 +199,6 @@ export const InstrumentInfoPanel = ({
productType: market.tradableInstrument.instrument.product.__typename,
quoteName: market.tradableInstrument.instrument.product.quoteName,
}}
parentData={
parentMarket && {
marketName: parentMarket?.tradableInstrument?.instrument?.name,
code: parentMarket?.tradableInstrument?.instrument?.code,
productType:
parentMarket?.tradableInstrument?.instrument?.product?.__typename,
quoteName:
parentMarket?.tradableInstrument?.instrument?.product?.quoteName,
}
}
/>
);
@@ -411,7 +211,6 @@ export const SettlementAssetInfoPanel = ({ market }: MarketInfoProps) => {
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
[market]
);
const { data: asset } = useAssetDataProvider(assetId ?? '');
return asset ? (
<>
@@ -434,148 +233,54 @@ export const SettlementAssetInfoPanel = ({ market }: MarketInfoProps) => {
);
};
const getMarketMetadata = (market: MarketInfo) =>
market.tradableInstrument.instrument.metadata.tags
?.map((tag) => {
const [key, value] = tag.split(':');
return { [key]: value };
})
.reduce((acc, curr) => ({ ...acc, ...curr }), {});
export const MetadataInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => (
export const MetadataInfoPanel = ({ market }: MarketInfoProps) => (
<MarketInfoTable
data={{
expiryDate: getMarketExpiryDateFormatted(
market.tradableInstrument.instrument.metadata.tags
),
...(getMarketMetadata(market) || {}),
...market.tradableInstrument.instrument.metadata.tags
?.map((tag) => {
const [key, value] = tag.split(':');
return { [key]: value };
})
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
}}
parentData={
parentMarket && {
expiryDate: getMarketExpiryDateFormatted(
parentMarket.tradableInstrument.instrument.metadata.tags
),
...(getMarketMetadata(parentMarket) || {}),
}
}
/>
);
export const RiskModelInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => {
export const RiskModelInfoPanel = ({ market }: MarketInfoProps) => {
if (market.tradableInstrument.riskModel.__typename !== 'LogNormalRiskModel') {
return null;
}
const { tau, riskAversionParameter } = market.tradableInstrument.riskModel;
let parentData;
if (
parentMarket?.tradableInstrument?.riskModel?.__typename ===
'LogNormalRiskModel'
) {
const {
tau: parentTau,
riskAversionParameter: parentRiskAversionParameter,
} = market.tradableInstrument.riskModel;
parentData = {
tau: parentTau,
riskAversionParameter: parentRiskAversionParameter,
};
}
return (
<MarketInfoTable
data={{ tau, riskAversionParameter }}
parentData={parentData}
unformatted
/>
);
return <MarketInfoTable data={{ tau, riskAversionParameter }} unformatted />;
};
export const RiskParametersInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => {
const marketType = market.tradableInstrument.riskModel.__typename;
let data, parentData;
if (marketType === 'LogNormalRiskModel') {
export const RiskParametersInfoPanel = ({ market }: MarketInfoProps) => {
if (market.tradableInstrument.riskModel.__typename === 'LogNormalRiskModel') {
const { r, sigma, mu } = market.tradableInstrument.riskModel.params;
data = { r, sigma, mu };
if (
parentMarket?.tradableInstrument?.riskModel.__typename ===
'LogNormalRiskModel'
) {
const parentParams = parentMarket.tradableInstrument.riskModel.params;
parentData = {
r: parentParams.r,
sigma: parentParams.sigma,
mu: parentParams.mu,
};
}
} else if (marketType === 'SimpleRiskModel') {
return <MarketInfoTable data={{ r, sigma, mu }} unformatted />;
}
if (market.tradableInstrument.riskModel.__typename === 'SimpleRiskModel') {
const { factorLong, factorShort } =
market.tradableInstrument.riskModel.params;
data = { factorLong, factorShort };
if (
parentMarket?.tradableInstrument?.riskModel.__typename ===
'SimpleRiskModel'
) {
const parentParams = parentMarket.tradableInstrument.riskModel.params;
parentData = {
factorLong: parentParams.factorLong,
factorShort: parentParams.factorShort,
};
}
return <MarketInfoTable data={{ factorLong, factorShort }} unformatted />;
}
if (!data) return null;
return <MarketInfoTable data={data} parentData={parentData} unformatted />;
return null;
};
export const RiskFactorsInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => {
export const RiskFactorsInfoPanel = ({ market }: MarketInfoProps) => {
if (!market.riskFactors) {
return null;
}
const { short, long } = market.riskFactors;
let parentData;
if (parentMarket?.riskFactors) {
const parentShort = parentMarket.riskFactors.short;
const parentLong = parentMarket.riskFactors.long;
parentData = { short: parentShort, long: parentLong };
}
return (
<MarketInfoTable
data={{ short, long }}
parentData={parentData}
unformatted
/>
);
return <MarketInfoTable data={{ short, long }} unformatted />;
};
export const PriceMonitoringBoundsInfoPanel = ({
market,
triggerIndex,
parentMarket,
}: MarketInfoProps & {
triggerIndex: number;
}) => {
@@ -583,35 +288,11 @@ export const PriceMonitoringBoundsInfoPanel = ({
dataProvider: marketDataProvider,
variables: { marketId: market.id },
});
const { data: parentData } = useDataProvider({
dataProvider: marketDataProvider,
variables: { marketId: parentMarket?.id || '' },
skip:
!parentMarket ||
!parentMarket?.priceMonitoringSettings?.parameters?.triggers?.[
triggerIndex
],
});
const quoteUnit =
market?.tradableInstrument.instrument.product?.quoteName || '';
const parentQuoteUnit =
parentMarket?.tradableInstrument.instrument.product?.quoteName || '';
const isParentQuoteUnitEqual = quoteUnit === parentQuoteUnit;
const trigger =
market.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex];
const parentTrigger =
parentMarket?.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex];
const isParentTriggerEqual = isEqual(trigger, parentTrigger);
const bounds = data?.priceMonitoringBounds?.[triggerIndex];
const parentBounds = parentData?.priceMonitoringBounds?.[triggerIndex];
const shouldShowParentData =
isParentQuoteUnitEqual && isParentTriggerEqual && !!parentBounds;
if (!trigger) {
console.error(
`Could not find data for trigger ${triggerIndex} (market id: ${market.id})`
@@ -638,14 +319,6 @@ export const PriceMonitoringBoundsInfoPanel = ({
highestPrice: bounds.maxValidPrice,
lowestPrice: bounds.minValidPrice,
}}
parentData={
shouldShowParentData
? {
highestPrice: parentBounds.maxValidPrice,
lowestPrice: parentBounds.minValidPrice,
}
: undefined
}
decimalPlaces={market.decimalPlaces}
assetSymbol={quoteUnit}
/>
@@ -661,31 +334,18 @@ export const PriceMonitoringBoundsInfoPanel = ({
export const LiquidityMonitoringParametersInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => {
const marketData = {
triggeringRatio: market.liquidityMonitoringParameters.triggeringRatio,
timeWindow:
market.liquidityMonitoringParameters.targetStakeParameters.timeWindow,
scalingFactor:
market.liquidityMonitoringParameters.targetStakeParameters.scalingFactor,
};
const parentMarketData = parentMarket
? {
triggeringRatio:
parentMarket.liquidityMonitoringParameters.triggeringRatio,
timeWindow:
parentMarket.liquidityMonitoringParameters.targetStakeParameters
.timeWindow,
scalingFactor:
parentMarket.liquidityMonitoringParameters.targetStakeParameters
.scalingFactor,
}
: {};
return <MarketInfoTable data={marketData} parentData={parentMarketData} />;
};
}: MarketInfoProps) => (
<MarketInfoTable
data={{
triggeringRatio: market.liquidityMonitoringParameters.triggeringRatio,
timeWindow:
market.liquidityMonitoringParameters.targetStakeParameters.timeWindow,
scalingFactor:
market.liquidityMonitoringParameters.targetStakeParameters
.scalingFactor,
}}
/>
);
export const LiquidityInfoPanel = ({ market, children }: MarketInfoProps) => {
const assetDecimals =
@@ -712,61 +372,16 @@ export const LiquidityInfoPanel = ({ market, children }: MarketInfoProps) => {
);
};
export const LiquidityPriceRangeInfoPanel = ({
market,
parentMarket,
}: MarketInfoProps) => {
export const LiquidityPriceRangeInfoPanel = ({ market }: MarketInfoProps) => {
const quoteUnit =
market?.tradableInstrument.instrument.product?.quoteName || '';
const parentQuoteUnit =
parentMarket?.tradableInstrument.instrument.product?.quoteName || '';
const liquidityPriceRange = formatNumberPercentage(
new BigNumber(market.lpPriceRange).times(100)
);
const parentLiquidityPriceRange = parentMarket
? formatNumberPercentage(
new BigNumber(parentMarket.lpPriceRange).times(100)
)
: null;
const { data } = useDataProvider({
dataProvider: marketDataProvider,
variables: { marketId: market.id },
});
const { data: parentMarketData } = useDataProvider({
dataProvider: marketDataProvider,
variables: { marketId: parentMarket?.id || '' },
skip: !parentMarket,
});
let parentData;
if (parentMarket && parentMarketData && quoteUnit === parentQuoteUnit) {
parentData = {
liquidityPriceRange: `${parentLiquidityPriceRange} of mid price`,
lowestPrice:
parentMarketData?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.minus(parentMarket.lpPriceRange)
.times(parentMarketData.midPrice)
.toString(),
parentMarket.decimalPlaces
)} ${quoteUnit}`,
highestPrice:
parentMarketData?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.plus(parentMarket.lpPriceRange)
.times(parentMarketData.midPrice)
.toString(),
parentMarket.decimalPlaces
)} ${quoteUnit}`,
};
}
return (
<>
<p className="text-sm mb-2">
@@ -799,7 +414,6 @@ export const LiquidityPriceRangeInfoPanel = ({
market.decimalPlaces
)} ${quoteUnit}`,
}}
parentData={parentData}
/>
</>
);
@@ -808,12 +422,8 @@ export const LiquidityPriceRangeInfoPanel = ({
export const OracleInfoPanel = ({
market,
type,
parentMarket,
}: MarketInfoProps & { type: 'settlementData' | 'termination' }) => {
// If this is a successor market, this component will only receive parent market
// data if the termination or settlement data is different from the parent.
const product = market.tradableInstrument.instrument.product;
const parentProduct = parentMarket?.tradableInstrument?.instrument?.product;
const { VEGA_EXPLORER_URL, ORACLE_PROOFS_URL } = useEnvironment();
const { data } = useOracleProofs(ORACLE_PROOFS_URL);
@@ -822,33 +432,12 @@ export const OracleInfoPanel = ({
? product.dataSourceSpecForSettlementData.id
: product.dataSourceSpecForTradingTermination.id;
const parentDataSourceSpecId =
type === 'settlementData'
? parentProduct?.dataSourceSpecForSettlementData?.id
: parentProduct?.dataSourceSpecForTradingTermination?.id;
const dataSourceSpec = (
type === 'settlementData'
? product.dataSourceSpecForSettlementData.data
: product.dataSourceSpecForTradingTermination.data
) as DataSourceDefinition;
const parentDataSourceSpec =
type === 'settlementData'
? parentProduct?.dataSourceSpecForSettlementData?.data
: (parentProduct?.dataSourceSpecForTradingTermination
?.data as DataSourceDefinition);
const isParentDataSourceSpecEqual =
parentDataSourceSpec !== undefined &&
dataSourceSpec === parentDataSourceSpec;
const isParentDataSourceSpecIdEqual =
parentDataSourceSpecId !== undefined &&
dataSourceSpecId === parentDataSourceSpecId;
// We'll only provide successor parent data (if it differs) to the
// DataSourceProof component. Having an old external link struck through
// is unlikely to be useful.
return (
<div className="flex flex-col gap-2">
<DataSourceProof
@@ -857,14 +446,7 @@ export const OracleInfoPanel = ({
providers={data}
type={type}
dataSourceSpecId={dataSourceSpecId}
parentData={
isParentDataSourceSpecEqual ? undefined : parentDataSourceSpec
}
parentDataSourceSpecId={
isParentDataSourceSpecIdEqual ? undefined : parentDataSourceSpecId
}
/>
<ExternalLink
data-testid="oracle-spec-links"
href={`${VEGA_EXPLORER_URL}/oracles/${
@@ -886,28 +468,14 @@ export const DataSourceProof = ({
providers,
type,
dataSourceSpecId,
parentData,
parentDataSourceSpecId,
}: {
data: DataSourceDefinition;
providers: Provider[] | undefined;
type: 'settlementData' | 'termination';
dataSourceSpecId: string;
parentData?: DataSourceDefinition;
parentDataSourceSpecId?: string;
}) => {
// If this is a successor market, we'll only pass parent data to child
// components for comparison if the data differs from the parent market.
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
let parentSigners: Signer[];
if (
parentData &&
parentData.sourceType.__typename === 'DataSourceDefinitionExternal'
) {
parentSigners = parentData.sourceType.sourceType?.signers || [];
}
if (!providers?.length) {
return <NoOracleProof type={type} />;
@@ -916,14 +484,7 @@ export const DataSourceProof = ({
return (
<div className="flex flex-col gap-2">
{signers.map(({ signer }, i) => {
const parentSigner = parentSigners?.find(
({ signer: ParentSigner }) =>
ParentSigner.__typename === signer.__typename
)?.signer;
const isParentSignerEqual = isEqual(signer, parentSigner);
return isParentSignerEqual ? (
return (
<OracleLink
key={i}
providers={providers}
@@ -931,16 +492,6 @@ export const DataSourceProof = ({
type={type}
dataSourceSpecId={dataSourceSpecId}
/>
) : (
<OracleLink
key={i}
providers={providers}
signer={signer}
type={type}
dataSourceSpecId={dataSourceSpecId}
parentSigner={parentSigner}
parentDataSourceSpecId={parentDataSourceSpecId}
/>
);
})}
</div>
@@ -974,8 +525,18 @@ export const DataSourceProof = ({
return <div>{t('Invalid data source')}</div>;
};
const getSignerProviders = (signer: SignerKind, providers: Provider[]) =>
providers.filter((p) => {
const OracleLink = ({
providers,
signer,
type,
dataSourceSpecId,
}: {
providers: Provider[];
signer: SignerKind;
type: 'settlementData' | 'termination';
dataSourceSpecId: string;
}) => {
const signerProviders = providers.filter((p) => {
if (signer.__typename === 'PubKey') {
if (
p.oracle.type === 'public_key' &&
@@ -997,62 +558,19 @@ const getSignerProviders = (signer: SignerKind, providers: Provider[]) =>
return false;
});
const OracleLink = ({
providers,
signer,
type,
dataSourceSpecId,
parentSigner,
parentDataSourceSpecId,
}: {
providers: Provider[];
signer: SignerKind;
type: 'settlementData' | 'termination';
dataSourceSpecId: string;
parentSigner?: SignerKind;
parentDataSourceSpecId?: string;
}) => {
// If this is a successor market, the parent market data will only have been passed
// in if it differs from the current data.
const signerProviders = getSignerProviders(signer, providers);
const parentSignerProviders = parentSigner
? getSignerProviders(parentSigner, providers)
: [];
if (!signerProviders.length) {
return <NoOracleProof type={type} />;
}
return (
<div className="mt-2">
{signerProviders.map((provider) => {
// Making the assumption here that if the provider name is the same,
// that it is the same provider that the parent market used.
const parentProvider = parentSignerProviders.find(
(p) => p.name === provider.name
);
const isParentProviderEqual =
parentProvider !== undefined && isEqual(provider, parentProvider);
// We only want to pass the parent data to the child component if the
// data differs from the parent market.
return isParentProviderEqual ? (
<OracleProfile
key={dataSourceSpecId}
provider={provider}
dataSourceSpecId={dataSourceSpecId}
/>
) : (
<OracleProfile
key={dataSourceSpecId}
provider={provider}
dataSourceSpecId={dataSourceSpecId}
parentProvider={parentProvider}
parentDataSourceSpecId={parentDataSourceSpecId}
/>
);
})}
{signerProviders.map((provider) => (
<OracleProfile
key={dataSourceSpecId}
provider={provider}
dataSourceSpecId={dataSourceSpecId}
/>
))}
</div>
);
};
@@ -1075,18 +593,13 @@ const NoOracleProof = ({
const OracleProfile = (props: {
provider: Provider;
dataSourceSpecId: string;
parentProvider?: Provider;
parentDataSourceSpecId?: string;
}) => {
// If this is a successor market, the parent market data will only have been passed
// in if it differs from the current data.
const [open, onChange] = useState(false);
return (
<div key={props.provider.name}>
<OracleBasicProfile
provider={props.provider}
onClick={() => onChange(!open)}
parentProvider={props.parentProvider}
/>
<OracleDialog {...props} open={open} onChange={onChange} />
</div>
@@ -1,9 +1,11 @@
import React, { forwardRef } from 'react';
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import {
AgGridLazy as AgGrid,
PriceFlashCell,
MarketNameCell,
} from '@vegaprotocol/datagrid';
import type { AgGridReact } from 'ag-grid-react';
import type { MarketMaybeWithData } from '../../markets-provider';
import { OracleStatus } from './oracle-status';
import { useColumnDefs } from './use-column-defs';
@@ -35,19 +37,20 @@ const MarketName = (props: MarketNameCellProps) => (
);
const defaultColDef = {
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
minWidth: 100,
};
type Props = TypedDataAgGrid<MarketMaybeWithData> & {
onMarketClick: (marketId: string, metaKey?: boolean) => void;
SuccessorMarketRenderer?: React.FC<{ value: string }>;
};
export const MarketListTable = ({
onMarketClick,
SuccessorMarketRenderer,
...props
}: Props) => {
export const MarketListTable = forwardRef<
AgGridReact,
TypedDataAgGrid<MarketMaybeWithData> & {
onMarketClick: (marketId: string, metaKey?: boolean) => void;
SuccessorMarketRenderer?: React.FC<{ value: string }>;
}
>(({ onMarketClick, SuccessorMarketRenderer, ...props }, ref) => {
const columnDefs = useColumnDefs({ onMarketClick });
const components = {
PriceFlashCell,
@@ -56,7 +59,9 @@ export const MarketListTable = ({
};
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
getRowId={getRowId}
ref={ref}
defaultColDef={defaultColDef}
columnDefs={columnDefs}
suppressCellFocus
@@ -64,6 +69,6 @@ export const MarketListTable = ({
{...props}
/>
);
};
});
export default MarketListTable;
@@ -1,5 +1,6 @@
import type { MouseEvent } from 'react';
import React, { useEffect } from 'react';
import React, { useEffect, useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import type { CellClickedEvent } from 'ag-grid-community';
import { t } from '@vegaprotocol/i18n';
import { MarketListTable } from './market-list-table';
@@ -17,6 +18,8 @@ export const MarketsContainer = ({
onSelect,
SuccessorMarketRenderer,
}: MarketsContainerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const { data, error, reload } = useDataProvider({
dataProvider,
variables: undefined,
@@ -34,6 +37,7 @@ export const MarketsContainer = ({
return (
<div className="h-full relative">
<MarketListTable
ref={gridRef}
rowData={data}
onCellClicked={(cellEvent: CellClickedEvent) => {
const { data, column, event } = cellEvent;
@@ -40,6 +40,7 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
{
headerName: t('Trading mode'),
field: 'tradingMode',
minWidth: 170,
valueFormatter: ({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => {
@@ -5,7 +5,6 @@ import {
ExternalLink,
Icon,
Intent,
Lozenge,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
@@ -60,12 +59,10 @@ export const OracleBasicProfile = ({
provider,
onClick,
markets: oracleMarkets,
parentProvider,
}: {
provider: Provider;
markets?: OracleMarketSpecFieldsFragment[] | undefined;
onClick?: (value?: boolean) => void;
parentProvider?: Provider;
}) => {
const { icon, message, intent } = getVerifiedStatusIcon(provider);
@@ -81,14 +78,8 @@ export const OracleBasicProfile = ({
icon: getLinkIcon(proof.type),
}));
// If this is a successor market and there's a different parent provider,
// we'll just show that there's been a change, rather than add old data
// in alongside the new provider.
return (
<>
{parentProvider && (
<Lozenge variant={Intent.Primary}>{t('Updated')}</Lozenge>
)}
<span className="flex gap-1">
{provider.url && (
<span className="flex align-items-bottom text-md gap-1">
@@ -11,27 +11,16 @@ export const OracleDialog = ({
dataSourceSpecId,
open,
onChange,
parentProvider,
}: {
dataSourceSpecId: string;
provider: Provider;
open: boolean;
onChange?: (isOpen: boolean) => void;
parentProvider?: Provider;
}) => {
// If this is a successor market, the parent market data will only have been passed
// in if it differs from the current data. We'll pass this on to the title component
// to show a change, but the full profile showing changes is unwieldy - it's enough
// to know from the title that the oracle has changed.
const oracleMarkets = useOracleMarkets(provider);
return (
<Dialog
title={
<OracleProfileTitle
provider={provider}
parentProvider={parentProvider}
/>
}
title={<OracleProfileTitle provider={provider} />}
aria-labelledby="oracle-proof-dialog"
open={open}
onChange={onChange}
@@ -6,7 +6,6 @@ import {
ExternalLink,
Icon,
Intent,
Lozenge,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
@@ -19,30 +18,15 @@ import type { OracleMarketSpecFieldsFragment } from '../../__generated__/OracleM
import ReactMarkdown from 'react-markdown';
import { useState } from 'react';
export const OracleProfileTitle = ({
provider,
parentProvider,
}: {
provider: Provider;
parentProvider?: Provider;
}) => {
// If this is a successor market, the parent provider will only have been passed
// in if it differs from the current provider. If it is different, we'll just
// show the change in name, not icons and proofs.
export const OracleProfileTitle = ({ provider }: { provider: Provider }) => {
const { icon, intent } = getVerifiedStatusIcon(provider);
const verifiedProofs = provider.proofs.filter(
(proof) => proof.available === true
);
return (
<span className="flex gap-1">
{parentProvider && (
<Lozenge variant={Intent.Primary}>{t('Updated')}</Lozenge>
)}
{provider.url && (
<span>
{parentProvider && parentProvider.name && (
<span className="line-through">{parentProvider.name}</span>
)}
<span className="pr-1">{provider.name}</span>
<span className="dark:text-vega-light-300 text-vega-dark-300">
({verifiedProofs.length})
@@ -150,9 +150,3 @@ query StopOrders($partyId: ID!) {
}
}
}
query StopOrderById($stopOrderId: ID!) {
stopOrder(id: $stopOrderId) {
...StopOrderFields
}
}
@@ -32,7 +32,7 @@ export type OrdersUpdateSubscriptionVariables = Types.Exact<{
export type OrdersUpdateSubscription = { __typename?: 'Subscription', orders?: Array<{ __typename?: 'OrderUpdate', id: string, marketId: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, liquidityProvisionId?: string | null, peggedOrder?: { __typename: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null, icebergOrder?: { __typename: 'IcebergOrder', peakSize: string, minimumVisibleSize: string, reservedRemaining: string } | null }> | null };
export type OrderSubmissionFieldsFragment = { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null };
export type OrderSubmissionFieldsFragment = { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null};
export type StopOrderFieldsFragment = { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } };
@@ -43,13 +43,6 @@ export type StopOrdersQueryVariables = Types.Exact<{
export type StopOrdersQuery = { __typename?: 'Query', stopOrders?: { __typename?: 'StopOrderConnection', edges?: Array<{ __typename?: 'StopOrderEdge', node?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null }> | null } | null };
export type StopOrderByIdQueryVariables = Types.Exact<{
stopOrderId: Types.Scalars['ID'];
}>;
export type StopOrderByIdQuery = { __typename?: 'Query', stopOrder?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } } | null };
export const OrderFieldsFragmentDoc = gql`
fragment OrderFields on Order {
id
@@ -316,39 +309,4 @@ export function useStopOrdersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
}
export type StopOrdersQueryHookResult = ReturnType<typeof useStopOrdersQuery>;
export type StopOrdersLazyQueryHookResult = ReturnType<typeof useStopOrdersLazyQuery>;
export type StopOrdersQueryResult = Apollo.QueryResult<StopOrdersQuery, StopOrdersQueryVariables>;
export const StopOrderByIdDocument = gql`
query StopOrderById($stopOrderId: ID!) {
stopOrder(id: $stopOrderId) {
...StopOrderFields
}
}
${StopOrderFieldsFragmentDoc}`;
/**
* __useStopOrderByIdQuery__
*
* To run a query within a React component, call `useStopOrderByIdQuery` and pass it any options that fit your needs.
* When your component renders, `useStopOrderByIdQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useStopOrderByIdQuery({
* variables: {
* stopOrderId: // value for 'stopOrderId'
* },
* });
*/
export function useStopOrderByIdQuery(baseOptions: Apollo.QueryHookOptions<StopOrderByIdQuery, StopOrderByIdQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<StopOrderByIdQuery, StopOrderByIdQueryVariables>(StopOrderByIdDocument, options);
}
export function useStopOrderByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<StopOrderByIdQuery, StopOrderByIdQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<StopOrderByIdQuery, StopOrderByIdQueryVariables>(StopOrderByIdDocument, options);
}
export type StopOrderByIdQueryHookResult = ReturnType<typeof useStopOrderByIdQuery>;
export type StopOrderByIdLazyQueryHookResult = ReturnType<typeof useStopOrderByIdLazyQuery>;
export type StopOrderByIdQueryResult = Apollo.QueryResult<StopOrderByIdQuery, StopOrderByIdQueryVariables>;
export type StopOrdersQueryResult = Apollo.QueryResult<StopOrdersQuery, StopOrdersQueryVariables>;
@@ -95,6 +95,7 @@ export const OrderListManager = ({
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
isReadOnly={isReadOnly}
suppressAutoSize
overlayNoRowsTemplate={error ? error.message : t('No orders')}
{...gridProps}
/>
@@ -114,11 +114,9 @@ describe('OrderListTable', () => {
it('should apply correct formatting applied for an iceberg order', async () => {
const icebergOrder = {
...limitOrder,
size: '100',
remaining: '50',
icebergOrder: {
__typename: 'IcebergOrder',
minimumVisibleSize: '1',
minimumVisibleSize: '100',
peakSize: '50',
reservedRemaining: '50',
} as OrderFieldsFragment['icebergOrder'],
@@ -129,8 +127,8 @@ describe('OrderListTable', () => {
const cells = screen.getAllByRole('gridcell');
const expectedValues: string[] = [
icebergOrder.market?.tradableInstrument.instrument.code || '',
'0.00',
'+1.00',
'0.05',
'0.10',
Schema.OrderTypeMapping[
icebergOrder.type || Schema.OrderType.TYPE_LIMIT
] + ' (Iceberg)',
@@ -37,12 +37,6 @@ import type { Order } from '../order-data-provider';
import { Filter } from '../order-list-manager';
import type { ColDef } from 'ag-grid-community';
const defaultColDef = {
resizable: true,
sortable: true,
filterParams: { buttons: ['reset'] },
};
export type OrderListTableProps = TypedDataAgGrid<Order> & {
marketId?: string;
onCancel: (order: Order) => void;
@@ -82,6 +76,7 @@ export const OrderListTable = memo<
field: 'market.tradableInstrument.instrument.code',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'market.id', onMarketClick },
minWidth: 150,
},
{
headerName: t('Filled'),
@@ -91,13 +86,21 @@ export const OrderListTable = memo<
valueGetter: ({ data }: VegaValueGetterParams<Order>) => {
if (data?.icebergOrder) {
return data?.size && data.market
? BigInt(data.size) -
BigInt(data.remaining) -
BigInt(data.icebergOrder.reservedRemaining)
? toBigNum(
(
BigInt(data.size) -
BigInt(data.remaining) -
BigInt(data.icebergOrder.reservedRemaining)
).toString(),
data.market.positionDecimalPlaces ?? 0
).toNumber()
: undefined;
}
return data?.size && data.market
? BigInt(data.size) - BigInt(data.remaining)
? toBigNum(
(BigInt(data.size) - BigInt(data.remaining)).toString(),
data.market.positionDecimalPlaces ?? 0
).toNumber()
: undefined;
},
valueFormatter: ({
@@ -111,10 +114,13 @@ export const OrderListTable = memo<
return '-';
}
return addDecimalsFormatNumber(
value,
data.market.positionDecimalPlaces ?? 0
(BigInt(data.size) - BigInt(data.remaining)).toString(),
data.market.positionDecimalPlaces
);
},
minWidth: 50,
width: 90,
flex: 0,
},
{
headerName: t('Size'),
@@ -156,6 +162,9 @@ export const OrderListTable = memo<
)
);
},
minWidth: 50,
width: 80,
flex: 0,
},
{
field: 'type',
@@ -167,6 +176,7 @@ export const OrderListTable = memo<
cellRendererParams: {
onClick: onOrderTypeClick,
},
minWidth: 80,
},
{
field: 'status',
@@ -199,6 +209,7 @@ export const OrderListTable = memo<
{valueFormatted}
</span>
),
minWidth: 100,
},
{
field: 'price',
@@ -220,6 +231,7 @@ export const OrderListTable = memo<
}
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
},
minWidth: 100,
},
{
field: 'timeInForce',
@@ -248,6 +260,7 @@ export const OrderListTable = memo<
return label;
},
minWidth: 150,
},
{
field: 'updatedAt',
@@ -267,12 +280,13 @@ export const OrderListTable = memo<
</span>
);
},
minWidth: 150,
},
{
colId: 'amend',
...COL_DEFS.actions,
minWidth: showAllActions ? 110 : COL_DEFS.actions.minWidth,
maxWidth: showAllActions ? 110 : COL_DEFS.actions.minWidth,
minWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
maxWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
cellRenderer: ({ data }: { data?: Order }) => {
if (!data) return null;
@@ -330,8 +344,16 @@ export const OrderListTable = memo<
return (
<AgGrid
ref={ref}
defaultColDef={defaultColDef}
defaultColDef={{
resizable: true,
sortable: true,
filterParams: { buttons: ['reset'] },
}}
columnDefs={columnDefs}
style={{
width: '100%',
height: '100%',
}}
getRowId={({ data }) => data.id}
components={{ MarketNameCell, OrderTypeCell }}
{...props}
@@ -2,13 +2,17 @@ import { render, screen, act } from '@testing-library/react';
import { StopOrdersManager } from './stop-orders-manager';
import * as useDataProviderHook from '@vegaprotocol/data-provider';
import type { StopOrder } from '../order-data-provider/stop-orders-data-provider';
import * as stopOrdersTableMock from '../stop-orders-table/stop-orders-table';
import { forwardRef } from 'react';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { MockedProvider } from '@apollo/client/testing';
jest.mock('../stop-orders-table/stop-orders-table', () => ({
StopOrdersTable: () => <div>StopOrdersTable</div>,
}));
// @ts-ignore StopOrdersTable is read only but we need to override with the forwardRef to
// avoid warnings about padding refs
stopOrdersTableMock.StopOrdersTable = forwardRef(() => (
<div>StopOrdersTable</div>
));
const generateJsx = () => {
const pubKey = '0x123';
@@ -3,13 +3,12 @@ import {
getDateTimeFormat,
isNumeric,
toBigNum,
formatTrigger,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import type { ForwardedRef } from 'react';
import { memo, useMemo } from 'react';
import { memo, forwardRef, useMemo } from 'react';
import {
AgGridLazy as AgGrid,
SetFilter,
@@ -29,12 +28,6 @@ import type { AgGridReact } from 'ag-grid-react';
import type { StopOrder } from '../order-data-provider/stop-orders-data-provider';
import type { ColDef } from 'ag-grid-community';
const defaultColDef = {
resizable: true,
sortable: true,
filterParams: { buttons: ['reset'] },
};
export type StopOrdersTableProps = TypedDataAgGrid<StopOrder> & {
onCancel: (order: StopOrder) => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
@@ -43,214 +36,265 @@ export type StopOrdersTableProps = TypedDataAgGrid<StopOrder> & {
export const StopOrdersTable = memo<
StopOrdersTableProps & { ref?: ForwardedRef<AgGridReact> }
>(({ onCancel, onMarketClick, ...props }: StopOrdersTableProps) => {
const showAllActions = !props.isReadOnly;
const columnDefs: ColDef[] = useMemo(
() => [
{
headerName: t('Market'),
field: 'market.tradableInstrument.instrument.code',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'market.id', onMarketClick },
},
{
headerName: t('Trigger'),
field: 'trigger',
cellClass: 'font-mono text-right',
type: 'rightAligned',
sortable: false,
valueFormatter: ({
data,
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string =>
data ? formatTrigger(data, data.market.decimalPlaces) : '',
},
{
field: 'expiresAt',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<StopOrder, 'expiresAt'>) => {
if (
data &&
value &&
data?.expiryStrategy !==
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_UNSPECIFIED
) {
const expiresAt = getDateTimeFormat().format(new Date(value));
const expiryStrategy =
data.expiryStrategy ===
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
? t('Submit')
: t('Cancels');
return `${expiryStrategy} ${expiresAt}`;
}
return '';
},
},
{
headerName: t('Size'),
field: 'submission.size',
cellClass: 'font-mono text-right',
type: 'rightAligned',
cellClassRules: {
[positiveClassNames]: ({ data }: { data: StopOrder }) =>
data?.submission.size === Schema.Side.SIDE_BUY,
[negativeClassNames]: ({ data }: { data: StopOrder }) =>
data?.submission.size === Schema.Side.SIDE_SELL,
},
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) => {
return data?.submission.size && data.market
? toBigNum(
data.submission.size,
data.market.positionDecimalPlaces ?? 0
)
.multipliedBy(
data.submission.side === Schema.Side.SIDE_SELL ? -1 : 1
>(
forwardRef<AgGridReact, StopOrdersTableProps>(
({ onCancel, onMarketClick, ...props }, ref) => {
const showAllActions = !props.isReadOnly;
const columnDefs: ColDef[] = useMemo(
() => [
{
headerName: t('Market'),
field: 'market.tradableInstrument.instrument.code',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'market.id', onMarketClick },
minWidth: 150,
},
{
headerName: t('Trigger'),
field: 'trigger',
cellClass: 'font-mono text-right',
type: 'rightAligned',
sortable: false,
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string => {
if (data && value?.__typename === 'StopOrderPrice') {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '<'
: '>'
} ${addDecimalsFormatNumber(
value.price,
data.market.decimalPlaces
)}`;
}
if (
data &&
value?.__typename === 'StopOrderTrailingPercentOffset'
) {
return `${t('Mark')} ${
data?.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
? '+'
: '-'
}${(Number(value.trailingPercentOffset) * 100).toFixed(1)}%`;
}
return '-';
},
minWidth: 100,
},
{
field: 'expiresAt',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<StopOrder, 'expiresAt'>) => {
if (
data &&
value &&
data?.expiryStrategy !==
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_UNSPECIFIED
) {
const expiresAt = getDateTimeFormat().format(new Date(value));
const expiryStrategy =
data.expiryStrategy ===
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
? t('Submit')
: t('Cancels');
return `${expiryStrategy} ${expiresAt}`;
}
return '';
},
minWidth: 150,
},
{
headerName: t('Size'),
field: 'submission.size',
cellClass: 'font-mono text-right',
type: 'rightAligned',
cellClassRules: {
[positiveClassNames]: ({ data }: { data: StopOrder }) =>
data?.submission.size === Schema.Side.SIDE_BUY,
[negativeClassNames]: ({ data }: { data: StopOrder }) =>
data?.submission.size === Schema.Side.SIDE_SELL,
},
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) => {
return data?.submission.size && data.market
? toBigNum(
data.submission.size,
data.market.positionDecimalPlaces ?? 0
)
.multipliedBy(
data.submission.side === Schema.Side.SIDE_SELL ? -1 : 1
)
.toNumber()
: undefined;
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<StopOrder, 'size'>) => {
if (!data) {
return '';
}
if (!data?.market || !isNumeric(data.submission.size)) {
return '-';
}
const prefix = data
? data.submission.side === Schema.Side.SIDE_BUY
? '+'
: '-'
: '';
return (
prefix +
addDecimalsFormatNumber(
data.submission.size,
data.market.positionDecimalPlaces
)
.toNumber()
: undefined;
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<StopOrder, 'size'>) => {
if (!data) {
return '';
}
if (!data?.market || !isNumeric(data.submission.size)) {
return '-';
}
const prefix = data
? data.submission.side === Schema.Side.SIDE_BUY
? '+'
: '-'
: '';
return (
prefix +
addDecimalsFormatNumber(
data.submission.size,
data.market.positionDecimalPlaces
)
);
},
},
{
field: 'submission.type',
filter: SetFilter,
filterParams: {
set: Schema.OrderTypeMapping,
},
cellRenderer: ({
value,
}: VegaICellRendererParams<StopOrder, 'submission.type'>) =>
value ? Schema.OrderTypeMapping[value] : '',
},
{
field: 'status',
filter: SetFilter,
filterParams: {
set: Schema.StopOrderStatusMapping,
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<StopOrder, 'status'>) => {
return value ? Schema.StopOrderStatusMapping[value] : '';
},
cellRenderer: ({
valueFormatted,
data,
}: {
valueFormatted: string;
data: StopOrder;
}) => (
<span data-testid={`order-status-${data?.id}`}>{valueFormatted}</span>
),
},
{
field: 'submission.price',
type: 'rightAligned',
cellClass: 'font-mono text-right',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<StopOrder, 'submission.price'>) => {
if (!data) {
return '';
}
if (
!data?.market ||
data.submission.type === Schema.OrderType.TYPE_MARKET ||
!isNumeric(value)
) {
return '-';
}
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
},
},
{
field: 'submission.timeInForce',
filter: SetFilter,
filterParams: {
set: Schema.OrderTimeInForceMapping,
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<StopOrder, 'submission.timeInForce'>) => {
return value ? Schema.OrderTimeInForceCode[value] : '';
},
},
{
field: 'updatedAt',
filter: DateRangeFilter,
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) =>
data?.updatedAt || data?.createdAt,
cellRenderer: ({
data,
}: VegaICellRendererParams<StopOrder, 'createdAt'>) => {
if (!data) {
return undefined;
}
const value = data.updatedAt || data.createdAt;
return (
<span data-value={value}>
{value ? getDateTimeFormat().format(new Date(value)) : '-'}
</span>
);
},
},
{
colId: 'actions',
...COL_DEFS.actions,
minWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
maxWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
cellRenderer: ({ data }: { data?: StopOrder }) => {
if (!data) return null;
);
},
minWidth: 80,
},
{
field: 'submission.type',
filter: SetFilter,
filterParams: {
set: Schema.OrderTypeMapping,
},
cellRenderer: ({
value,
}: VegaICellRendererParams<StopOrder, 'submission.type'>) =>
value ? Schema.OrderTypeMapping[value] : '',
minWidth: 80,
},
{
field: 'status',
filter: SetFilter,
filterParams: {
set: Schema.StopOrderStatusMapping,
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<StopOrder, 'status'>) => {
return value ? Schema.StopOrderStatusMapping[value] : '';
},
cellRenderer: ({
valueFormatted,
data,
}: {
valueFormatted: string;
data: StopOrder;
}) => (
<span data-testid={`order-status-${data?.id}`}>
{valueFormatted}
</span>
),
minWidth: 100,
},
{
field: 'submission.price',
type: 'rightAligned',
cellClass: 'font-mono text-right',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<StopOrder, 'submission.price'>) => {
if (!data) {
return '';
}
if (
!data?.market ||
data.submission.type === Schema.OrderType.TYPE_MARKET ||
!isNumeric(value)
) {
return '-';
}
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
},
minWidth: 100,
},
{
field: 'submission.timeInForce',
filter: SetFilter,
filterParams: {
set: Schema.OrderTimeInForceMapping,
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<
StopOrder,
'submission.timeInForce'
>) => {
return value ? Schema.OrderTimeInForceCode[value] : '';
},
minWidth: 150,
},
{
field: 'updatedAt',
filter: DateRangeFilter,
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) =>
data?.updatedAt || data?.createdAt,
cellRenderer: ({
data,
}: VegaICellRendererParams<StopOrder, 'createdAt'>) => {
if (!data) {
return undefined;
}
const value = data.updatedAt || data.createdAt;
return (
<span data-value={value}>
{value ? getDateTimeFormat().format(new Date(value)) : '-'}
</span>
);
},
minWidth: 150,
},
{
colId: 'actions',
...COL_DEFS.actions,
minWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
maxWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
cellRenderer: ({ data }: { data?: StopOrder }) => {
if (!data) return null;
return (
<div className="flex gap-2 items-center justify-end">
{data.status === Schema.StopOrderStatus.STATUS_PENDING &&
!props.isReadOnly && (
<ButtonLink
data-testid="cancel"
onClick={() => onCancel(data)}
>
{t('Cancel')}
</ButtonLink>
)}
</div>
);
},
},
],
[onCancel, onMarketClick, props.isReadOnly, showAllActions]
);
return (
<div className="flex gap-2 items-center justify-end">
{data.status === Schema.StopOrderStatus.STATUS_PENDING &&
!props.isReadOnly && (
<ButtonLink
data-testid="cancel"
onClick={() => onCancel(data)}
>
{t('Cancel')}
</ButtonLink>
)}
</div>
);
},
},
],
[onCancel, onMarketClick, props.isReadOnly, showAllActions]
);
return (
<AgGrid
defaultColDef={defaultColDef}
columnDefs={columnDefs}
getRowId={({ data }) => data.id}
components={{ MarketNameCell }}
{...props}
/>
);
});
return (
<AgGrid
ref={ref}
defaultColDef={{
resizable: true,
sortable: true,
filterParams: { buttons: ['reset'] },
}}
columnDefs={columnDefs}
style={{
width: '100%',
height: '100%',
}}
getRowId={({ data }) => data.id}
components={{ MarketNameCell }}
{...props}
/>
);
}
)
);
+1
View File
@@ -1,3 +1,4 @@
export * from './__generated__/OrdersSubscription';
export * from './use-has-amendable-order';
export * from './use-order-update';
export * from './use-order-store';

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