From fc5641cf33ebbf393031b9ff9f6bb72ef6335666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Tue, 16 May 2023 21:50:31 +0200 Subject: [PATCH] feat: add margin charts to deal-ticket fee details --- .../trading-deal-ticket-basics.cy.ts | 2 +- .../trading-deal-ticket-order.cy.ts | 8 +- .../trading-deal-ticket-submit-account.cy.ts | 6 +- ...trading-deal-ticket-submit-suspended.cy.ts | 6 +- .../client-pages/market/trade-grid.tsx | 2 + .../client-pages/portfolio/portfolio.tsx | 5 +- .../accounts-container/accounts-container.tsx | 3 + libs/accounts/src/lib/Margins.graphql | 38 ++ .../accounts/src/lib/__generated__/Margins.ts | 114 ++++++ libs/accounts/src/lib/accounts-manager.tsx | 36 +- libs/accounts/src/lib/accounts-table.tsx | 2 +- .../accounts/src/lib/breakdown-table.spec.tsx | 8 +- libs/accounts/src/lib/breakdown-table.tsx | 47 ++- libs/accounts/src/lib/index.ts | 3 + .../src/lib/margin-data-provider.ts | 4 +- libs/accounts/src/lib/margin-health-chart.tsx | 195 +++++++++ .../src/lib/cells/grid-progress-bar.tsx | 12 - .../src/lib/cells/market-name-cell.tsx | 12 +- .../deal-ticket-validation/margin-warning.tsx | 2 +- .../zero-balance-error.tsx | 2 +- .../deal-ticket-fee-details.spec.tsx} | 4 +- .../deal-ticket/deal-ticket-fee-details.tsx | 379 ++++++++++++++++-- .../deal-ticket/deal-ticket-limit-amount.tsx | 4 +- .../deal-ticket/deal-ticket-market-amount.tsx | 2 +- .../components/deal-ticket/deal-ticket.tsx | 18 +- .../deal-ticket/expiry-selector.tsx | 2 +- .../deal-ticket/time-in-force-selector.tsx | 2 +- .../components/deal-ticket/type-selector.tsx | 2 +- libs/deal-ticket/src/hooks/index.ts | 2 +- .../src/hooks/use-estimate-fees.tsx | 25 ++ .../src/hooks/use-fee-deal-ticket-details.tsx | 327 --------------- libs/positions/src/index.ts | 1 - libs/positions/src/lib/Positions.graphql | 39 -- .../src/lib/__generated__/Positions.ts | 109 ----- libs/positions/src/lib/positions.mock.ts | 5 +- libs/positions/src/lib/use-market-margin.tsx | 4 +- 36 files changed, 838 insertions(+), 594 deletions(-) create mode 100644 libs/accounts/src/lib/Margins.graphql create mode 100644 libs/accounts/src/lib/__generated__/Margins.ts rename libs/{positions => accounts}/src/lib/margin-data-provider.ts (96%) create mode 100644 libs/accounts/src/lib/margin-health-chart.tsx rename libs/deal-ticket/src/{hooks/use-fee-deal-ticket-details.spec.tsx => components/deal-ticket/deal-ticket-fee-details.spec.tsx} (93%) create mode 100644 libs/deal-ticket/src/hooks/use-estimate-fees.tsx delete mode 100644 libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.tsx diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket-basics.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket-basics.cy.ts index aa5fa7a0c..9512f317a 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket-basics.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket-basics.cy.ts @@ -81,7 +81,7 @@ describe( cy.visit('/#/markets/market-0'); }); it('must display that market is not accepting orders', function () { - cy.getByTestId('dealticket-error-message-summary').should( + cy.getByTestId('deal-ticket-error-message-summary').should( 'have.text', `This market is ${marketState .split('_') diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts index 9e3030f10..8de406c00 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts @@ -45,7 +45,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => { cy.getByTestId(placeOrderBtn).click(); - cy.getByTestId('dealticket-error-message-expiry').should( + cy.getByTestId('deal-ticket-error-message-expiry').should( 'have.text', 'The expiry date that you have entered appears to be in the past' ); @@ -57,7 +57,7 @@ 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('dealticket-error-message-price-limit').should( + cy.getByTestId('deal-ticket-error-message-price-limit').should( 'have.text', 'Price accepts up to 5 decimal places' ); @@ -79,7 +79,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => { cy.getByTestId(orderSizeField).clear().type('1.234'); // 7002-SORD-060 cy.getByTestId(placeOrderBtn).should('be.enabled'); - cy.getByTestId('dealticket-error-message-size-market').should( + cy.getByTestId('deal-ticket-error-message-size-market').should( 'have.text', 'Size must be whole numbers for this market' ); @@ -88,7 +88,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => { it('must warn if order size is set to 0', function () { cy.getByTestId(orderSizeField).clear().type('0'); cy.getByTestId(placeOrderBtn).should('be.enabled'); - cy.getByTestId('dealticket-error-message-size-market').should( + cy.getByTestId('deal-ticket-error-message-size-market').should( 'have.text', 'Size cannot be lower than 1' ); diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket-submit-account.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket-submit-account.cy.ts index fb703229b..e01dd607d 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket-submit-account.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket-submit-account.cy.ts @@ -26,7 +26,7 @@ describe( // 7002-SORD-060 cy.getByTestId('place-order').should('be.enabled'); // 7002-SORD-003 - cy.getByTestId('dealticket-error-message-zero-balance').should( + cy.getByTestId('deal-ticket-error-message-zero-balance').should( 'have.text', 'You need ' + 'tDAI' + @@ -54,11 +54,11 @@ describe( // 7002-SORD-003 // warning should show immediately - cy.getByTestId('dealticket-warning-margin').should( + cy.getByTestId('deal-ticket-warning-margin').should( 'contain.text', 'You may not have enough margin available to open this position' ); - cy.getByTestId('dealticket-warning-margin').should( + cy.getByTestId('deal-ticket-warning-margin').should( 'contain.text', 'You may not have enough margin available to open this position. 5.00 tDAI is currently required. You have only 0.01001 tDAI available.' ); diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket-submit-suspended.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket-submit-suspended.cy.ts index 18405b4e9..5dd19a79d 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket-submit-suspended.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket-submit-suspended.cy.ts @@ -37,7 +37,7 @@ describe('suspended market validation', { tags: '@regression' }, () => { // 7002-SORD-060 cy.getByTestId(placeOrderBtn).should('be.enabled'); cy.getByTestId(placeOrderBtn).click(); - cy.getByTestId('dealticket-error-message-type').should( + cy.getByTestId('deal-ticket-error-message-type').should( 'have.text', 'This market is in auction until it reaches sufficient liquidity. Only limit orders are permitted when market is in auction' ); @@ -48,7 +48,7 @@ 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('dealticket-warning-auction').should( + cy.getByTestId('deal-ticket-warning-auction').should( 'have.text', 'Any orders placed now will not trade until the auction ends' ); @@ -60,7 +60,7 @@ describe('suspended market validation', { tags: '@regression' }, () => { TIFlist.filter((item) => item.code === 'FOK')[0].value ); cy.getByTestId(placeOrderBtn).should('be.enabled'); - cy.getByTestId('dealticket-error-message-tif').should( + 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' ); diff --git a/apps/trading/client-pages/market/trade-grid.tsx b/apps/trading/client-pages/market/trade-grid.tsx index 632c87b35..0078ca0ba 100644 --- a/apps/trading/client-pages/market/trade-grid.tsx +++ b/apps/trading/client-pages/market/trade-grid.tsx @@ -143,6 +143,7 @@ const MarketBottomPanel = memo( diff --git a/apps/trading/client-pages/portfolio/portfolio.tsx b/apps/trading/client-pages/portfolio/portfolio.tsx index 4968b459b..8e6a8fe91 100644 --- a/apps/trading/client-pages/portfolio/portfolio.tsx +++ b/apps/trading/client-pages/portfolio/portfolio.tsx @@ -92,7 +92,10 @@ export const Portfolio = () => { - + diff --git a/apps/trading/components/accounts-container/accounts-container.tsx b/apps/trading/components/accounts-container/accounts-container.tsx index 6b7902f44..bec2f3724 100644 --- a/apps/trading/components/accounts-container/accounts-container.tsx +++ b/apps/trading/components/accounts-container/accounts-container.tsx @@ -14,11 +14,13 @@ export const AccountsContainer = ({ hideButtons, noBottomPlaceholder, storeKey, + onMarketClick, }: { pinnedAsset?: PinnedAsset; hideButtons?: boolean; noBottomPlaceholder?: boolean; storeKey?: string; + onMarketClick?: (marketId: string, metaKey?: boolean) => void; }) => { const { pubKey, isReadOnly } = useVegaWallet(); const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); @@ -48,6 +50,7 @@ export const AccountsContainer = ({ onClickAsset={onClickAsset} onClickWithdraw={openWithdrawalDialog} onClickDeposit={openDepositDialog} + onMarketClick={onMarketClick} isReadOnly={isReadOnly} pinnedAsset={pinnedAsset} noBottomPlaceholder={noBottomPlaceholder} diff --git a/libs/accounts/src/lib/Margins.graphql b/libs/accounts/src/lib/Margins.graphql new file mode 100644 index 000000000..9580c96af --- /dev/null +++ b/libs/accounts/src/lib/Margins.graphql @@ -0,0 +1,38 @@ +fragment MarginFields on MarginLevels { + maintenanceLevel + searchLevel + initialLevel + collateralReleaseLevel + asset { + id + } + market { + id + } +} + +query Margins($partyId: ID!) { + party(id: $partyId) { + id + marginsConnection { + edges { + node { + ...MarginFields + } + } + } + } +} + +subscription MarginsSubscription($partyId: ID!) { + margins(partyId: $partyId) { + marketId + asset + partyId + maintenanceLevel + searchLevel + initialLevel + collateralReleaseLevel + timestamp + } +} diff --git a/libs/accounts/src/lib/__generated__/Margins.ts b/libs/accounts/src/lib/__generated__/Margins.ts new file mode 100644 index 000000000..daf477bc6 --- /dev/null +++ b/libs/accounts/src/lib/__generated__/Margins.ts @@ -0,0 +1,114 @@ +import * as Types from '@vegaprotocol/types'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } }; + +export type MarginsQueryVariables = Types.Exact<{ + partyId: Types.Scalars['ID']; +}>; + + +export type MarginsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, marginsConnection?: { __typename?: 'MarginConnection', edges?: Array<{ __typename?: 'MarginEdge', node: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } } }> | null } | null } | null }; + +export type MarginsSubscriptionSubscriptionVariables = Types.Exact<{ + partyId: Types.Scalars['ID']; +}>; + + +export type MarginsSubscriptionSubscription = { __typename?: 'Subscription', margins: { __typename?: 'MarginLevelsUpdate', marketId: string, asset: string, partyId: string, maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, timestamp: any } }; + +export const MarginFieldsFragmentDoc = gql` + fragment MarginFields on MarginLevels { + maintenanceLevel + searchLevel + initialLevel + collateralReleaseLevel + asset { + id + } + market { + id + } +} + `; +export const MarginsDocument = gql` + query Margins($partyId: ID!) { + party(id: $partyId) { + id + marginsConnection { + edges { + node { + ...MarginFields + } + } + } + } +} + ${MarginFieldsFragmentDoc}`; + +/** + * __useMarginsQuery__ + * + * To run a query within a React component, call `useMarginsQuery` and pass it any options that fit your needs. + * When your component renders, `useMarginsQuery` 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 } = useMarginsQuery({ + * variables: { + * partyId: // value for 'partyId' + * }, + * }); + */ +export function useMarginsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(MarginsDocument, options); + } +export function useMarginsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(MarginsDocument, options); + } +export type MarginsQueryHookResult = ReturnType; +export type MarginsLazyQueryHookResult = ReturnType; +export type MarginsQueryResult = Apollo.QueryResult; +export const MarginsSubscriptionDocument = gql` + subscription MarginsSubscription($partyId: ID!) { + margins(partyId: $partyId) { + marketId + asset + partyId + maintenanceLevel + searchLevel + initialLevel + collateralReleaseLevel + timestamp + } +} + `; + +/** + * __useMarginsSubscriptionSubscription__ + * + * To run a query within a React component, call `useMarginsSubscriptionSubscription` and pass it any options that fit your needs. + * When your component renders, `useMarginsSubscriptionSubscription` 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 subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useMarginsSubscriptionSubscription({ + * variables: { + * partyId: // value for 'partyId' + * }, + * }); + */ +export function useMarginsSubscriptionSubscription(baseOptions: Apollo.SubscriptionHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSubscription(MarginsSubscriptionDocument, options); + } +export type MarginsSubscriptionSubscriptionHookResult = ReturnType; +export type MarginsSubscriptionSubscriptionResult = Apollo.SubscriptionResult; \ No newline at end of file diff --git a/libs/accounts/src/lib/accounts-manager.tsx b/libs/accounts/src/lib/accounts-manager.tsx index 8f9ee3517..600f33fe9 100644 --- a/libs/accounts/src/lib/accounts-manager.tsx +++ b/libs/accounts/src/lib/accounts-manager.tsx @@ -18,14 +18,25 @@ import BreakdownTable from './breakdown-table'; const AccountBreakdown = ({ assetId, partyId, + onMarketClick, }: { assetId: string; partyId: string; + onMarketClick?: (marketId: string, metaKey?: boolean) => void; }) => { + const gridRef = useRef(null); const { data } = useDataProvider({ dataProvider: aggregatedAccountDataProvider, variables: { partyId, assetId }, + update: ({ data }) => { + if (gridRef.current?.api && data?.breakdown) { + gridRef.current?.api.setRowData(data?.breakdown); + return true; + } + return false; + }, }); + return (
)} - +
); }; @@ -52,6 +68,7 @@ interface AccountManagerProps { onClickAsset: (assetId: string) => void; onClickWithdraw?: (assetId?: string) => void; onClickDeposit?: (assetId?: string) => void; + onMarketClick?: (marketId: string, metaKey?: boolean) => void; isReadOnly: boolean; pinnedAsset?: PinnedAsset; noBottomPlaceholder?: boolean; @@ -67,6 +84,7 @@ export const AccountManager = ({ pinnedAsset, noBottomPlaceholder, storeKey, + onMarketClick, }: AccountManagerProps) => { const gridRef = useRef(null); const [breakdownAssetId, setBreakdownAssetId] = useState(); @@ -109,6 +127,16 @@ export const AccountManager = ({ disabled: noBottomPlaceholder, }); + const onMarketClickInternal = useCallback( + (...args: Parameters>) => { + setBreakdownAssetId(undefined); + if (onMarketClick) { + onMarketClick(...args); + } + }, + [onMarketClick] + ); + return (
{breakdownAssetId && ( - + )}
diff --git a/libs/accounts/src/lib/accounts-table.tsx b/libs/accounts/src/lib/accounts-table.tsx index b62ba37c8..e9670ff8e 100644 --- a/libs/accounts/src/lib/accounts-table.tsx +++ b/libs/accounts/src/lib/accounts-table.tsx @@ -44,7 +44,7 @@ const colorClass = (percentageUsed: number, neutral = false) => { export const percentageValue = (part?: string, total?: string) => new BigNumber(part || 0) - .dividedBy(total || 1) + .dividedBy(!total || total === '0' ? 1 : total) .multipliedBy(100) .toNumber(); diff --git a/libs/accounts/src/lib/breakdown-table.spec.tsx b/libs/accounts/src/lib/breakdown-table.spec.tsx index 16bb21b80..1c6c915a7 100644 --- a/libs/accounts/src/lib/breakdown-table.spec.tsx +++ b/libs/accounts/src/lib/breakdown-table.spec.tsx @@ -4,6 +4,8 @@ import * as Types from '@vegaprotocol/types'; import type { AccountFields } from './accounts-data-provider'; import { getAccountData } from './accounts-data-provider'; +jest.mock('./margin-health-chart'); + const singleRow = { __typename: 'AccountBalance', type: Types.AccountType.ACCOUNT_TYPE_MARGIN, @@ -37,10 +39,10 @@ describe('BreakdownTable', () => { render(); }); const headers = await screen.findAllByRole('columnheader'); - expect(headers).toHaveLength(3); + expect(headers).toHaveLength(4); expect( headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim()) - ).toEqual(['Market', 'Account type', 'Balance']); + ).toEqual(['Market', 'Account type', 'Balance', 'Margin health']); }); it('should apply correct formatting', async () => { @@ -55,7 +57,7 @@ describe('BreakdownTable', () => { '1,256.00', '1,256.00', ]; - cells.forEach((cell, i) => { + cells.slice(0, -1).forEach((cell, i) => { expect(cell).toHaveTextContent(expectedValues[i]); }); }); diff --git a/libs/accounts/src/lib/breakdown-table.tsx b/libs/accounts/src/lib/breakdown-table.tsx index b3beb43aa..e25dba47b 100644 --- a/libs/accounts/src/lib/breakdown-table.tsx +++ b/libs/accounts/src/lib/breakdown-table.tsx @@ -9,11 +9,15 @@ import { AccountTypeMapping } from '@vegaprotocol/types'; import type { ValueProps, VegaValueFormatterParams, + VegaICellRendererParams, } from '@vegaprotocol/datagrid'; -import { progressBarCellRendererSelector } from '@vegaprotocol/datagrid'; +import { ProgressBarCell } from '@vegaprotocol/datagrid'; import { AgGridLazy as AgGrid, PriceCell } from '@vegaprotocol/datagrid'; import type { ValueFormatterParams } from 'ag-grid-community'; import { accountValuesComparator } from './accounts-table'; +import { MarginHealthChart } from './margin-health-chart'; +import { MarketNameCell } from '@vegaprotocol/datagrid'; +import { AccountType } from '@vegaprotocol/types'; export const progressBarValueFormatter = ({ data, @@ -36,10 +40,11 @@ export const progressBarValueFormatter = ({ interface BreakdownTableProps extends AgGridReactProps { data: AccountFields[] | null; + onMarketClick?: (marketId: string, metaKey?: boolean) => void; } const BreakdownTable = forwardRef( - ({ data }, ref) => { + ({ data, onMarketClick }, ref) => { return ( ( } ref={ref} rowHeight={34} - components={{ PriceCell }} + components={{ PriceCell, MarketNameCell, ProgressBarCell }} tooltipShowDelay={500} defaultColDef={{ flex: 1, @@ -61,21 +66,18 @@ const BreakdownTable = forwardRef( ) => { - if (!value) return 'None'; - return value; + cellRendererParams={{ + onMarketClick, + defaultValue: t('None'), + idPath: 'market.id', }} + cellRenderer="MarketNameCell" minWidth={200} /> ) => @@ -90,10 +92,29 @@ const BreakdownTable = forwardRef( field="used" flex={2} maxWidth={500} - cellRendererSelector={progressBarCellRendererSelector} + cellRenderer="ProgressBarCell" valueFormatter={progressBarValueFormatter} comparator={accountValuesComparator} /> + ) => + data?.market?.id && + data.type === AccountType['ACCOUNT_TYPE_MARGIN'] && + data?.asset.id ? ( + + ) : null + } + /> ); } diff --git a/libs/accounts/src/lib/index.ts b/libs/accounts/src/lib/index.ts index ce1402607..dc1203f73 100644 --- a/libs/accounts/src/lib/index.ts +++ b/libs/accounts/src/lib/index.ts @@ -8,3 +8,6 @@ export * from './use-account-balance'; export * from './get-settlement-account'; export * from './use-market-account-balance'; export * from './transfer-dialog'; +export * from './__generated__/Margins'; +export * from './margin-health-chart'; +export * from './margin-data-provider'; diff --git a/libs/positions/src/lib/margin-data-provider.ts b/libs/accounts/src/lib/margin-data-provider.ts similarity index 96% rename from libs/positions/src/lib/margin-data-provider.ts rename to libs/accounts/src/lib/margin-data-provider.ts index e18ba0180..7e59da2f2 100644 --- a/libs/positions/src/lib/margin-data-provider.ts +++ b/libs/accounts/src/lib/margin-data-provider.ts @@ -7,13 +7,13 @@ import { import { MarginsSubscriptionDocument, MarginsDocument, -} from './__generated__/Positions'; +} from './__generated__/Margins'; import type { MarginsQuery, MarginFieldsFragment, MarginsSubscriptionSubscription, MarginsQueryVariables, -} from './__generated__/Positions'; +} from './__generated__/Margins'; const update = ( data: MarginFieldsFragment[] | null, diff --git a/libs/accounts/src/lib/margin-health-chart.tsx b/libs/accounts/src/lib/margin-health-chart.tsx new file mode 100644 index 000000000..69dbc9fbf --- /dev/null +++ b/libs/accounts/src/lib/margin-health-chart.tsx @@ -0,0 +1,195 @@ +import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; +import { useVegaWallet } from '@vegaprotocol/wallet'; +import { Tooltip, ExternalLink } from '@vegaprotocol/ui-toolkit'; +import { useDataProvider } from '@vegaprotocol/data-provider'; +import { marketMarginDataProvider } from './margin-data-provider'; +import { useAssetsMapProvider } from '@vegaprotocol/assets'; +import { t } from '@vegaprotocol/i18n'; +import { useAccountBalance } from './use-account-balance'; +import { useMarketAccountBalance } from './use-market-account-balance'; + +const TooltipContentRow = ({ + label, + value, + decimals, + href, +}: { + label: string; + value: string; + decimals: number; + href?: string; +}) => ( + <> +
+ {href ? ( + + {label} + + ) : ( + label + )} +
+
+ {addDecimalsFormatNumber(value, decimals)} +
+ +); + +export const MarginHealthChart = ({ + marketId, + assetId, +}: { + marketId: string; + assetId: string; +}) => { + const { data: assetsMap } = useAssetsMapProvider(); + const { pubKey: partyId } = useVegaWallet(); + const { data } = useDataProvider({ + dataProvider: marketMarginDataProvider, + variables: { marketId, partyId: partyId ?? '' }, + skip: !partyId, + }); + const { accountBalance: rawGeneralAccountBalance } = + useAccountBalance(assetId); + const { accountBalance: rawMarginAccountBalance } = + useMarketAccountBalance(marketId); + const asset = assetsMap && assetsMap[assetId]; + if (!data || !asset) { + return null; + } + const { decimals } = asset; + + const collateralReleaseLevel = Number(data.collateralReleaseLevel); + const initialLevel = Number(data.initialLevel); + const maintenanceLevel = Number(data.maintenanceLevel); + const searchLevel = Number(data.searchLevel); + const marginAccountBalance = Number(rawMarginAccountBalance); + const generalAccountBalance = Number(rawGeneralAccountBalance); + const max = Math.max( + marginAccountBalance + generalAccountBalance, + collateralReleaseLevel + ); + + const red = maintenanceLevel / max; + const orange = (searchLevel - maintenanceLevel) / max; + const yellow = ((searchLevel + initialLevel) / 2 - searchLevel) / max; + const green = (collateralReleaseLevel - initialLevel) / max + yellow; + const balanceMarker = marginAccountBalance / max; + + const tooltipContent = [ + , + , + , + , + ]; + + if (rawGeneralAccountBalance) { + const balance = ( + + ); + if (BigInt(rawMarginAccountBalance) < BigInt(data.searchLevel)) { + tooltipContent.splice(1, 0, balance); + } else if (BigInt(rawMarginAccountBalance) < BigInt(data.initialLevel)) { + tooltipContent.splice(2, 0, balance); + } else if ( + BigInt(rawMarginAccountBalance) < BigInt(data.collateralReleaseLevel) + ) { + tooltipContent.splice(3, 0, balance); + } else { + tooltipContent.push(balance); + } + } + + return ( +
+ {addDecimalsFormatNumber( + (BigInt(marginAccountBalance) - BigInt(maintenanceLevel)).toString(), + decimals + )}{' '} + {t('above')}{' '} + + {t('maintenance level')} + + {tooltipContent}
} + > +
+
+
+
+
+ {balanceMarker > 0 && balanceMarker < 100 && ( +
+ )} +
+ + + ); +}; diff --git a/libs/datagrid/src/lib/cells/grid-progress-bar.tsx b/libs/datagrid/src/lib/cells/grid-progress-bar.tsx index 098f2339c..0c822596b 100644 --- a/libs/datagrid/src/lib/cells/grid-progress-bar.tsx +++ b/libs/datagrid/src/lib/cells/grid-progress-bar.tsx @@ -1,9 +1,5 @@ import type { Intent } from '@vegaprotocol/ui-toolkit'; import { ProgressBar } from '@vegaprotocol/ui-toolkit'; -import type { - CellRendererSelectorResult, - ICellRendererParams, -} from 'ag-grid-community'; export interface ValueProps { valueFormatted?: { @@ -32,11 +28,3 @@ export const ProgressBarCell = ({ valueFormatted }: ValueProps) => { ) : null; }; - -export const progressBarCellRendererSelector = ( - params: ICellRendererParams -): CellRendererSelectorResult => { - return { - component: ProgressBarCell, - }; -}; diff --git a/libs/datagrid/src/lib/cells/market-name-cell.tsx b/libs/datagrid/src/lib/cells/market-name-cell.tsx index 9ce81c21d..2f5dea4b1 100644 --- a/libs/datagrid/src/lib/cells/market-name-cell.tsx +++ b/libs/datagrid/src/lib/cells/market-name-cell.tsx @@ -1,4 +1,4 @@ -import type { MouseEvent } from 'react'; +import type { MouseEvent, ReactNode } from 'react'; import { useCallback } from 'react'; import get from 'lodash/get'; @@ -7,6 +7,7 @@ interface MarketNameCellProps { data?: { id?: string; marketId?: string; market?: { id: string } }; idPath?: string; onMarketClick?: (marketId: string, metaKey?: boolean) => void; + defaultValue?: ReactNode; } export const MarketNameCell = ({ @@ -14,6 +15,7 @@ export const MarketNameCell = ({ data, idPath, onMarketClick, + defaultValue = null, }: MarketNameCellProps) => { const id = data ? get(data, idPath ?? 'id', 'all') : ''; const handleOnClick = useCallback( @@ -26,10 +28,14 @@ export const MarketNameCell = ({ }, [id, onMarketClick] ); - if (!data) return null; - return ( + // eslint-disable-next-line react/jsx-no-useless-fragment + if (!value || !data) return <>{defaultValue}; + return onMarketClick ? ( + ) : ( + // eslint-disable-next-line react/jsx-no-useless-fragment + <>{value} ); }; diff --git a/libs/deal-ticket/src/components/deal-ticket-validation/margin-warning.tsx b/libs/deal-ticket/src/components/deal-ticket-validation/margin-warning.tsx index 86efe1982..329e47afe 100644 --- a/libs/deal-ticket/src/components/deal-ticket-validation/margin-warning.tsx +++ b/libs/deal-ticket/src/components/deal-ticket-validation/margin-warning.tsx @@ -18,7 +18,7 @@ export const MarginWarning = ({ margin, balance, asset }: Props) => { return ( {t( diff --git a/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.spec.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.spec.tsx similarity index 93% rename from libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.spec.tsx rename to libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.spec.tsx index f5a899fbf..871b12c3e 100644 --- a/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.spec.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.spec.tsx @@ -1,6 +1,6 @@ -import { formatRange, formatValue } from './use-fee-deal-ticket-details'; +import { formatRange, formatValue } from './deal-ticket-fee-details'; -describe('useFeeDealTicketDetails', () => { +describe('formatRange, formatValue', () => { it.each([ { v: 123000, d: 5, o: '1.23' }, { v: 123000, d: 3, o: '123.00' }, diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx index 4309d0069..ae81837bd 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx @@ -1,14 +1,67 @@ import { Tooltip } from '@vegaprotocol/ui-toolkit'; import classnames from 'classnames'; import type { ReactNode } from 'react'; -import { getFeeDetailsValues } from '../../hooks/use-fee-deal-ticket-details'; -import type { FeeDetails } from '../../hooks/use-fee-deal-ticket-details'; +import { t } from '@vegaprotocol/i18n'; +import { FeesBreakdown } from '@vegaprotocol/markets'; +import { useVegaWallet } from '@vegaprotocol/wallet'; -export interface DealTicketFeeDetailProps { +import type { Market } from '@vegaprotocol/markets'; +import type { EstimatePositionQuery } from '@vegaprotocol/positions'; +import type { EstimateFeesQuery } from '../../hooks/__generated__/EstimateOrder'; + +import { + addDecimalsFormatNumber, + isNumeric, + addDecimalsFormatNumberQuantum, +} from '@vegaprotocol/utils'; +import { marketMarginDataProvider } from '@vegaprotocol/accounts'; +import { useDataProvider } from '@vegaprotocol/data-provider'; + +import { + NOTIONAL_SIZE_TOOLTIP_TEXT, + MARGIN_DIFF_TOOLTIP_TEXT, + DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT, + TOTAL_MARGIN_AVAILABLE, + LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT, + EST_TOTAL_MARGIN_TOOLTIP_TEXT, + MARGIN_ACCOUNT_TOOLTIP_TEXT, +} from '../../constants'; + +const emptyValue = '-'; + +export const formatValue = ( + value: string | number | null | undefined, + formatDecimals: number, + quantum?: string +): string => { + if (!isNumeric(value)) return emptyValue; + if (!quantum) return addDecimalsFormatNumber(value, formatDecimals); + return addDecimalsFormatNumberQuantum(value, formatDecimals, quantum); +}; + +export const formatRange = ( + min: string | number | null | undefined, + max: string | number | null | undefined, + formatDecimals: number, + quantum?: string +) => { + const minFormatted = formatValue(min, formatDecimals, quantum); + const maxFormatted = formatValue(max, formatDecimals, quantum); + if (minFormatted !== maxFormatted) { + return `${minFormatted} - ${maxFormatted}`; + } + if (minFormatted !== emptyValue) { + return minFormatted; + } + return maxFormatted; +}; +export interface DealTicketFeeDetailPros { label: string; - value?: string | number | null; - labelDescription?: string | ReactNode; - symbol?: string; + value?: string | null | undefined; + symbol: string; + indent?: boolean | undefined; + labelDescription?: ReactNode; + formattedValue?: string; } export const DealTicketFeeDetail = ({ @@ -16,52 +69,294 @@ export const DealTicketFeeDetail = ({ value, labelDescription, symbol, -}: DealTicketFeeDetailProps) => ( -
+ indent, + formattedValue, +}: DealTicketFeeDetailPros) => ( +
{label}
-
{`${value ?? '-'} ${ - symbol || '' - }`}
+ +
{`${ + formattedValue ?? '-' + } ${symbol || ''}`}
+
); -export const DealTicketFeeDetails = (props: FeeDetails) => { - const details = getFeeDetailsValues(props); +export interface DealTicketFeeDetailsProps { + generalAccountBalance?: string; + marginAccountBalance?: string; + market: Market; + assetSymbol: string; + notionalSize: string | null; + feeEstimate: EstimateFeesQuery['estimateFees'] | undefined; + positionEstimate: EstimatePositionQuery['estimatePosition']; +} + +export const DealTicketFeeDetails = ({ + marginAccountBalance, + generalAccountBalance, + assetSymbol, + feeEstimate, + market, + notionalSize, + positionEstimate, +}: DealTicketFeeDetailsProps) => { + const { pubKey } = useVegaWallet(); + const { data: currentMargins } = useDataProvider({ + dataProvider: marketMarginDataProvider, + variables: { marketId: market.id, partyId: pubKey || '' }, + skip: !pubKey, + }); + const liquidationEstimate = positionEstimate?.liquidation; + const marginEstimate = positionEstimate?.margin; + const totalBalance = + BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0'); + const assetDecimals = + market.tradableInstrument.instrument.product.settlementAsset.decimals; + const quantum = + market.tradableInstrument.instrument.product.settlementAsset.quantum; + let marginRequiredBestCase: string | undefined = undefined; + let marginRequiredWorstCase: string | undefined = undefined; + if (marginEstimate) { + if (currentMargins) { + marginRequiredBestCase = ( + BigInt(marginEstimate.bestCase.initialLevel) - + BigInt(currentMargins.initialLevel) + ).toString(); + if (marginRequiredBestCase.startsWith('-')) { + marginRequiredBestCase = '0'; + } + marginRequiredWorstCase = ( + BigInt(marginEstimate.worstCase.initialLevel) - + BigInt(currentMargins.initialLevel) + ).toString(); + if (marginRequiredWorstCase.startsWith('-')) { + marginRequiredWorstCase = '0'; + } + } else { + marginRequiredBestCase = marginEstimate.bestCase.initialLevel; + marginRequiredWorstCase = marginEstimate.worstCase.initialLevel; + } + } + + const totalMarginAvailable = ( + currentMargins + ? totalBalance - BigInt(currentMargins.maintenanceLevel) + : totalBalance + ).toString(); + + let deductionFromCollateral = null; + let projectedMargin = null; + if (marginAccountBalance) { + const deductionFromCollateralBestCase = + BigInt(marginEstimate?.bestCase.initialLevel ?? 0) - + BigInt(marginAccountBalance); + + const deductionFromCollateralWorstCase = + BigInt(marginEstimate?.worstCase.initialLevel ?? 0) - + BigInt(marginAccountBalance); + + deductionFromCollateral = ( + 0 + ? deductionFromCollateralBestCase.toString() + : '0', + deductionFromCollateralWorstCase > 0 + ? deductionFromCollateralWorstCase.toString() + : '0', + assetDecimals + )} + formattedValue={formatRange( + deductionFromCollateralBestCase > 0 + ? deductionFromCollateralBestCase.toString() + : '0', + deductionFromCollateralWorstCase > 0 + ? deductionFromCollateralWorstCase.toString() + : '0', + assetDecimals, + quantum + )} + symbol={assetSymbol} + labelDescription={DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT(assetSymbol)} + /> + ); + projectedMargin = ( + + ); + } + + let liquidationPriceEstimate = emptyValue; + let liquidationPriceEstimateFormatted; + + if (liquidationEstimate) { + const liquidationEstimateBestCaseIncludingBuyOrders = BigInt( + liquidationEstimate.bestCase.including_buy_orders.replace(/\..*/, '') + ); + const liquidationEstimateBestCaseIncludingSellOrders = BigInt( + liquidationEstimate.bestCase.including_sell_orders.replace(/\..*/, '') + ); + const liquidationEstimateBestCase = + liquidationEstimateBestCaseIncludingBuyOrders > + liquidationEstimateBestCaseIncludingSellOrders + ? liquidationEstimateBestCaseIncludingBuyOrders + : liquidationEstimateBestCaseIncludingSellOrders; + + const liquidationEstimateWorstCaseIncludingBuyOrders = BigInt( + liquidationEstimate.worstCase.including_buy_orders.replace(/\..*/, '') + ); + const liquidationEstimateWorstCaseIncludingSellOrders = BigInt( + liquidationEstimate.worstCase.including_sell_orders.replace(/\..*/, '') + ); + const liquidationEstimateWorstCase = + liquidationEstimateWorstCaseIncludingBuyOrders > + liquidationEstimateWorstCaseIncludingSellOrders + ? liquidationEstimateWorstCaseIncludingBuyOrders + : liquidationEstimateWorstCaseIncludingSellOrders; + liquidationPriceEstimate = formatRange( + (liquidationEstimateBestCase < liquidationEstimateWorstCase + ? liquidationEstimateBestCase + : liquidationEstimateWorstCase + ).toString(), + (liquidationEstimateBestCase > liquidationEstimateWorstCase + ? liquidationEstimateBestCase + : liquidationEstimateWorstCase + ).toString(), + assetDecimals + ); + liquidationPriceEstimateFormatted = formatRange( + (liquidationEstimateBestCase < liquidationEstimateWorstCase + ? liquidationEstimateBestCase + : liquidationEstimateWorstCase + ).toString(), + (liquidationEstimateBestCase > liquidationEstimateWorstCase + ? liquidationEstimateBestCase + : liquidationEstimateWorstCase + ).toString(), + assetDecimals, + quantum + ); + } + return (
- {details.map( - ({ - label, - value, - labelDescription, - symbol, - indent, - formattedValue, - }) => ( -
-
- -
{label}
-
-
- -
{`${ - formattedValue ?? '-' - } ${symbol || ''}`}
-
-
- ) - )} + + + + {t( + `An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.` + )} + + + + } + symbol={assetSymbol} + /> + + + {deductionFromCollateral} + {projectedMargin} + +
); }; diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-limit-amount.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-limit-amount.tsx index dcbd52274..0db3f545a 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-limit-amount.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-limit-amount.tsx @@ -25,7 +25,7 @@ export const DealTicketLimitAmount = ({ const renderError = () => { if (sizeError) { return ( - + {sizeError} ); @@ -33,7 +33,7 @@ export const DealTicketLimitAmount = ({ if (priceError) { return ( - + {priceError} ); diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-market-amount.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-market-amount.tsx index c55c7fbe8..6933ff15f 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-market-amount.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-market-amount.tsx @@ -90,7 +90,7 @@ export const DealTicketMarketAmount = ({ {sizeError && ( {sizeError} diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx index d568afb57..9d72a017b 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -31,7 +31,7 @@ import { } from '@vegaprotocol/positions'; import { toBigNum, removeDecimal } from '@vegaprotocol/utils'; import { activeOrdersProvider } from '@vegaprotocol/orders'; -import { useEstimateFees } from '../../hooks/use-fee-deal-ticket-details'; +import { useEstimateFees } from '../../hooks/use-estimate-fees'; import { getDerivedPrice } from '../../utils/get-price'; import type { OrderInfo } from '@vegaprotocol/types'; @@ -55,8 +55,6 @@ import { OrderTimeInForce, OrderType } from '@vegaprotocol/types'; import { useOrderForm } from '../../hooks/use-order-form'; import { useDataProvider } from '@vegaprotocol/data-provider'; -import { marketMarginDataProvider } from '@vegaprotocol/positions'; - export interface DealTicketProps { market: Market; marketData: MarketData; @@ -176,12 +174,6 @@ export const DealTicket = ({ const assetSymbol = market.tradableInstrument.instrument.product.settlementAsset.symbol; - const { data: currentMargins } = useDataProvider({ - dataProvider: marketMarginDataProvider, - variables: { marketId: market.id, partyId: pubKey || '' }, - skip: !pubKey, - }); - useEffect(() => { if (!pubKey) { setError('summary', { @@ -494,8 +486,6 @@ export const DealTicket = ({ generalAccountBalance={generalAccountBalance} positionEstimate={positionEstimate?.estimatePosition} market={market} - currentInitialMargin={currentMargins?.initialLevel} - currentMaintenanceMargin={currentMargins?.maintenanceLevel} /> @@ -536,7 +526,7 @@ const SummaryMessage = memo( if (isReadOnly) { return (
- + { 'You need to connect your own wallet to start trading on this market' } @@ -585,7 +575,7 @@ const SummaryMessage = memo( if (errorMessage) { return (
- + {errorMessage}
@@ -613,7 +603,7 @@ const SummaryMessage = memo(
{errorMessage && ( - + {errorMessage} )} diff --git a/libs/deal-ticket/src/components/deal-ticket/time-in-force-selector.tsx b/libs/deal-ticket/src/components/deal-ticket/time-in-force-selector.tsx index 1c75a30aa..a0cfc94d9 100644 --- a/libs/deal-ticket/src/components/deal-ticket/time-in-force-selector.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/time-in-force-selector.tsx @@ -108,7 +108,7 @@ export const TimeInForceSelector = ({ ))} {errorMessage && ( - + {renderError(errorMessage)} )} diff --git a/libs/deal-ticket/src/components/deal-ticket/type-selector.tsx b/libs/deal-ticket/src/components/deal-ticket/type-selector.tsx index 9b03f459c..037835e15 100644 --- a/libs/deal-ticket/src/components/deal-ticket/type-selector.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/type-selector.tsx @@ -83,7 +83,7 @@ export const TypeSelector = ({ onChange={(e) => onSelect(e.target.value as Schema.OrderType)} /> {errorMessage && ( - + {renderError(errorMessage as MarketModeValidationType)} )} diff --git a/libs/deal-ticket/src/hooks/index.ts b/libs/deal-ticket/src/hooks/index.ts index 577896228..776b08278 100644 --- a/libs/deal-ticket/src/hooks/index.ts +++ b/libs/deal-ticket/src/hooks/index.ts @@ -1,2 +1,2 @@ export * from './__generated__/EstimateOrder'; -export * from './use-fee-deal-ticket-details'; +export * from './use-estimate-fees'; diff --git a/libs/deal-ticket/src/hooks/use-estimate-fees.tsx b/libs/deal-ticket/src/hooks/use-estimate-fees.tsx new file mode 100644 index 000000000..9ad2b0b5a --- /dev/null +++ b/libs/deal-ticket/src/hooks/use-estimate-fees.tsx @@ -0,0 +1,25 @@ +import { useVegaWallet } from '@vegaprotocol/wallet'; +import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; + +import { useEstimateFeesQuery } from './__generated__/EstimateOrder'; + +export const useEstimateFees = ( + order?: OrderSubmissionBody['orderSubmission'] +) => { + const { pubKey } = useVegaWallet(); + + const { data } = useEstimateFeesQuery({ + variables: order && { + marketId: order.marketId, + partyId: pubKey || '', + price: order.price, + size: order.size, + side: order.side, + timeInForce: order.timeInForce, + type: order.type, + }, + fetchPolicy: 'no-cache', + skip: !pubKey || !order?.size || !order?.price, + }); + return data?.estimateFees; +}; diff --git a/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.tsx b/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.tsx deleted file mode 100644 index 735c2b54f..000000000 --- a/libs/deal-ticket/src/hooks/use-fee-deal-ticket-details.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import { FeesBreakdown } from '@vegaprotocol/markets'; -import { - addDecimalsFormatNumber, - addDecimalsFormatNumberQuantum, - isNumeric, -} from '@vegaprotocol/utils'; -import { t } from '@vegaprotocol/i18n'; -import { useVegaWallet } from '@vegaprotocol/wallet'; -import type { Market } from '@vegaprotocol/markets'; -import type { EstimatePositionQuery } from '@vegaprotocol/positions'; -import type { OrderSubmissionBody } from '@vegaprotocol/wallet'; -import { - EST_TOTAL_MARGIN_TOOLTIP_TEXT, - NOTIONAL_SIZE_TOOLTIP_TEXT, - MARGIN_ACCOUNT_TOOLTIP_TEXT, - MARGIN_DIFF_TOOLTIP_TEXT, - DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT, - TOTAL_MARGIN_AVAILABLE, - LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT, -} from '../constants'; - -import { useEstimateFeesQuery } from './__generated__/EstimateOrder'; -import type { EstimateFeesQuery } from './__generated__/EstimateOrder'; - -export const useEstimateFees = ( - order?: OrderSubmissionBody['orderSubmission'] -) => { - const { pubKey } = useVegaWallet(); - - const { data } = useEstimateFeesQuery({ - variables: order && { - marketId: order.marketId, - partyId: pubKey || '', - price: order.price, - size: order.size, - side: order.side, - timeInForce: order.timeInForce, - type: order.type, - }, - skip: !pubKey || !order?.size || !order?.price, - fetchPolicy: 'no-cache', - }); - return data?.estimateFees; -}; - -export interface FeeDetails { - generalAccountBalance?: string; - marginAccountBalance?: string; - market: Market; - assetSymbol: string; - notionalSize: string | null; - feeEstimate: EstimateFeesQuery['estimateFees'] | undefined; - currentInitialMargin?: string; - currentMaintenanceMargin?: string; - positionEstimate: EstimatePositionQuery['estimatePosition']; -} - -const emptyValue = '-'; - -export const formatValue = ( - value: string | number | null | undefined, - formatDecimals: number, - quantum?: string -): string => { - if (!isNumeric(value)) return emptyValue; - if (!quantum) return addDecimalsFormatNumber(value, formatDecimals); - return addDecimalsFormatNumberQuantum(value, formatDecimals, quantum); -}; - -export const formatRange = ( - min: string | number | null | undefined, - max: string | number | null | undefined, - formatDecimals: number, - quantum?: string -) => { - const minFormatted = formatValue(min, formatDecimals, quantum); - const maxFormatted = formatValue(max, formatDecimals, quantum); - if (minFormatted !== maxFormatted) { - return `${minFormatted} - ${maxFormatted}`; - } - if (minFormatted !== emptyValue) { - return minFormatted; - } - return maxFormatted; -}; - -export const getFeeDetailsValues = ({ - marginAccountBalance, - generalAccountBalance, - assetSymbol, - feeEstimate, - market, - notionalSize, - currentInitialMargin, - currentMaintenanceMargin, - positionEstimate, -}: FeeDetails) => { - const liquidationEstimate = positionEstimate?.liquidation; - const marginEstimate = positionEstimate?.margin; - const totalBalance = - BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0'); - const assetDecimals = - market.tradableInstrument.instrument.product.settlementAsset.decimals; - const quantum = - market.tradableInstrument.instrument.product.settlementAsset.quantum; - const details: { - label: string; - value?: string | null; - formattedValue?: string | null; - symbol: string; - indent?: boolean; - labelDescription?: React.ReactNode; - }[] = [ - { - label: t('Notional'), - value: formatValue(notionalSize, assetDecimals), - formattedValue: formatValue(notionalSize, assetDecimals, quantum), - symbol: assetSymbol, - labelDescription: NOTIONAL_SIZE_TOOLTIP_TEXT(assetSymbol), - }, - { - label: t('Fees'), - value: - feeEstimate?.totalFeeAmount && - `~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`, - formattedValue: - feeEstimate?.totalFeeAmount && - `~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`, - labelDescription: ( - <> - - {t( - `An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.` - )} - - - - ), - symbol: assetSymbol, - }, - ]; - let marginRequiredBestCase: string | undefined = undefined; - let marginRequiredWorstCase: string | undefined = undefined; - if (marginEstimate) { - if (currentInitialMargin) { - marginRequiredBestCase = ( - BigInt(marginEstimate.bestCase.initialLevel) - - BigInt(currentInitialMargin) - ).toString(); - if (marginRequiredBestCase.startsWith('-')) { - marginRequiredBestCase = '0'; - } - marginRequiredWorstCase = ( - BigInt(marginEstimate.worstCase.initialLevel) - - BigInt(currentInitialMargin) - ).toString(); - if (marginRequiredWorstCase.startsWith('-')) { - marginRequiredWorstCase = '0'; - } - } else { - marginRequiredBestCase = marginEstimate.bestCase.initialLevel; - marginRequiredWorstCase = marginEstimate.worstCase.initialLevel; - } - } - details.push({ - label: t('Margin required'), - formattedValue: formatRange( - marginRequiredBestCase, - marginRequiredWorstCase, - assetDecimals, - quantum - ), - value: formatRange( - marginRequiredBestCase, - marginRequiredWorstCase, - assetDecimals - ), - symbol: assetSymbol, - labelDescription: MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol), - }); - - const totalMarginAvailable = ( - currentMaintenanceMargin - ? totalBalance - BigInt(currentMaintenanceMargin) - : totalBalance - ).toString(); - - details.push({ - indent: true, - label: t('Total margin available'), - formattedValue: formatValue(totalMarginAvailable, assetDecimals, quantum), - value: formatValue(totalMarginAvailable, assetDecimals), - symbol: assetSymbol, - labelDescription: TOTAL_MARGIN_AVAILABLE( - formatValue(generalAccountBalance, assetDecimals, quantum), - formatValue(marginAccountBalance, assetDecimals, quantum), - formatValue(currentMaintenanceMargin, assetDecimals, quantum), - assetSymbol - ), - }); - - if (marginAccountBalance) { - const deductionFromCollateralBestCase = - BigInt(marginEstimate?.bestCase.initialLevel ?? 0) - - BigInt(marginAccountBalance); - - const deductionFromCollateralWorstCase = - BigInt(marginEstimate?.worstCase.initialLevel ?? 0) - - BigInt(marginAccountBalance); - - details.push({ - indent: true, - label: t('Deduction from collateral'), - value: formatRange( - deductionFromCollateralBestCase > 0 - ? deductionFromCollateralBestCase.toString() - : '0', - deductionFromCollateralWorstCase > 0 - ? deductionFromCollateralWorstCase.toString() - : '0', - assetDecimals - ), - formattedValue: formatRange( - deductionFromCollateralBestCase > 0 - ? deductionFromCollateralBestCase.toString() - : '0', - deductionFromCollateralWorstCase > 0 - ? deductionFromCollateralWorstCase.toString() - : '0', - assetDecimals, - quantum - ), - symbol: assetSymbol, - labelDescription: DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT(assetSymbol), - }); - - details.push({ - label: t('Projected margin'), - value: formatRange( - marginEstimate?.bestCase.initialLevel, - marginEstimate?.worstCase.initialLevel, - assetDecimals - ), - formattedValue: formatRange( - marginEstimate?.bestCase.initialLevel, - marginEstimate?.worstCase.initialLevel, - assetDecimals, - quantum - ), - symbol: assetSymbol, - labelDescription: EST_TOTAL_MARGIN_TOOLTIP_TEXT, - }); - } - details.push({ - label: t('Current margin allocation'), - value: formatValue(marginAccountBalance, assetDecimals), - symbol: assetSymbol, - labelDescription: MARGIN_ACCOUNT_TOOLTIP_TEXT, - formattedValue: formatValue(marginAccountBalance, assetDecimals, quantum), - }); - - let liquidationPriceEstimate = emptyValue; - let liquidationPriceEstimateFormatted; - - if (liquidationEstimate) { - const liquidationEstimateBestCaseIncludingBuyOrders = BigInt( - liquidationEstimate.bestCase.including_buy_orders.replace(/\..*/, '') - ); - const liquidationEstimateBestCaseIncludingSellOrders = BigInt( - liquidationEstimate.bestCase.including_sell_orders.replace(/\..*/, '') - ); - const liquidationEstimateBestCase = - liquidationEstimateBestCaseIncludingBuyOrders > - liquidationEstimateBestCaseIncludingSellOrders - ? liquidationEstimateBestCaseIncludingBuyOrders - : liquidationEstimateBestCaseIncludingSellOrders; - - const liquidationEstimateWorstCaseIncludingBuyOrders = BigInt( - liquidationEstimate.worstCase.including_buy_orders.replace(/\..*/, '') - ); - const liquidationEstimateWorstCaseIncludingSellOrders = BigInt( - liquidationEstimate.worstCase.including_sell_orders.replace(/\..*/, '') - ); - const liquidationEstimateWorstCase = - liquidationEstimateWorstCaseIncludingBuyOrders > - liquidationEstimateWorstCaseIncludingSellOrders - ? liquidationEstimateWorstCaseIncludingBuyOrders - : liquidationEstimateWorstCaseIncludingSellOrders; - liquidationPriceEstimate = formatRange( - (liquidationEstimateBestCase < liquidationEstimateWorstCase - ? liquidationEstimateBestCase - : liquidationEstimateWorstCase - ).toString(), - (liquidationEstimateBestCase > liquidationEstimateWorstCase - ? liquidationEstimateBestCase - : liquidationEstimateWorstCase - ).toString(), - assetDecimals - ); - liquidationPriceEstimateFormatted = formatRange( - (liquidationEstimateBestCase < liquidationEstimateWorstCase - ? liquidationEstimateBestCase - : liquidationEstimateWorstCase - ).toString(), - (liquidationEstimateBestCase > liquidationEstimateWorstCase - ? liquidationEstimateBestCase - : liquidationEstimateWorstCase - ).toString(), - assetDecimals, - quantum - ); - } - - details.push({ - label: t('Liquidation price estimate'), - value: liquidationPriceEstimate, - formattedValue: liquidationPriceEstimateFormatted, - symbol: assetSymbol, - labelDescription: LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT, - }); - return details; -}; diff --git a/libs/positions/src/index.ts b/libs/positions/src/index.ts index e6b3b3a92..6139213d0 100644 --- a/libs/positions/src/index.ts +++ b/libs/positions/src/index.ts @@ -1,7 +1,6 @@ export * from './lib/__generated__/Positions'; export * from './lib/positions-container'; export * from './lib/positions-data-providers'; -export * from './lib/margin-data-provider'; export * from './lib/positions-table'; export * from './lib/use-market-margin'; export * from './lib/use-open-volume'; diff --git a/libs/positions/src/lib/Positions.graphql b/libs/positions/src/lib/Positions.graphql index cbf7be70a..657ca9493 100644 --- a/libs/positions/src/lib/Positions.graphql +++ b/libs/positions/src/lib/Positions.graphql @@ -38,45 +38,6 @@ subscription PositionsSubscription($partyId: ID!) { } } -fragment MarginFields on MarginLevels { - maintenanceLevel - searchLevel - initialLevel - collateralReleaseLevel - asset { - id - } - market { - id - } -} - -query Margins($partyId: ID!) { - party(id: $partyId) { - id - marginsConnection { - edges { - node { - ...MarginFields - } - } - } - } -} - -subscription MarginsSubscription($partyId: ID!) { - margins(partyId: $partyId) { - marketId - asset - partyId - maintenanceLevel - searchLevel - initialLevel - collateralReleaseLevel - timestamp - } -} - query EstimatePosition( $marketId: ID! $openVolume: String! diff --git a/libs/positions/src/lib/__generated__/Positions.ts b/libs/positions/src/lib/__generated__/Positions.ts index 804ab64fe..3cd120a3b 100644 --- a/libs/positions/src/lib/__generated__/Positions.ts +++ b/libs/positions/src/lib/__generated__/Positions.ts @@ -19,22 +19,6 @@ export type PositionsSubscriptionSubscriptionVariables = Types.Exact<{ export type PositionsSubscriptionSubscription = { __typename?: 'Subscription', positions: Array<{ __typename?: 'PositionUpdate', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, marketId: string, lossSocializationAmount: string, positionStatus: Types.PositionStatus, partyId: string }> }; -export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } }; - -export type MarginsQueryVariables = Types.Exact<{ - partyId: Types.Scalars['ID']; -}>; - - -export type MarginsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, marginsConnection?: { __typename?: 'MarginConnection', edges?: Array<{ __typename?: 'MarginEdge', node: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } } }> | null } | null } | null }; - -export type MarginsSubscriptionSubscriptionVariables = Types.Exact<{ - partyId: Types.Scalars['ID']; -}>; - - -export type MarginsSubscriptionSubscription = { __typename?: 'Subscription', margins: { __typename?: 'MarginLevelsUpdate', marketId: string, asset: string, partyId: string, maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, timestamp: any } }; - export type EstimatePositionQueryVariables = Types.Exact<{ marketId: Types.Scalars['ID']; openVolume: Types.Scalars['String']; @@ -62,20 +46,6 @@ export const PositionFieldsFragmentDoc = gql` } } `; -export const MarginFieldsFragmentDoc = gql` - fragment MarginFields on MarginLevels { - maintenanceLevel - searchLevel - initialLevel - collateralReleaseLevel - asset { - id - } - market { - id - } -} - `; export const PositionsDocument = gql` query Positions($partyIds: [ID!]!) { positions(filter: {partyIds: $partyIds}) { @@ -153,85 +123,6 @@ export function usePositionsSubscriptionSubscription(baseOptions: Apollo.Subscri } export type PositionsSubscriptionSubscriptionHookResult = ReturnType; export type PositionsSubscriptionSubscriptionResult = Apollo.SubscriptionResult; -export const MarginsDocument = gql` - query Margins($partyId: ID!) { - party(id: $partyId) { - id - marginsConnection { - edges { - node { - ...MarginFields - } - } - } - } -} - ${MarginFieldsFragmentDoc}`; - -/** - * __useMarginsQuery__ - * - * To run a query within a React component, call `useMarginsQuery` and pass it any options that fit your needs. - * When your component renders, `useMarginsQuery` 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 } = useMarginsQuery({ - * variables: { - * partyId: // value for 'partyId' - * }, - * }); - */ -export function useMarginsQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(MarginsDocument, options); - } -export function useMarginsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(MarginsDocument, options); - } -export type MarginsQueryHookResult = ReturnType; -export type MarginsLazyQueryHookResult = ReturnType; -export type MarginsQueryResult = Apollo.QueryResult; -export const MarginsSubscriptionDocument = gql` - subscription MarginsSubscription($partyId: ID!) { - margins(partyId: $partyId) { - marketId - asset - partyId - maintenanceLevel - searchLevel - initialLevel - collateralReleaseLevel - timestamp - } -} - `; - -/** - * __useMarginsSubscriptionSubscription__ - * - * To run a query within a React component, call `useMarginsSubscriptionSubscription` and pass it any options that fit your needs. - * When your component renders, `useMarginsSubscriptionSubscription` 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 subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useMarginsSubscriptionSubscription({ - * variables: { - * partyId: // value for 'partyId' - * }, - * }); - */ -export function useMarginsSubscriptionSubscription(baseOptions: Apollo.SubscriptionHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useSubscription(MarginsSubscriptionDocument, options); - } -export type MarginsSubscriptionSubscriptionHookResult = ReturnType; -export type MarginsSubscriptionSubscriptionResult = Apollo.SubscriptionResult; export const EstimatePositionDocument = gql` query EstimatePosition($marketId: ID!, $openVolume: String!, $orders: [OrderInfo!], $collateralAvailable: String) { estimatePosition( diff --git a/libs/positions/src/lib/positions.mock.ts b/libs/positions/src/lib/positions.mock.ts index 458747ff1..11826f879 100644 --- a/libs/positions/src/lib/positions.mock.ts +++ b/libs/positions/src/lib/positions.mock.ts @@ -4,9 +4,12 @@ import type { PartialDeep } from 'type-fest'; import type { PositionsQuery, PositionFieldsFragment, +} from './__generated__/Positions'; + +import type { MarginsQuery, MarginFieldsFragment, -} from './__generated__/Positions'; +} from '@vegaprotocol/accounts'; export const positionsQuery = ( override?: PartialDeep diff --git a/libs/positions/src/lib/use-market-margin.tsx b/libs/positions/src/lib/use-market-margin.tsx index 89ff18634..1eb4b6bdf 100644 --- a/libs/positions/src/lib/use-market-margin.tsx +++ b/libs/positions/src/lib/use-market-margin.tsx @@ -1,8 +1,8 @@ import { useCallback, useState } from 'react'; import { useVegaWallet } from '@vegaprotocol/wallet'; import { useDataProvider } from '@vegaprotocol/data-provider'; -import { marginsDataProvider } from './margin-data-provider'; -import type { MarginFieldsFragment } from './__generated__/Positions'; +import { marginsDataProvider } from '@vegaprotocol/accounts'; +import type { MarginFieldsFragment } from '@vegaprotocol/accounts'; export const useMarketMargin = (marketId: string) => { const { pubKey } = useVegaWallet();