From 6de90c6b1f51f049f46043adc1c7063e1be1c94e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Fri, 2 Sep 2022 22:53:00 +0200 Subject: [PATCH] feat(#840): update positions tab (#1101) * feat(#473): add positions metrics data provider * feat(#473) add positions stats * feat(#473) add positions stats * feat(#473): add positions stats * feat(#473): add positions stats * feat(#473): position metrics, test and refactoring * feat(#473): add unit tests to positions table * feat(#473): fix spelling, order positions by updated at desc * feat(#473): protect from division by 0 * feat(#473): fix trading positions e2e tests * feat(#473): fix e2e data mocks * feat(#473): post code review clean up * feat(#993): dependencies handling in data provider * feat(#993): fix e2e tests data mocks * feat(#993): remove position metrics mocks, add market data market id * feat(#993): add missing mocks, fix combine function * feat(#993): set loading initially to true, add unit tests * feat(#993): cleanup, add comments * feat(#993): remove undefined from client type * feat(#993): cosmetic changes * feat(#840): update positions tab * feat:(#993): pass informaton about update callback cause * feat(#840): update positions tab * feat(#840): update positions tab * feat(#840): update positions tab * chore: skip handles 5000 markets e2e test * feat(#840): update positions tab * feat(#840): rename assetDecimals to decimals * feat(#840): close position * feat(#993): notify about update * feat(#840): add use close position hook * feat(#840): do not show 0 volume positions, make liquidation price minimum 0 * feat(#840): post code review fixes and improvments * feat: fix fill-table spec --- .../deal-ticket/deal-ticket-steps.tsx | 33 +- libs/accounts/src/lib/accounts-manager.tsx | 2 +- libs/accounts/src/lib/asset-balance.tsx | 42 ++ libs/accounts/src/lib/index.ts | 1 + .../src/components/deal-ticket-manager.tsx | 2 +- .../src/components/deal-ticket.tsx | 17 +- libs/fills/src/lib/fills-table.spec.tsx | 142 +++-- libs/fills/src/lib/fills-table.tsx | 7 +- .../order-feedback/order-feedback.tsx | 11 +- .../lib/components/order-list/order-list.tsx | 12 +- .../lib/order-hooks/use-order-submit.spec.tsx | 23 +- .../src/lib/order-hooks/use-order-submit.ts | 103 ---- .../src/lib/order-hooks/use-order-submit.tsx | 165 ++++++ .../orders/src/lib/utils/get-default-order.ts | 8 +- libs/positions/src/index.ts | 6 +- .../src/lib/positions-data-providers.spec.ts | 4 +- .../src/lib/positions-data-providers.ts | 48 +- libs/positions/src/lib/positions-manager.tsx | 70 ++- .../src/lib/positions-table.spec.tsx | 2 +- .../src/lib/positions-table.stories.tsx | 4 +- libs/positions/src/lib/positions-table.tsx | 542 ++++++++++-------- libs/positions/src/lib/positions.tsx | 108 ++++ libs/positions/src/lib/use-close-position.ts | 64 +++ libs/positions/src/lib/use-position-event.ts | 14 + .../src/lib/generic-data-provider.ts | 16 +- .../src/lib/grid/cell-class-rules.ts | 23 + libs/react-helpers/src/lib/grid/index.tsx | 1 + .../price-change/price-change-cell.tsx | 11 +- 28 files changed, 946 insertions(+), 535 deletions(-) create mode 100644 libs/accounts/src/lib/asset-balance.tsx delete mode 100644 libs/orders/src/lib/order-hooks/use-order-submit.ts create mode 100644 libs/orders/src/lib/order-hooks/use-order-submit.tsx create mode 100644 libs/positions/src/lib/positions.tsx create mode 100644 libs/positions/src/lib/use-close-position.ts create mode 100644 libs/positions/src/lib/use-position-event.ts create mode 100644 libs/react-helpers/src/lib/grid/cell-class-rules.ts diff --git a/apps/console-lite/src/app/components/deal-ticket/deal-ticket-steps.tsx b/apps/console-lite/src/app/components/deal-ticket/deal-ticket-steps.tsx index 073025c2d..e8b584fd2 100644 --- a/apps/console-lite/src/app/components/deal-ticket/deal-ticket-steps.tsx +++ b/apps/console-lite/src/app/components/deal-ticket/deal-ticket-steps.tsx @@ -5,19 +5,22 @@ import { Stepper } from '../stepper'; import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket'; import { InputError } from '@vegaprotocol/ui-toolkit'; import { BigNumber } from 'bignumber.js'; -import { - getOrderDialogTitle, - getOrderDialogIntent, - getOrderDialogIcon, - MarketSelector, -} from '@vegaprotocol/deal-ticket'; +import { MarketSelector } from '@vegaprotocol/deal-ticket'; import type { Order } from '@vegaprotocol/orders'; import { useVegaWallet, VegaTxStatus } from '@vegaprotocol/wallet'; -import { t, addDecimal, toDecimal } from '@vegaprotocol/react-helpers'; +import { + t, + addDecimal, + toDecimal, + removeDecimal, +} from '@vegaprotocol/react-helpers'; import { getDefaultOrder, useOrderValidation, useOrderSubmit, + getOrderDialogTitle, + getOrderDialogIntent, + getOrderDialogIcon, OrderFeedback, validateSize, } from '@vegaprotocol/orders'; @@ -108,7 +111,7 @@ export const DealTicketSteps = ({ }); const { submit, transaction, finalizedOrder, TransactionDialog } = - useOrderSubmit(market); + useOrderSubmit(); const onSizeChange = (value: number[]) => { const newVal = new BigNumber(value[0]) @@ -151,10 +154,20 @@ export const DealTicketSteps = ({ const onSubmit = React.useCallback( (order: Order) => { if (transactionStatus !== 'pending') { - submit(order); + submit({ + ...order, + price: + order.price && removeDecimal(order.price, market.decimalPlaces), + size: removeDecimal(order.size, market.positionDecimalPlaces), + }); } }, - [transactionStatus, submit] + [ + transactionStatus, + submit, + market.decimalPlaces, + market.positionDecimalPlaces, + ] ); const steps = [ diff --git a/libs/accounts/src/lib/accounts-manager.tsx b/libs/accounts/src/lib/accounts-manager.tsx index 9d5717f44..2e06b2daa 100644 --- a/libs/accounts/src/lib/accounts-manager.tsx +++ b/libs/accounts/src/lib/accounts-manager.tsx @@ -25,7 +25,7 @@ export const AccountsManager = ({ partyId }: AccountsManagerProps) => { ({ delta }: { delta: AccountSubscribe_accounts }) => { const update: Accounts_party_accounts[] = []; const add: Accounts_party_accounts[] = []; - if (!gridRef.current) { + if (!gridRef.current?.api) { return false; } const rowNode = gridRef.current.api.getRowNode(getId(delta)); diff --git a/libs/accounts/src/lib/asset-balance.tsx b/libs/accounts/src/lib/asset-balance.tsx new file mode 100644 index 000000000..9b3c9b53d --- /dev/null +++ b/libs/accounts/src/lib/asset-balance.tsx @@ -0,0 +1,42 @@ +import { useMemo } from 'react'; +import { + addDecimalsFormatNumber, + useDataProvider, +} from '@vegaprotocol/react-helpers'; +import type { AccountSubscribe_accounts } from './__generated__/AccountSubscribe'; +import type { Accounts_party_accounts } from './__generated__/Accounts'; + +import { accountsDataProvider } from './accounts-data-provider'; + +interface AssetBalanceProps { + partyId: string; + assetSymbol: string; +} + +export const AssetBalance = ({ partyId, assetSymbol }: AssetBalanceProps) => { + const variables = useMemo(() => ({ partyId }), [partyId]); + const { data } = useDataProvider< + Accounts_party_accounts[], + AccountSubscribe_accounts + >({ + dataProvider: accountsDataProvider, + variables, + }); + if (data && data.length) { + const totalBalance = data.reduce((a, c) => { + if (c.asset.symbol === assetSymbol) { + return a + BigInt(c.balance); + } + return a; + }, BigInt(0)); + return ( + + {addDecimalsFormatNumber( + totalBalance.toString(), + data[0].asset.decimals + )} + + ); + } + return null; +}; diff --git a/libs/accounts/src/lib/index.ts b/libs/accounts/src/lib/index.ts index 1abadaae9..58ebfe888 100644 --- a/libs/accounts/src/lib/index.ts +++ b/libs/accounts/src/lib/index.ts @@ -3,3 +3,4 @@ export * from './accounts-container'; export * from './accounts-data-provider'; export * from './accounts-manager'; export * from './accounts-table'; +export * from './asset-balance'; diff --git a/libs/deal-ticket/src/components/deal-ticket-manager.tsx b/libs/deal-ticket/src/components/deal-ticket-manager.tsx index d944d4a26..ca816a587 100644 --- a/libs/deal-ticket/src/components/deal-ticket-manager.tsx +++ b/libs/deal-ticket/src/components/deal-ticket-manager.tsx @@ -17,7 +17,7 @@ export const DealTicketManager = ({ children, }: DealTicketManagerProps) => { const { submit, transaction, finalizedOrder, TransactionDialog } = - useOrderSubmit(market); + useOrderSubmit(); return ( <> diff --git a/libs/deal-ticket/src/components/deal-ticket.tsx b/libs/deal-ticket/src/components/deal-ticket.tsx index 3a917cded..e0cb6ab4c 100644 --- a/libs/deal-ticket/src/components/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket.tsx @@ -1,6 +1,10 @@ import { useCallback } from 'react'; import { useForm, Controller } from 'react-hook-form'; -import { t, addDecimalsFormatNumber } from '@vegaprotocol/react-helpers'; +import { + t, + addDecimalsFormatNumber, + removeDecimal, +} from '@vegaprotocol/react-helpers'; import { Button, InputError } from '@vegaprotocol/ui-toolkit'; import { TypeSelector } from './type-selector'; import { SideSelector } from './side-selector'; @@ -51,10 +55,15 @@ export const DealTicket = ({ const onSubmit = useCallback( (order: Order) => { if (!isDisabled) { - submit(order); + submit({ + ...order, + price: + order.price && removeDecimal(order.price, market.decimalPlaces), + size: removeDecimal(order.size, market.positionDecimalPlaces), + }); } }, - [isDisabled, submit] + [isDisabled, submit, market.decimalPlaces, market.positionDecimalPlaces] ); return ( @@ -111,7 +120,7 @@ export const DealTicket = ({ {orderType === OrderType.TYPE_LIMIT && orderTimeInForce === OrderTimeInForce.TIME_IN_FORCE_GTT && ( ( diff --git a/libs/fills/src/lib/fills-table.spec.tsx b/libs/fills/src/lib/fills-table.spec.tsx index 25c054124..8fe0637cc 100644 --- a/libs/fills/src/lib/fills-table.spec.tsx +++ b/libs/fills/src/lib/fills-table.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, waitFor, act } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { getDateTimeFormat } from '@vegaprotocol/react-helpers'; import { Side } from '@vegaprotocol/types'; import type { PartialDeep } from 'type-fest'; @@ -141,82 +141,78 @@ describe('FillsTable', () => { }); it('should format cells correctly for seller fill', async () => { - act(async () => { - const partyId = 'party-id'; - const buyerFill = generateFill({ - ...defaultFill, - seller: { - id: partyId, - }, - aggressor: Side.SIDE_SELL, - sellerFee: { - makerFee: '1', - infrastructureFee: '1', - liquidityFee: '1', - }, - }); - - render(); - await waitForGridToBeInTheDOM(); - await waitForDataToHaveLoaded(); - - const cells = screen.getAllByRole('gridcell'); - const expectedValues = [ - buyerFill.market.name, - '-3.00000', - '1.00 BTC', - '3.00 BTC', - 'Taker', - '0.03 BTC', - getDateTimeFormat().format(new Date(buyerFill.createdAt)), - ]; - cells.forEach((cell, i) => { - expect(cell).toHaveTextContent(expectedValues[i]); - }); - - const amountCell = cells.find((c) => c.getAttribute('col-id') === 'size'); - expect(amountCell).toHaveClass('text-vega-red'); + const partyId = 'party-id'; + const buyerFill = generateFill({ + ...defaultFill, + seller: { + id: partyId, + }, + aggressor: Side.SIDE_SELL, + sellerFee: { + makerFee: '1', + infrastructureFee: '1', + liquidityFee: '1', + }, }); + + render(); + await waitForGridToBeInTheDOM(); + await waitForDataToHaveLoaded(); + + const cells = screen.getAllByRole('gridcell'); + const expectedValues = [ + buyerFill.market.name, + '-3.00000', + '1.00 BTC', + '3.00 BTC', + 'Taker', + '0.03 BTC', + getDateTimeFormat().format(new Date(buyerFill.createdAt)), + ]; + cells.forEach((cell, i) => { + expect(cell).toHaveTextContent(expectedValues[i]); + }); + + const amountCell = cells.find((c) => c.getAttribute('col-id') === 'size'); + expect(amountCell).toHaveClass('text-vega-red-dark'); }); it('should render correct maker or taker role', async () => { - act(async () => { - const partyId = 'party-id'; - const takerFill = generateFill({ - seller: { - id: partyId, - }, - aggressor: Side.SIDE_SELL, - }); - - const { rerender } = render( - - ); - await waitForGridToBeInTheDOM(); - await waitForDataToHaveLoaded(); - - expect( - screen - .getAllByRole('gridcell') - .find((c) => c.getAttribute('col-id') === 'aggressor') - ).toHaveTextContent('Taker'); - - const makerFill = generateFill({ - seller: { - id: partyId, - }, - aggressor: Side.SIDE_BUY, - }); - - rerender(); - await waitForGridToBeInTheDOM(); - await waitForDataToHaveLoaded(); - - expect( - screen - .getAllByRole('gridcell') - .find((c) => c.getAttribute('col-id') === 'aggressor') - ).toHaveTextContent('Maker'); + const partyId = 'party-id'; + const takerFill = generateFill({ + seller: { + id: partyId, + }, + aggressor: Side.SIDE_SELL, }); + + const { rerender } = render( + + ); + await waitForGridToBeInTheDOM(); + await waitForDataToHaveLoaded(); + + expect( + screen + .getAllByRole('gridcell') + .find((c) => c.getAttribute('col-id') === 'aggressor') + ).toHaveTextContent('Taker'); + + const makerFill = generateFill({ + seller: { + id: partyId, + }, + aggressor: Side.SIDE_BUY, + }); + + rerender(); + await waitForGridToBeInTheDOM(); + await waitForDataToHaveLoaded(); + + expect( + screen + .getAllByRole('gridcell') + .find((c) => c.getAttribute('col-id') === 'aggressor') + ).toHaveTextContent('Maker'); }); }); diff --git a/libs/fills/src/lib/fills-table.tsx b/libs/fills/src/lib/fills-table.tsx index 6f939f4a4..966d95c4f 100644 --- a/libs/fills/src/lib/fills-table.tsx +++ b/libs/fills/src/lib/fills-table.tsx @@ -4,6 +4,8 @@ import { addDecimalsFormatNumber, formatNumber, getDateTimeFormat, + positiveClassNames, + negativeClassNames, t, } from '@vegaprotocol/react-helpers'; import { Side } from '@vegaprotocol/types'; @@ -49,9 +51,8 @@ export const FillsTable = forwardRef( field="size" cellClass={({ data }: { data: FillFields }) => { return classNames('text-right', { - 'text-vega-green-dark dark:text-vega-green': - data?.buyer.id === partyId, - 'text-vega-red-dark dark:text-vega-red': data?.seller.id, + [positiveClassNames]: data?.buyer.id === partyId, + [negativeClassNames]: data?.seller.id, }); }} valueFormatter={formatSize(partyId)} diff --git a/libs/orders/src/lib/components/order-feedback/order-feedback.tsx b/libs/orders/src/lib/components/order-feedback/order-feedback.tsx index 1845f41fe..e829966f5 100644 --- a/libs/orders/src/lib/components/order-feedback/order-feedback.tsx +++ b/libs/orders/src/lib/components/order-feedback/order-feedback.tsx @@ -1,6 +1,11 @@ import { useEnvironment } from '@vegaprotocol/environment'; import type { OrderEvent_busEvents_event_Order } from '../../order-hooks/__generated__'; -import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers'; +import { + addDecimalsFormatNumber, + t, + positiveClassNames, + negativeClassNames, +} from '@vegaprotocol/react-helpers'; import { OrderRejectionReasonMapping, OrderStatus, @@ -49,8 +54,8 @@ export const OrderFeedback = ({ transaction, order }: OrderFeedbackProps) => {

{`${ diff --git a/libs/orders/src/lib/components/order-list/order-list.tsx b/libs/orders/src/lib/components/order-list/order-list.tsx index 7b2ae1a72..0fdd81514 100644 --- a/libs/orders/src/lib/components/order-list/order-list.tsx +++ b/libs/orders/src/lib/components/order-list/order-list.tsx @@ -8,7 +8,13 @@ import { OrderTimeInForceMapping, OrderRejectionReasonMapping, } from '@vegaprotocol/types'; -import { addDecimal, getDateTimeFormat, t } from '@vegaprotocol/react-helpers'; +import { + addDecimal, + getDateTimeFormat, + t, + positiveClassNames, + negativeClassNames, +} from '@vegaprotocol/react-helpers'; import { AgGridDynamic as AgGrid, Button, @@ -125,12 +131,12 @@ export const OrderListTable = forwardRef( cellClass="font-mono text-right" type="rightAligned" cellClassRules={{ - 'text-vega-green-dark dark:text-vega-green': ({ + [positiveClassNames]: ({ data, }: { data: Orders_party_ordersConnection_edges_node; }) => data?.side === Side.SIDE_BUY, - 'text-vega-red-dark dark:text-vega-red': ({ + [negativeClassNames]: ({ data, }: { data: Orders_party_ordersConnection_edges_node; diff --git a/libs/orders/src/lib/order-hooks/use-order-submit.spec.tsx b/libs/orders/src/lib/order-hooks/use-order-submit.spec.tsx index 542cf73e3..e294a3e02 100644 --- a/libs/orders/src/lib/order-hooks/use-order-submit.spec.tsx +++ b/libs/orders/src/lib/order-hooks/use-order-submit.spec.tsx @@ -60,10 +60,7 @@ const defaultWalletContext = { connector: null, }; -function setup( - context?: Partial, - market = defaultMarket -) { +function setup(context?: Partial) { const mocks: MockedResponse = { request: { query: ORDER_EVENT_SUB, @@ -144,7 +141,7 @@ function setup( ); - return renderHook(() => useOrderSubmit(market), { wrapper }); + return renderHook(() => useOrderSubmit(), { wrapper }); } describe('useOrderSubmit', () => { @@ -164,11 +161,11 @@ describe('useOrderSubmit', () => { size: '10', timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT, side: Side.SIDE_BUY, - price: '1234567.89', - expiration: new Date('2022-01-01'), + price: '123456789', + expiresAt: new Date('2022-01-01'), }; await act(async () => { - result.current.submit(order); + result.current.submit({ ...order, marketId: defaultMarket.id }); }); expect(mockSendTx).toHaveBeenCalledWith({ @@ -176,14 +173,12 @@ describe('useOrderSubmit', () => { propagate: true, orderSubmission: { type: OrderType.TYPE_LIMIT, - marketId: defaultMarket.id, // Market provided from hook argument - size: '100', // size adjusted based on positionDecimalPlaces + marketId: defaultMarket.id, + size: '10', side: Side.SIDE_BUY, timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT, - price: '123456789', // Decimal removed - expiresAt: order.expiration - ? toNanoSeconds(order.expiration) - : undefined, + price: '123456789', + expiresAt: order.expiresAt ? toNanoSeconds(order.expiresAt) : undefined, }, }); }); diff --git a/libs/orders/src/lib/order-hooks/use-order-submit.ts b/libs/orders/src/lib/order-hooks/use-order-submit.ts deleted file mode 100644 index 3b5fad5a6..000000000 --- a/libs/orders/src/lib/order-hooks/use-order-submit.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { useCallback, useState } from 'react'; -import type { OrderEvent_busEvents_event_Order } from './__generated__'; -import { useVegaWallet } from '@vegaprotocol/wallet'; -import { - determineId, - removeDecimal, - toNanoSeconds, -} from '@vegaprotocol/react-helpers'; -import { useVegaTransaction } from '@vegaprotocol/wallet'; -import * as Sentry from '@sentry/react'; -import { useOrderEvent } from './use-order-event'; -import type { OrderTimeInForce, Side } from '@vegaprotocol/types'; -import { OrderType } from '@vegaprotocol/types'; - -export interface Order { - type: OrderType; - size: string; - side: Side; - timeInForce: OrderTimeInForce; - price?: string; - expiration?: Date; -} - -export interface Market { - id: string; - decimalPlaces: number; - positionDecimalPlaces: number; -} - -export const useOrderSubmit = (market: Market) => { - const { keypair } = useVegaWallet(); - const waitForOrderEvent = useOrderEvent(); - - const { - send, - transaction, - reset: resetTransaction, - setComplete, - TransactionDialog, - } = useVegaTransaction(); - - const [finalizedOrder, setFinalizedOrder] = - useState(null); - - const reset = useCallback(() => { - resetTransaction(); - setFinalizedOrder(null); - }, [resetTransaction]); - - const submit = useCallback( - async (order: Order) => { - if (!keypair || !order.side) { - return; - } - - setFinalizedOrder(null); - - try { - const res = await send({ - pubKey: keypair.pub, - propagate: true, - orderSubmission: { - marketId: market.id, - price: - order.type === OrderType.TYPE_LIMIT && order.price - ? removeDecimal(order.price, market.decimalPlaces) - : undefined, - size: removeDecimal(order.size, market.positionDecimalPlaces), - type: order.type, - side: order.side, - timeInForce: order.timeInForce, - expiresAt: order.expiration - ? toNanoSeconds(order.expiration) // Wallet expects timestamp in nanoseconds - : undefined, - }, - }); - - if (res?.signature) { - const resId = determineId(res.signature); - if (resId) { - waitForOrderEvent(resId, keypair.pub, (order) => { - setFinalizedOrder(order); - setComplete(); - }); - } - } - return res; - } catch (e) { - Sentry.captureException(e); - return; - } - }, - [keypair, send, market, setComplete, waitForOrderEvent] - ); - - return { - transaction, - finalizedOrder, - TransactionDialog, - submit, - reset, - }; -}; diff --git a/libs/orders/src/lib/order-hooks/use-order-submit.tsx b/libs/orders/src/lib/order-hooks/use-order-submit.tsx new file mode 100644 index 000000000..1a14bc209 --- /dev/null +++ b/libs/orders/src/lib/order-hooks/use-order-submit.tsx @@ -0,0 +1,165 @@ +import { useCallback, useState } from 'react'; +import type { ReactNode } from 'react'; +import type { OrderEvent_busEvents_event_Order } from './__generated__'; +import { useVegaWallet } from '@vegaprotocol/wallet'; +import { determineId, toNanoSeconds } from '@vegaprotocol/react-helpers'; +import { useVegaTransaction } from '@vegaprotocol/wallet'; +import * as Sentry from '@sentry/react'; +import { useOrderEvent } from './use-order-event'; +import type { OrderTimeInForce, Side } from '@vegaprotocol/types'; +import { OrderType, OrderStatus } from '@vegaprotocol/types'; +import { Icon, Intent } from '@vegaprotocol/ui-toolkit'; +import { t } from '@vegaprotocol/react-helpers'; + +export interface Order { + marketId: string; + type: OrderType; + size: string; + side: Side; + timeInForce: OrderTimeInForce; + price?: string; + expiresAt?: Date; +} + +export const getOrderDialogTitle = ( + status?: OrderStatus +): string | undefined => { + if (!status) { + return; + } + + switch (status) { + case OrderStatus.STATUS_ACTIVE: + return t('Order submitted'); + case OrderStatus.STATUS_FILLED: + return t('Order filled'); + case OrderStatus.STATUS_PARTIALLY_FILLED: + return t('Order partially filled'); + case OrderStatus.STATUS_PARKED: + return t('Order parked'); + case OrderStatus.STATUS_STOPPED: + return t('Order stopped'); + case OrderStatus.STATUS_CANCELLED: + return t('Order cancelled'); + case OrderStatus.STATUS_EXPIRED: + return t('Order expired'); + case OrderStatus.STATUS_REJECTED: + return t('Order rejected'); + default: + return t('Submission failed'); + } +}; + +export const getOrderDialogIntent = ( + status?: OrderStatus +): Intent | undefined => { + if (!status) { + return; + } + switch (status) { + case OrderStatus.STATUS_PARKED: + case OrderStatus.STATUS_EXPIRED: + case OrderStatus.STATUS_PARTIALLY_FILLED: + return Intent.Warning; + case OrderStatus.STATUS_REJECTED: + case OrderStatus.STATUS_STOPPED: + case OrderStatus.STATUS_CANCELLED: + return Intent.Danger; + case OrderStatus.STATUS_FILLED: + case OrderStatus.STATUS_ACTIVE: + return Intent.Success; + default: + return; + } +}; + +export const getOrderDialogIcon = ( + status?: OrderStatus +): ReactNode | undefined => { + if (!status) { + return; + } + + switch (status) { + case OrderStatus.STATUS_PARKED: + case OrderStatus.STATUS_EXPIRED: + return ; + case OrderStatus.STATUS_REJECTED: + case OrderStatus.STATUS_STOPPED: + case OrderStatus.STATUS_CANCELLED: + return ; + default: + return; + } +}; + +export const useOrderSubmit = () => { + const { keypair } = useVegaWallet(); + const waitForOrderEvent = useOrderEvent(); + + const { + send, + transaction, + reset: resetTransaction, + setComplete, + TransactionDialog, + } = useVegaTransaction(); + + const [finalizedOrder, setFinalizedOrder] = + useState(null); + + const reset = useCallback(() => { + resetTransaction(); + setFinalizedOrder(null); + }, [resetTransaction]); + + const submit = useCallback( + async (order: Order) => { + if (!keypair || !order.side) { + return; + } + + setFinalizedOrder(null); + + try { + const res = await send({ + pubKey: keypair.pub, + propagate: true, + orderSubmission: { + ...order, + price: + order.type === OrderType.TYPE_LIMIT && order.price + ? order.price + : undefined, + expiresAt: order.expiresAt + ? toNanoSeconds(order.expiresAt) // Wallet expects timestamp in nanoseconds + : undefined, + }, + }); + + if (res?.signature) { + const resId = determineId(res.signature); + if (resId) { + waitForOrderEvent(resId, keypair.pub, (order) => { + setFinalizedOrder(order); + setComplete(); + }); + } + } + return res; + } catch (e) { + Sentry.captureException(e); + return; + } + }, + [keypair, send, setComplete, waitForOrderEvent] + ); + + return { + transaction, + finalizedOrder, + TransactionDialog, + submit, + reset, + }; +}; diff --git a/libs/orders/src/lib/utils/get-default-order.ts b/libs/orders/src/lib/utils/get-default-order.ts index 775b955bb..1f193b46b 100644 --- a/libs/orders/src/lib/utils/get-default-order.ts +++ b/libs/orders/src/lib/utils/get-default-order.ts @@ -1,8 +1,12 @@ import { toDecimal } from '@vegaprotocol/react-helpers'; -import type { Order, Market } from '../order-hooks'; +import type { Order } from '../order-hooks'; import { OrderTimeInForce, OrderType, Side } from '@vegaprotocol/types'; -export const getDefaultOrder = (market: Market): Order => ({ +export const getDefaultOrder = (market: { + id: string; + positionDecimalPlaces: number; +}): Order => ({ + marketId: market.id, type: OrderType.TYPE_MARKET, side: Side.SIDE_BUY, timeInForce: OrderTimeInForce.TIME_IN_FORCE_IOC, diff --git a/libs/positions/src/index.ts b/libs/positions/src/index.ts index 8fb366719..99df7895b 100644 --- a/libs/positions/src/index.ts +++ b/libs/positions/src/index.ts @@ -1,4 +1,6 @@ -export * from './lib/positions-table'; +export * from './lib/__generated__/Positions'; export * from './lib/positions-container'; export * from './lib/positions-data-providers'; -export * from './lib/__generated__/Positions'; +export * from './lib/positions-table'; +export * from './lib/use-close-position'; +export * from './lib/use-position-event'; diff --git a/libs/positions/src/lib/positions-data-providers.spec.ts b/libs/positions/src/lib/positions-data-providers.spec.ts index 3d50e4afe..b4b2b9d5d 100644 --- a/libs/positions/src/lib/positions-data-providers.spec.ts +++ b/libs/positions/src/lib/positions-data-providers.spec.ts @@ -197,7 +197,7 @@ describe('getMetrics', () => { expect(metrics[0].currentLeverage).toBeCloseTo(1.02); expect(metrics[0].marketDecimalPlaces).toEqual(5); expect(metrics[0].positionDecimalPlaces).toEqual(0); - expect(metrics[0].assetDecimals).toEqual(5); + expect(metrics[0].decimals).toEqual(5); expect(metrics[0].liquidationPrice).toEqual('169990'); expect(metrics[0].lowMarginLevel).toEqual(false); expect(metrics[0].markPrice).toEqual('9431775'); @@ -222,7 +222,7 @@ describe('getMetrics', () => { expect(metrics[1].currentLeverage).toBeCloseTo(0.097); expect(metrics[1].marketDecimalPlaces).toEqual(5); expect(metrics[1].positionDecimalPlaces).toEqual(0); - expect(metrics[1].assetDecimals).toEqual(5); + expect(metrics[1].decimals).toEqual(5); expect(metrics[1].liquidationPrice).toEqual('9830750'); expect(metrics[1].lowMarginLevel).toEqual(false); expect(metrics[1].markPrice).toEqual('869762'); diff --git a/libs/positions/src/lib/positions-data-providers.ts b/libs/positions/src/lib/positions-data-providers.ts index c763d137a..bfaec808e 100644 --- a/libs/positions/src/lib/positions-data-providers.ts +++ b/libs/positions/src/lib/positions-data-providers.ts @@ -24,7 +24,7 @@ export interface Position { averageEntryPrice: string; capitalUtilisation: number; currentLeverage: number; - assetDecimals: number; + decimals: number; marketDecimalPlaces: number; positionDecimalPlaces: number; totalBalance: string; @@ -133,7 +133,12 @@ export const getMetrics = ( const marginAccount = accounts?.find((account) => { return account.market?.id === market.id; }); - if (!marginAccount || !marginLevel || !marketData) { + if ( + !marginAccount || + !marginLevel || + !marketData || + position.node.openVolume === '0' + ) { return; } const generalAccount = accounts?.find( @@ -141,7 +146,7 @@ export const getMetrics = ( account.asset.id === marginAccount.asset.id && account.type === AccountType.ACCOUNT_TYPE_GENERAL ); - const assetDecimals = marginAccount.asset.decimals; + const decimals = marginAccount.asset.decimals; const { positionDecimalPlaces, decimalPlaces: marketDecimalPlaces } = market; const openVolume = toBigNum( @@ -149,13 +154,10 @@ export const getMetrics = ( positionDecimalPlaces ); - const marginAccountBalance = toBigNum( - marginAccount.balance ?? 0, - assetDecimals - ); + const marginAccountBalance = toBigNum(marginAccount.balance ?? 0, decimals); const generalAccountBalance = toBigNum( generalAccount?.balance ?? 0, - assetDecimals + decimals ); const markPrice = toBigNum(marketData.markPrice, marketDecimalPlaces); @@ -180,19 +182,19 @@ export const getMetrics = ( marketDecimalPlaces ); - const searchPrice = openVolume.isEqualTo(0) - ? markPrice - : marginSearch - .minus(marginAccountBalance) - .dividedBy(openVolume) - .plus(markPrice); - const liquidationPrice = openVolume.isEqualTo(0) - ? markPrice - : marginMaintenance - .minus(marginAccountBalance) - .minus(generalAccountBalance) - .dividedBy(openVolume) - .plus(markPrice); + const searchPrice = marginSearch + .minus(marginAccountBalance) + .dividedBy(openVolume) + .plus(markPrice); + + const liquidationPrice = BigNumber.maximum( + 0, + marginMaintenance + .minus(marginAccountBalance) + .minus(generalAccountBalance) + .dividedBy(openVolume) + .plus(markPrice) + ); const lowMarginLevel = marginAccountBalance.isLessThan( @@ -206,9 +208,9 @@ export const getMetrics = ( currentLeverage: currentLeverage.toNumber(), marketDecimalPlaces, positionDecimalPlaces, - assetDecimals, + decimals, assetSymbol: marginLevel.asset.symbol, - totalBalance: totalBalance.multipliedBy(10 ** assetDecimals).toFixed(), + totalBalance: totalBalance.multipliedBy(10 ** decimals).toFixed(), lowMarginLevel, liquidationPrice: liquidationPrice .multipliedBy(10 ** marketDecimalPlaces) diff --git a/libs/positions/src/lib/positions-manager.tsx b/libs/positions/src/lib/positions-manager.tsx index d4d8f9a8f..d8b34f738 100644 --- a/libs/positions/src/lib/positions-manager.tsx +++ b/libs/positions/src/lib/positions-manager.tsx @@ -1,26 +1,40 @@ -import { useRef, useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useRef } from 'react'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { useDataProvider } from '@vegaprotocol/react-helpers'; -import type { AgGridReact } from 'ag-grid-react'; -import PositionsTable from './positions-table'; -import type { GetRowsParams } from './positions-table'; import { positionsMetricsDataProvider as dataProvider } from './positions-data-providers'; import type { Position } from './positions-data-providers'; +import { Positions } from './positions'; +import { useClosePosition } from '../'; interface PositionsManagerProps { partyId: string; } +const getSymbols = (positions: Position[]) => + Array.from(new Set(positions.map((position) => position.assetSymbol))).sort(); + export const PositionsManager = ({ partyId }: PositionsManagerProps) => { - const gridRef = useRef(null); const variables = useMemo(() => ({ partyId }), [partyId]); - const dataRef = useRef(null); + const assetSymbols = useRef(); + const { submit, TransactionDialog } = useClosePosition(); + const onClose = useCallback( + (position: Position) => { + submit(position); + }, + [submit] + ); const update = useCallback(({ data }: { data: Position[] | null }) => { - if (!gridRef.current?.api) { - return false; + if (data?.length) { + const newAssetSymbols = getSymbols(data); + if ( + !newAssetSymbols.every( + (symbol) => + assetSymbols.current && assetSymbols.current.includes(symbol) + ) + ) { + return false; + } } - dataRef.current = data; - gridRef.current.api.refreshInfiniteCache(); return true; }, []); const { data, error, loading } = useDataProvider({ @@ -28,26 +42,22 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => { update, variables, }); - dataRef.current = data; - const getRows = async ({ - successCallback, - startRow, - endRow, - }: GetRowsParams) => { - const rowsThisBlock = dataRef.current - ? dataRef.current.slice(startRow, endRow) - : []; - const lastRow = dataRef.current?.length ?? -1; - successCallback(rowsThisBlock, lastRow); - }; - return ( - - - + <> + + {data && + getSymbols(data)?.map((assetSymbol) => ( + + ))} + + +

Your position was not closed! This is still not implemented.

+ + ); }; diff --git a/libs/positions/src/lib/positions-table.spec.tsx b/libs/positions/src/lib/positions-table.spec.tsx index 9b5575234..0df7fc177 100644 --- a/libs/positions/src/lib/positions-table.spec.tsx +++ b/libs/positions/src/lib/positions-table.spec.tsx @@ -11,7 +11,7 @@ const singleRow: Position = { currentLeverage: 1.1, marketDecimalPlaces: 1, positionDecimalPlaces: 0, - assetDecimals: 2, + decimals: 2, totalBalance: '123456', assetSymbol: 'BTC', liquidationPrice: '83', // 8.3 diff --git a/libs/positions/src/lib/positions-table.stories.tsx b/libs/positions/src/lib/positions-table.stories.tsx index 88cf4ad53..65a4cce55 100644 --- a/libs/positions/src/lib/positions-table.stories.tsx +++ b/libs/positions/src/lib/positions-table.stories.tsx @@ -16,7 +16,7 @@ const longPosition: Position = { averageEntryPrice: '1134564', capitalUtilisation: 10, currentLeverage: 11, - assetDecimals: 2, + decimals: 2, marketDecimalPlaces: 2, positionDecimalPlaces: 2, // generalAccountBalance: '0', @@ -48,7 +48,7 @@ const shortPosition: Position = { averageEntryPrice: '23976', capitalUtilisation: 87, currentLeverage: 7, - assetDecimals: 2, + decimals: 2, marketDecimalPlaces: 2, positionDecimalPlaces: 2, // generalAccountBalance: '0', diff --git a/libs/positions/src/lib/positions-table.tsx b/libs/positions/src/lib/positions-table.tsx index 52c74e442..e82830bc5 100644 --- a/libs/positions/src/lib/positions-table.tsx +++ b/libs/positions/src/lib/positions-table.tsx @@ -1,6 +1,12 @@ import classNames from 'classnames'; import { forwardRef } from 'react'; -import type { ValueFormatterParams } from 'ag-grid-community'; +import type { CSSProperties } from 'react'; +import type { + ValueFormatterParams, + ValueGetterParams, + ICellRendererParams, + CellRendererSelectorResult, +} from 'ag-grid-community'; import { PriceFlashCell, addDecimalsFormatNumber, @@ -8,6 +14,8 @@ import { t, formatNumber, getDateTimeFormat, + signedNumberCssClass, + signedNumberCssClassRules, } from '@vegaprotocol/react-helpers'; import { AgGridDynamic as AgGrid, ProgressBar } from '@vegaprotocol/ui-toolkit'; import { AgGridColumn } from 'ag-grid-react'; @@ -15,7 +23,7 @@ import type { AgGridReact, AgGridReactProps } from 'ag-grid-react'; import type { IDatasource, IGetRowsParams } from 'ag-grid-community'; import type { Position } from './positions-data-providers'; import { MarketTradingMode } from '@vegaprotocol/types'; -import { Intent } from '@vegaprotocol/ui-toolkit'; +import { Intent, Button } from '@vegaprotocol/ui-toolkit'; export const getRowId = ({ data }: { data: Position }) => data.marketId; @@ -29,6 +37,8 @@ export interface Datasource extends IDatasource { interface Props extends AgGridReactProps { rowData?: Position[] | null; datasource?: Datasource; + onClose?: (data: Position) => void; + style?: CSSProperties; } type PositionsTableValueFormatterParams = Omit< @@ -43,12 +53,15 @@ export interface MarketNameCellProps { } export const MarketNameCell = ({ valueFormatted }: MarketNameCellProps) => { - return valueFormatted ? ( -
-
{valueFormatted[0]}
- {valueFormatted[1] ?
{valueFormatted[1]}
: null} -
- ) : null; + if (valueFormatted && valueFormatted[1]) { + return ( +
+
{valueFormatted[0]}
+
{valueFormatted[1]}
+
+ ); + } + return (valueFormatted && valueFormatted[0]) || undefined; }; export interface PriceCellProps { @@ -63,14 +76,14 @@ export interface PriceCellProps { export const ProgressBarCell = ({ valueFormatted }: PriceCellProps) => { return valueFormatted ? ( <> -
+
{valueFormatted.low}
{valueFormatted.high}
) : null; @@ -91,14 +104,10 @@ export const AmountCell = ({ valueFormatted }: AmountCellProps) => { } const { openVolume, positionDecimalPlaces, marketDecimalPlaces, notional } = valueFormatted; - const isShortPosition = openVolume.startsWith('-'); return valueFormatted ? ( -
+
{volumePrefix( addDecimalsFormatNumber(openVolume, positionDecimalPlaces) @@ -113,234 +122,281 @@ export const AmountCell = ({ valueFormatted }: AmountCellProps) => { AmountCell.displayName = 'AmountCell'; -export const PositionsTable = forwardRef((props, ref) => { +const ButtonCell = ({ + onClick, + data, +}: { + onClick: (position: Position) => void; + data: Position; +}) => { return ( - - { - if (!value) { - return undefined; - } - // split market name into two parts, 'Part1 (Part2)' - const matches = value.match(/^(.*)\((.*)\)\s*$/); - if (matches) { - return [matches[1].trim(), matches[2].trim()]; - } - return [value]; - }} - /> - { - if (!value || !data) { - return undefined; - } - return data; - }} - /> - { - if (!data) { - return undefined; - } - if ( - data.marketTradingMode === - MarketTradingMode.TRADING_MODE_OPENING_AUCTION - ) { - return '-'; - } - return addDecimalsFormatNumber( - value.toString(), - data.marketDecimalPlaces - ); - }} - /> - ' + - ` ${t('Liquidation price (est)')}` + - ' ' + - '
', - }} - flex={2} - cellRenderer="ProgressBarCell" - valueFormatter={({ - data, - }: PositionsTableValueFormatterParams): - | PriceCellProps['valueFormatted'] - | undefined => { - if (!data) { - return undefined; - } - const min = BigInt(data.averageEntryPrice); - const max = BigInt(data.liquidationPrice); - const mid = BigInt(data.markPrice); - const range = max - min; - return { - low: addDecimalsFormatNumber( - min.toString(), - data.marketDecimalPlaces - ), - high: addDecimalsFormatNumber( - max.toString(), - data.marketDecimalPlaces - ), - value: range ? Number(((mid - min) * BigInt(100)) / range) : 0, - intent: data.lowMarginLevel ? Intent.Warning : undefined, - }; - }} - /> - - value === undefined ? undefined : formatNumber(value.toString(), 1) - } - /> - { - if (!data) { - return undefined; - } - return { - low: `${formatNumber(value, 2)}%`, - high: addDecimalsFormatNumber( - data.totalBalance, - data.assetDecimals - ), - value: Number(value), - }; - }} - /> - value && BigInt(value) > 0, - 'text-vega-red-dark dark:text-vega-red': ({ - value, - }: { - value: string; - }) => value && BigInt(value) < 0, - }} - valueFormatter={({ - value, - data, - }: PositionsTableValueFormatterParams & { - value: Position['realisedPNL']; - }) => - value === undefined - ? undefined - : addDecimalsFormatNumber(value.toString(), data.assetDecimals) - } - cellRenderer="PriceFlashCell" - headerTooltip={t('P&L excludes any fees paid.')} - /> - value && BigInt(value) > 0, - 'text-vega-red-dark dark:text-vega-red': ({ - value, - }: { - value: string; - }) => value && BigInt(value) < 0, - }} - valueFormatter={({ - value, - data, - }: PositionsTableValueFormatterParams & { - value: Position['unrealisedPNL']; - }) => - value === undefined - ? undefined - : addDecimalsFormatNumber(value.toString(), data.assetDecimals) - } - cellRenderer="PriceFlashCell" - /> - { - if (!value) { - return value; - } - return getDateTimeFormat().format(new Date(value)); - }} - /> - + ); -}); +}; + +const EmptyCell = () => ''; + +export const PositionsTable = forwardRef( + ({ onClose, ...props }, ref) => { + return ( + + { + if (!value) { + return undefined; + } + // split market name into two parts, 'Part1 (Part2)' or 'Part1 - Part2' + const matches = value.match(/^(.*)(\((.*)\)| - (.*))\s*$/); + if (matches) { + return [matches[1].trim(), matches[3].trim()]; + } + return [value]; + }} + /> + { + return node?.rowPinned ? data?.notional : data?.openVolume; + }} + type="rightAligned" + cellRendererSelector={( + params: ICellRendererParams + ): CellRendererSelectorResult => { + return { + component: params.node.rowPinned ? PriceFlashCell : AmountCell, + }; + }} + valueFormatter={({ + value, + data, + node, + }: PositionsTableValueFormatterParams & { + value: Position['openVolume']; + }): AmountCellProps['valueFormatted'] | string => { + if (!value || !data) { + return undefined; + } + if (node?.rowPinned) { + return addDecimalsFormatNumber(value, data.decimals); + } + return data; + }} + /> + { + return { + component: params.node.rowPinned ? EmptyCell : PriceFlashCell, + }; + }} + valueFormatter={({ + value, + data, + node, + }: PositionsTableValueFormatterParams & { + value: Position['markPrice']; + }) => { + if (!data || !value || node?.rowPinned) { + return undefined; + } + if ( + data.marketTradingMode === + MarketTradingMode.TRADING_MODE_OPENING_AUCTION + ) { + return '-'; + } + return addDecimalsFormatNumber( + value.toString(), + data.marketDecimalPlaces + ); + }} + /> + ' + + ` ${t('Liquidation price (est)')}` + + ' ' + + '
', + }} + flex={2} + cellRendererSelector={( + params: ICellRendererParams + ): CellRendererSelectorResult => { + return { + component: params.node.rowPinned ? EmptyCell : ProgressBarCell, + }; + }} + valueFormatter={({ + data, + node, + }: PositionsTableValueFormatterParams): + | PriceCellProps['valueFormatted'] + | undefined => { + if (!data || node?.rowPinned) { + return undefined; + } + const min = BigInt(data.averageEntryPrice); + const max = BigInt(data.liquidationPrice); + const mid = BigInt(data.markPrice); + const range = max - min; + return { + low: addDecimalsFormatNumber( + min.toString(), + data.marketDecimalPlaces + ), + high: addDecimalsFormatNumber( + max.toString(), + data.marketDecimalPlaces + ), + value: range ? Number(((mid - min) * BigInt(100)) / range) : 0, + intent: data.lowMarginLevel ? Intent.Warning : undefined, + }; + }} + /> + { + return { + component: params.node.rowPinned ? EmptyCell : PriceFlashCell, + }; + }} + valueFormatter={({ + value, + node, + }: PositionsTableValueFormatterParams & { + value: Position['currentLeverage']; + }) => + value === undefined ? undefined : formatNumber(value.toString(), 1) + } + /> + { + return { + component: params.node.rowPinned ? EmptyCell : ProgressBarCell, + }; + }} + valueFormatter={({ + data, + value, + node, + }: PositionsTableValueFormatterParams & { + value: Position['capitalUtilisation']; + }): PriceCellProps['valueFormatted'] | undefined => { + if (!data || node?.rowPinned) { + return undefined; + } + return { + low: `${formatNumber(value, 2)}%`, + high: addDecimalsFormatNumber(data.totalBalance, data.decimals), + value: Number(value), + }; + }} + /> + + value === undefined + ? undefined + : addDecimalsFormatNumber(value.toString(), data.decimals) + } + cellRenderer="PriceFlashCell" + headerTooltip={t('P&L excludes any fees paid.')} + /> + + value === undefined + ? undefined + : addDecimalsFormatNumber(value.toString(), data.decimals) + } + cellRenderer="PriceFlashCell" + /> + { + if (!value) { + return value; + } + return getDateTimeFormat().format(new Date(value)); + }} + /> + {onClose ? ( + { + return { + component: params.node.rowPinned ? EmptyCell : ButtonCell, + }; + }} + cellRendererParams={{ onClick: onClose }} + /> + ) : null} + + ); + } +); export default PositionsTable; diff --git a/libs/positions/src/lib/positions.tsx b/libs/positions/src/lib/positions.tsx new file mode 100644 index 000000000..4b5d19146 --- /dev/null +++ b/libs/positions/src/lib/positions.tsx @@ -0,0 +1,108 @@ +import { useRef, useCallback, useMemo, memo } from 'react'; +import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; +import { BigNumber } from 'bignumber.js'; +import { t, toBigNum, useDataProvider } from '@vegaprotocol/react-helpers'; +import type { AgGridReact } from 'ag-grid-react'; +import filter from 'lodash/filter'; +import PositionsTable from './positions-table'; +import type { GetRowsParams } from './positions-table'; +import { positionsMetricsDataProvider as dataProvider } from './positions-data-providers'; +import { AssetBalance } from '@vegaprotocol/accounts'; +import type { Position } from './positions-data-providers'; +interface PositionsProps { + partyId: string; + assetSymbol: string; + onClose: (position: Position) => void; +} + +const getSummaryRow = (positions: Position[]) => { + const summaryRow = { + notional: new BigNumber(0), + realisedPNL: BigInt(0), + unrealisedPNL: BigInt(0), + }; + positions.forEach((position) => { + summaryRow.notional = summaryRow.notional.plus( + toBigNum(position.notional, position.marketDecimalPlaces) + ); + summaryRow.realisedPNL += BigInt(position.realisedPNL); + summaryRow.unrealisedPNL += BigInt(position.unrealisedPNL); + }); + const decimals = positions[0]?.decimals || 0; + return { + marketName: t('Total'), + // we are using asset decimals instead of market decimals because each market can have different decimals + notional: summaryRow.notional + .multipliedBy(10 ** decimals) + .toFixed() + .toString(), + realisedPNL: summaryRow.realisedPNL.toString(), + unrealisedPNL: summaryRow.unrealisedPNL.toString(), + decimals, + }; +}; + +export const Positions = memo( + ({ partyId, assetSymbol, onClose }: PositionsProps) => { + const gridRef = useRef(null); + const variables = useMemo(() => ({ partyId }), [partyId]); + const dataRef = useRef(null); + const update = useCallback( + ({ data }: { data: Position[] | null }) => { + if (!gridRef.current?.api) { + return false; + } + dataRef.current = filter(data, { assetSymbol }); + gridRef.current.api.refreshInfiniteCache(); + return true; + }, + [assetSymbol] + ); + const { data, error, loading } = useDataProvider({ + dataProvider, + update, + variables, + }); + dataRef.current = filter(data, { assetSymbol }); + const getRows = async ({ + successCallback, + startRow, + endRow, + }: GetRowsParams) => { + const rowsThisBlock = dataRef.current + ? dataRef.current.slice(startRow, endRow) + : []; + const lastRow = dataRef.current?.length ?? -1; + successCallback(rowsThisBlock, lastRow); + if (gridRef.current?.api) { + gridRef.current.api.setPinnedBottomRowData([ + getSummaryRow(rowsThisBlock), + ]); + } + }; + return ( + +
+

+ {assetSymbol} {t('markets')} +

+

+ {assetSymbol} {t('balance')}: + + + +

+
+ +
+ ); + } +); diff --git a/libs/positions/src/lib/use-close-position.ts b/libs/positions/src/lib/use-close-position.ts new file mode 100644 index 000000000..2903b6bc2 --- /dev/null +++ b/libs/positions/src/lib/use-close-position.ts @@ -0,0 +1,64 @@ +import { useCallback } from 'react'; +import { useVegaWallet } from '@vegaprotocol/wallet'; +import { determineId } from '@vegaprotocol/react-helpers'; +import { useVegaTransaction } from '@vegaprotocol/wallet'; +import * as Sentry from '@sentry/react'; +import { usePositionEvent } from '../'; +import type { Position } from '../'; + +export const useClosePosition = () => { + const { keypair } = useVegaWallet(); + const waitForPositionEvent = usePositionEvent(); + + const { + send, + transaction, + reset: resetTransaction, + setComplete, + TransactionDialog, + } = useVegaTransaction(); + + const reset = useCallback(() => { + resetTransaction(); + }, [resetTransaction]); + + const submit = useCallback( + async (position: Position) => { + if (!keypair || position.openVolume === '0') { + return; + } + + try { + const res = await send({ + pubKey: keypair.pub, + propagate: true, + orderCancellation: { + marketId: position.marketId, + orderId: '', + }, + }); + + if (res?.signature) { + const resId = determineId(res.signature); + if (resId) { + waitForPositionEvent(resId, keypair.pub, () => { + setComplete(); + }); + } + } + return res; + } catch (e) { + Sentry.captureException(e); + return; + } + }, + [keypair, send, setComplete, waitForPositionEvent] + ); + + return { + transaction, + TransactionDialog, + submit, + reset, + }; +}; diff --git a/libs/positions/src/lib/use-position-event.ts b/libs/positions/src/lib/use-position-event.ts new file mode 100644 index 000000000..680ec36b9 --- /dev/null +++ b/libs/positions/src/lib/use-position-event.ts @@ -0,0 +1,14 @@ +import { useCallback } from 'react'; + +// this should be replaced by implementation of busEvents listener when it will be available +export const usePositionEvent = () => { + const waitForOrderEvent = useCallback( + (id: string, partyId: string, callback: () => void) => { + Promise.resolve().then(() => { + callback(); + }); + }, + [] + ); + return waitForOrderEvent; +}; diff --git a/libs/react-helpers/src/lib/generic-data-provider.ts b/libs/react-helpers/src/lib/generic-data-provider.ts index d235669a0..94b8427e4 100644 --- a/libs/react-helpers/src/lib/generic-data-provider.ts +++ b/libs/react-helpers/src/lib/generic-data-provider.ts @@ -488,23 +488,27 @@ function makeDerivedDataProviderInternal( let loaded = false; // notify single callback about current state, delta is passes optionally only if notify was invoked onNext - const notify = (callback: UpdateCallback) => { + const notify = ( + callback: UpdateCallback, + updateData?: UpdateData + ) => { callback({ data, error, loading, loaded, pageInfo: null, + ...updateData, }); }; // notify all callbacks - const notifyAll = () => + const notifyAll = (updateData?: UpdateData) => callbacks.forEach((callback) => { - notify(callback); + notify(callback, updateData); }); - const combine = () => { + const combine = (isUpdate = false) => { let newError: Error | undefined; let newLoading = false; let newLoaded = true; @@ -529,7 +533,7 @@ function makeDerivedDataProviderInternal( error = newError; loaded = newLoaded; data = newData; - notifyAll(); + notifyAll({ isUpdate }); } }; @@ -541,7 +545,7 @@ function makeDerivedDataProviderInternal( dependency( (updateData) => { parts[i] = updateData; - combine(); + combine(updateData.isUpdate); }, client, variables diff --git a/libs/react-helpers/src/lib/grid/cell-class-rules.ts b/libs/react-helpers/src/lib/grid/cell-class-rules.ts new file mode 100644 index 000000000..882af2e72 --- /dev/null +++ b/libs/react-helpers/src/lib/grid/cell-class-rules.ts @@ -0,0 +1,23 @@ +export const positiveClassNames = 'text-vega-green-dark dark:text-vega-green'; +export const negativeClassNames = 'text-vega-red-dark dark:text-vega-red'; + +const isPositive = ({ value }: { value: string | bigint | number }) => + value && ((typeof value === 'string' && !value.startsWith('-')) || value > 0); + +const isNegative = ({ value }: { value: string | bigint | number }) => + value && ((typeof value === 'string' && value.startsWith('-')) || value < 0); + +export const signedNumberCssClass = (value: string | bigint | number) => { + if (isPositive({ value })) { + return positiveClassNames; + } + if (isNegative({ value })) { + return negativeClassNames; + } + return ''; +}; + +export const signedNumberCssClassRules = { + [positiveClassNames]: isPositive, + [negativeClassNames]: isNegative, +}; diff --git a/libs/react-helpers/src/lib/grid/index.tsx b/libs/react-helpers/src/lib/grid/index.tsx index a9a1a56e2..1f96724a5 100644 --- a/libs/react-helpers/src/lib/grid/index.tsx +++ b/libs/react-helpers/src/lib/grid/index.tsx @@ -1,3 +1,4 @@ +export * from './cell-class-rules'; export * from './cumulative-vol-cell'; export * from './flash-cell'; export * from './price-cell'; diff --git a/libs/ui-toolkit/src/components/price-change/price-change-cell.tsx b/libs/ui-toolkit/src/components/price-change/price-change-cell.tsx index 6fdf977e3..523c1436f 100644 --- a/libs/ui-toolkit/src/components/price-change/price-change-cell.tsx +++ b/libs/ui-toolkit/src/components/price-change/price-change-cell.tsx @@ -4,7 +4,7 @@ import { } from '@vegaprotocol/react-helpers'; import BigNumber from 'bignumber.js'; import React from 'react'; - +import { signedNumberCssClass } from '@vegaprotocol/react-helpers'; import { Arrow } from '../arrows/arrow'; export interface PriceChangeCellProps { @@ -36,20 +36,13 @@ export const priceChange = (candles: string[]) => { : 0; }; -const priceChangeClassNames = (value: number | bigint) => - value === 0 - ? 'text-black dark:text-white' - : value > 0 - ? `text-vega-green-dark dark:text-vega-green ` - : `text-vega-red-dark dark:text-vega-red`; - export const PriceCellChange = React.memo( ({ candles, decimalPlaces }: PriceChangeCellProps) => { const change = priceChange(candles); const changePercentage = priceChangePercentage(candles); return (