diff --git a/apps/simple-trading-app-e2e/.env b/apps/simple-trading-app-e2e/.env index 16bb3ce7c..979b577d6 100644 --- a/apps/simple-trading-app-e2e/.env +++ b/apps/simple-trading-app-e2e/.env @@ -24,3 +24,4 @@ NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL NX_VEGA_URL=https://lb.testnet.vega.xyz/query NX_VEGA_ENV=TESTNET NX_VEGA_REST=https://lb.testnet.vega.xyz/datanode/rest +NX_VEGA_WALLET_URL=http://localhost:1789/api/v1 diff --git a/apps/simple-trading-app-e2e/cypress.config.js b/apps/simple-trading-app-e2e/cypress.config.js index 1436e587c..889458a22 100644 --- a/apps/simple-trading-app-e2e/cypress.config.js +++ b/apps/simple-trading-app-e2e/cypress.config.js @@ -18,4 +18,22 @@ module.exports = defineConfig({ viewportWidth: 1440, viewportHeight: 900, }, + env: { + TRADING_TEST_VEGA_WALLET_NAME: 'UI_Trading_Test', + ETHEREUM_PROVIDER_URL: + 'https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8', + VEGA_PUBLIC_KEY: + '47836c253520d2661bf5bed6339c0de08fd02cf5d4db0efee3b4373f20c7d278', + VEGA_PUBLIC_KEY2: + '1a18cdcaaa4f44a57b35a4e9b77e0701c17a476f2b407620f8c17371740cf2e4', + TRUNCATED_VEGA_PUBLIC_KEY: '47836c…c7d278', + TRUNCATED_VEGA_PUBLIC_KEY2: '1a18cd…0cf2e4', + ETHEREUM_WALLET_ADDRESS: '0x265Cc6d39a1B53d0d92068443009eE7410807158', + ETHERSCAN_URL: 'https://ropsten.etherscan.io', + tsConfig: 'tsconfig.json', + TAGS: 'not @todo and not @ignore and not @manual', + TRADING_TEST_VEGA_WALLET_PASSPHRASE: '123', + ETH_WALLET_MNEMONIC: + 'ugly gallery notice network true range brave clarify flat logic someone chunk', + }, }); diff --git a/apps/simple-trading-app-e2e/src/integration/market-trade.test.ts b/apps/simple-trading-app-e2e/src/integration/market-trade.test.ts index e88b7d90d..0c09677c1 100644 --- a/apps/simple-trading-app-e2e/src/integration/market-trade.test.ts +++ b/apps/simple-trading-app-e2e/src/integration/market-trade.test.ts @@ -1,6 +1,22 @@ import { aliasQuery } from '@vegaprotocol/cypress'; import { generateSimpleMarkets } from '../support/mocks/generate-markets'; import { generateDealTicket } from '../support/mocks/generate-deal-ticket'; +import { generateMarketTags } from '../support/mocks/generate-market-tags'; +import { generateMarketPositions } from '../support/mocks/generate-market-positions'; +import { generateEstimateOrder } from '../support/mocks/generate-estimate-order'; +import { generatePartyBalance } from '../support/mocks/generate-party-balance'; + +const connectVegaWallet = () => { + const form = 'rest-connector-form'; + const walletName = Cypress.env('TRADING_TEST_VEGA_WALLET_NAME'); + const walletPassphrase = Cypress.env('TRADING_TEST_VEGA_WALLET_PASSPHRASE'); + + cy.getByTestId('connect-vega-wallet').click(); + cy.getByTestId('connectors-list').find('button').click(); + cy.getByTestId(form).find('#wallet').click().type(walletName); + cy.getByTestId(form).find('#passphrase').click().type(walletPassphrase); + cy.getByTestId('rest-connector-form').find('button[type=submit]').click(); +}; describe('Market trade', () => { let markets; @@ -8,6 +24,10 @@ describe('Market trade', () => { cy.mockGQL((req) => { aliasQuery(req, 'SimpleMarkets', generateSimpleMarkets()); aliasQuery(req, 'DealTicketQuery', generateDealTicket()); + aliasQuery(req, 'MarketTags', generateMarketTags()); + aliasQuery(req, 'MarketPositions', generateMarketPositions()); + aliasQuery(req, 'EstimateOrder', generateEstimateOrder()); + aliasQuery(req, 'PartyBalanceQuery', generatePartyBalance()); }); cy.visit('/markets'); cy.wait('@SimpleMarkets').then((response) => { @@ -64,4 +84,32 @@ describe('Market trade', () => { ); } }); + + it('order review should display proper calculations', () => { + if (markets?.length) { + cy.visit(`/trading/${markets[0].id}`); + connectVegaWallet(); + cy.get('h3').contains('Review Trade').click(); + cy.getByTestId('key-value-table') + .find('dl') + .eq(1) + .find('dd div') + .should('have.text', '3.44055'); + cy.getByTestId('key-value-table') + .find('dl') + .eq(2) + .find('dd div') + .should('have.text', '1.00000'); + cy.getByTestId('key-value-table') + .find('dl') + .eq(3) + .find('dd div') + .should('have.text', '-785.81045'); + cy.getByTestId('place-order').click(); + cy.getByTestId('dialog-title').should( + 'have.text', + 'Confirm transaction in wallet' + ); + } + }); }); diff --git a/apps/simple-trading-app-e2e/src/support/mocks/generate-estimate-order.ts b/apps/simple-trading-app-e2e/src/support/mocks/generate-estimate-order.ts new file mode 100644 index 000000000..f7ee51bc1 --- /dev/null +++ b/apps/simple-trading-app-e2e/src/support/mocks/generate-estimate-order.ts @@ -0,0 +1,12 @@ +export const generateEstimateOrder = () => { + return { + estimateOrder: { + totalFeeAmount: '16085.09240212.7380425.46', + marginLevels: { + initialLevel: '2844054.80937741220203', + __typename: 'MarginLevels', + }, + __typename: 'OrderEstimate', + }, + }; +}; diff --git a/apps/simple-trading-app-e2e/src/support/mocks/generate-market-positions.ts b/apps/simple-trading-app-e2e/src/support/mocks/generate-market-positions.ts new file mode 100644 index 000000000..aa0f2f329 --- /dev/null +++ b/apps/simple-trading-app-e2e/src/support/mocks/generate-market-positions.ts @@ -0,0 +1,43 @@ +export const generateMarketPositions = () => { + return { + party: { + id: '2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1', + positionsConnection: { + edges: [ + { + node: { + openVolume: '3', + market: { + id: '2751c508f9759761f912890f37fb3f97a00300bf7685c02a56a86e05facfe221', + accounts: [ + { balance: '0', __typename: 'Account' }, + { balance: '0', __typename: 'Account' }, + ], + __typename: 'Market', + }, + __typename: 'Position', + }, + __typename: 'PositionEdge', + }, + { + node: { + openVolume: '12', + market: { + id: 'first-btcusd-id', + accounts: [ + { balance: '10', __typename: 'Account' }, + { balance: '15', __typename: 'Account' }, + ], + __typename: 'Market', + }, + __typename: 'Position', + }, + __typename: 'PositionEdge', + }, + ], + __typename: 'PositionConnection', + }, + __typename: 'Party', + }, + }; +}; diff --git a/apps/simple-trading-app-e2e/src/support/mocks/generate-market-tags.ts b/apps/simple-trading-app-e2e/src/support/mocks/generate-market-tags.ts new file mode 100644 index 000000000..abf5477e3 --- /dev/null +++ b/apps/simple-trading-app-e2e/src/support/mocks/generate-market-tags.ts @@ -0,0 +1,25 @@ +export const generateMarketTags = () => { + return { + market: { + tradableInstrument: { + instrument: { + metadata: { + tags: [ + 'formerly:2839D9B2329C9E70', + 'base:AAVE', + 'quote:DAI', + 'class:fx/crypto', + 'monthly', + 'sector:defi', + 'settlement:2022-08-01', + ], + __typename: 'InstrumentMetadata', + }, + __typename: 'Instrument', + }, + __typename: 'TradableInstrument', + }, + __typename: 'Market', + }, + }; +}; diff --git a/apps/simple-trading-app-e2e/src/support/mocks/generate-party-balance.ts b/apps/simple-trading-app-e2e/src/support/mocks/generate-party-balance.ts new file mode 100644 index 000000000..c38801314 --- /dev/null +++ b/apps/simple-trading-app-e2e/src/support/mocks/generate-party-balance.ts @@ -0,0 +1,53 @@ +export const generatePartyBalance = () => { + return { + party: { + accounts: [ + { + balance: '88474051', + asset: { + id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61', + symbol: 'tDAI', + name: 'tDAI TEST', + decimals: 5, + __typename: 'Asset', + }, + __typename: 'Account', + }, + { + balance: '100000000', + asset: { + id: '8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4', + symbol: 'tEURO', + name: 'tEURO TEST', + decimals: 5, + __typename: 'Asset', + }, + __typename: 'Account', + }, + { + balance: '3412867', + asset: { + id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61', + symbol: 'tDAI', + name: 'tDAI TEST', + decimals: 5, + __typename: 'Asset', + }, + __typename: 'Account', + }, + { + balance: '70007', + asset: { + id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61', + symbol: 'tDAI', + name: 'tDAI TEST', + decimals: 5, + __typename: 'Asset', + }, + __typename: 'Account', + }, + ], + __typename: 'Party', + }, + }; +}; diff --git a/apps/simple-trading-app/.env b/apps/simple-trading-app/.env index 72bdc67a8..f572c2eeb 100644 --- a/apps/simple-trading-app/.env +++ b/apps/simple-trading-app/.env @@ -21,3 +21,4 @@ NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL NX_VEGA_CONFIG_URL="https://static.vega.xyz/assets/testnet-network.json" NX_VEGA_ENV = 'TESTNET' NX_VEGA_URL="https://lb.testnet.vega.xyz/query" +NX_VEGA_WALLET_URL=http://localhost:1789/api/v1 diff --git a/apps/simple-trading-app/src/app/components/deal-ticket/__generated__/MarketTags.ts b/apps/simple-trading-app/src/app/components/deal-ticket/__generated__/MarketTags.ts new file mode 100644 index 000000000..36694839a --- /dev/null +++ b/apps/simple-trading-app/src/app/components/deal-ticket/__generated__/MarketTags.ts @@ -0,0 +1,51 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: MarketTags +// ==================================================== + +export interface MarketTags_market_tradableInstrument_instrument_metadata { + __typename: "InstrumentMetadata"; + /** + * An arbitrary list of tags to associated to associate to the Instrument (string list) + */ + tags: string[] | null; +} + +export interface MarketTags_market_tradableInstrument_instrument { + __typename: "Instrument"; + /** + * Metadata for this instrument + */ + metadata: MarketTags_market_tradableInstrument_instrument_metadata; +} + +export interface MarketTags_market_tradableInstrument { + __typename: "TradableInstrument"; + /** + * An instance of or reference to a fully specified instrument. + */ + instrument: MarketTags_market_tradableInstrument_instrument; +} + +export interface MarketTags_market { + __typename: "Market"; + /** + * An instance of or reference to a tradable instrument. + */ + tradableInstrument: MarketTags_market_tradableInstrument; +} + +export interface MarketTags { + /** + * An instrument that is trading on the VEGA network + */ + market: MarketTags_market | null; +} + +export interface MarketTagsVariables { + marketId: string; +} diff --git a/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-container.tsx b/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-container.tsx index 57c974f16..f645feb52 100644 --- a/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-container.tsx +++ b/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-container.tsx @@ -61,7 +61,7 @@ export const DealTicketContainer = () => { isWalletConnected={!!keypair?.pub} /> )} - + )} diff --git a/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-steps.tsx b/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-steps.tsx index 137bd6c11..4be517c03 100644 --- a/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-steps.tsx +++ b/apps/simple-trading-app/src/app/components/deal-ticket/deal-ticket-steps.tsx @@ -1,9 +1,16 @@ -import * as React from 'react'; +import React, { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useForm, Controller } from 'react-hook-form'; import { Stepper } from '../stepper'; import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket'; -import { Button, InputError } from '@vegaprotocol/ui-toolkit'; -import { DealTicketAmount, MarketSelector } from '@vegaprotocol/deal-ticket'; +import { InputError } from '@vegaprotocol/ui-toolkit'; +import { + DealTicketAmount, + getDialogTitle, + getDialogIntent, + getDialogIcon, + MarketSelector, +} from '@vegaprotocol/deal-ticket'; import type { Order } from '@vegaprotocol/orders'; import { VegaTxStatus } from '@vegaprotocol/wallet'; import { t, addDecimal, toDecimal } from '@vegaprotocol/react-helpers'; @@ -11,17 +18,22 @@ import { getDefaultOrder, useOrderValidation, useOrderSubmit, + OrderFeedback, } from '@vegaprotocol/orders'; -import { useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; import MarketNameRenderer from '../simple-market-list/simple-market-renderer'; import SideSelector, { SIDE_NAMES } from './side-selector'; +import ReviewTrade from './review-trade'; +import type { PartyBalanceQuery } from './__generated__/PartyBalanceQuery'; interface DealTicketMarketProps { market: DealTicketQuery_market; + partyData?: PartyBalanceQuery; } -export const DealTicketSteps = ({ market }: DealTicketMarketProps) => { +export const DealTicketSteps = ({ + market, + partyData, +}: DealTicketMarketProps) => { const navigate = useNavigate(); const setMarket = useCallback( (marketId) => { @@ -45,6 +57,7 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => { const orderType = watch('type'); const orderTimeInForce = watch('timeInForce'); const orderSide = watch('side'); + const order = watch(); const { message: invalidText, isDisabled } = useOrderValidation({ step, @@ -54,7 +67,8 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => { fieldErrors: errors, }); - const { submit, transaction } = useOrderSubmit(market); + const { submit, transaction, finalizedOrder, TransactionDialog } = + useOrderSubmit(market); const transactionStatus = transaction.status === VegaTxStatus.Requested || @@ -101,7 +115,7 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => { component: ( { {invalidText} )} - + + ), disabled: true, diff --git a/apps/simple-trading-app/src/app/components/deal-ticket/review-trade.tsx b/apps/simple-trading-app/src/app/components/deal-ticket/review-trade.tsx new file mode 100644 index 000000000..81527e229 --- /dev/null +++ b/apps/simple-trading-app/src/app/components/deal-ticket/review-trade.tsx @@ -0,0 +1,146 @@ +import { addDecimal, formatNumber, t } from '@vegaprotocol/react-helpers'; +import { + Button, + Icon, + KeyValueTable, + KeyValueTableRow, +} from '@vegaprotocol/ui-toolkit'; +import * as React from 'react'; +import classNames from 'classnames'; +import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket'; +import type { Order } from '@vegaprotocol/orders'; +import { SIDE_NAMES } from './side-selector'; +import { useVegaWallet, VegaWalletOrderSide } from '@vegaprotocol/wallet'; +import SimpleMarketExpires from '../simple-market-list/simple-market-expires'; +import { gql, useQuery } from '@apollo/client'; +import type { + MarketTags, + MarketTagsVariables, +} from './__generated__/MarketTags'; +import useOrderMargin from '../../hooks/use-order-margin'; +import useOrderCloseOut from '../../hooks/use-order-closeout'; +import { IconNames } from '@blueprintjs/icons'; +import type { PartyBalanceQuery } from './__generated__/PartyBalanceQuery'; + +export const MARKET_TAGS_QUERY = gql` + query MarketTags($marketId: ID!) { + market(id: $marketId) { + tradableInstrument { + instrument { + metadata { + tags + } + } + } + } + } +`; + +interface Props { + market: DealTicketQuery_market; + isDisabled: boolean; + transactionStatus?: string; + order: Order; + partyData?: PartyBalanceQuery; +} + +export default ({ + isDisabled, + market, + order, + transactionStatus, + partyData, +}: Props) => { + const { keypair } = useVegaWallet(); + const { data: tagsData } = useQuery( + MARKET_TAGS_QUERY, + { + variables: { marketId: market.id }, + } + ); + const estMargin = useOrderMargin({ + order, + market, + partyId: keypair?.pub || '', + }); + const estCloseOut = useOrderCloseOut({ order, market, partyData }); + return ( +
+ + +
+
+ {SIDE_NAMES[order.side]} +
+
{market.tradableInstrument.instrument.product.quoteName}
+
+ {tagsData?.market?.tradableInstrument.instrument.metadata + .tags && ( + + )} +
+
+
+ @{' '} + {market.depth.lastTrade + ? addDecimal(market.depth.lastTrade.price, market.decimalPlaces) + : ' - '}{' '} + (EST) +
+
+ + <>{t('Est. margin')} +
+ {estMargin} + +
+
+ + <> + {t('Size')}{' '} +
+ ({market.tradableInstrument.instrument.product.quoteName}) +
+ +
+ {formatNumber(order.size, market.decimalPlaces)} + +
+
+ + <>{t('Est. close out')} +
+ {estCloseOut} + +
+
+
+ + +
+ ); +}; diff --git a/apps/simple-trading-app/src/app/components/deal-ticket/side-selector.tsx b/apps/simple-trading-app/src/app/components/deal-ticket/side-selector.tsx index 1c03d045d..292975695 100644 --- a/apps/simple-trading-app/src/app/components/deal-ticket/side-selector.tsx +++ b/apps/simple-trading-app/src/app/components/deal-ticket/side-selector.tsx @@ -29,6 +29,7 @@ export default ({ value, onSelect }: SideSelectorProps) => { variant="inline-link" aria-label={t('Open long position')} className={classNames( + 'py-8', 'buyButton hover:buyButton dark:buyButtonDark dark:hover:buyButtonDark', { selected: value === VegaWalletOrderSide.Buy } )} @@ -40,6 +41,7 @@ export default ({ value, onSelect }: SideSelectorProps) => { variant="inline-link" aria-label={t('Open short position')} className={classNames( + 'py-8', 'sellButton hover:sellButton dark:sellButtonDark dark:hover:sellButtonDark', { selected: value === VegaWalletOrderSide.Sell } )} diff --git a/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-expires.spec.tsx b/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-expires.spec.tsx index a3da535f6..ed08fcbda 100644 --- a/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-expires.spec.tsx +++ b/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-expires.spec.tsx @@ -23,7 +23,7 @@ describe('SimpleMarketExpires', () => { 'settlement-date:2022-04-25T1200', ]; render(); - expect(screen.getByText('April 25')).toBeInTheDocument(); + expect(screen.getByText('Apr 25')).toBeInTheDocument(); }); it('last one proper tag should matter', () => { @@ -33,7 +33,7 @@ describe('SimpleMarketExpires', () => { 'settlement-expiry-date:2022-03-25T12:00:00', ]; render(); - expect(screen.getByText('March 25')).toBeInTheDocument(); + expect(screen.getByText('Mar 25')).toBeInTheDocument(); }); it('when no proper tag nor date should be null', () => { diff --git a/apps/simple-trading-app/src/app/constants/index.ts b/apps/simple-trading-app/src/app/constants/index.ts index 7de82e7f5..30885f110 100644 --- a/apps/simple-trading-app/src/app/constants/index.ts +++ b/apps/simple-trading-app/src/app/constants/index.ts @@ -1,7 +1,7 @@ import type { SimpleMarkets_markets } from '../components/simple-market-list/__generated__/SimpleMarkets'; export const DATE_FORMAT = 'dd MMMM yyyy HH:mm'; -export const EXPIRE_DATE_FORMAT = 'MMMM dd'; +export const EXPIRE_DATE_FORMAT = 'MMM dd'; export const TRADABLE_STATES = { Active: true, diff --git a/apps/simple-trading-app/src/app/hooks/__generated__/estimateOrder.ts b/apps/simple-trading-app/src/app/hooks/__generated__/estimateOrder.ts new file mode 100644 index 000000000..108368d2e --- /dev/null +++ b/apps/simple-trading-app/src/app/hooks/__generated__/estimateOrder.ts @@ -0,0 +1,48 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { Side, OrderTimeInForce, OrderType } from "@vegaprotocol/types"; + +// ==================================================== +// GraphQL query operation: EstimateOrder +// ==================================================== + +export interface EstimateOrder_estimateOrder_marginLevels { + __typename: "MarginLevels"; + /** + * this is the minimal margin required for a party to place a new order on the network (unsigned int actually) + */ + initialLevel: string; +} + +export interface EstimateOrder_estimateOrder { + __typename: "OrderEstimate"; + /** + * The total estimated amount of fee if the order was to trade + */ + totalFeeAmount: string; + /** + * The margin requirement for this order + */ + marginLevels: EstimateOrder_estimateOrder_marginLevels; +} + +export interface EstimateOrder { + /** + * return an estimation of the potential cost for a new order + */ + estimateOrder: EstimateOrder_estimateOrder; +} + +export interface EstimateOrderVariables { + marketId: string; + partyId: string; + price?: string | null; + size: string; + side: Side; + timeInForce: OrderTimeInForce; + expiration?: string | null; + type: OrderType; +} diff --git a/apps/simple-trading-app/src/app/hooks/__generated__/marketPositions.ts b/apps/simple-trading-app/src/app/hooks/__generated__/marketPositions.ts new file mode 100644 index 000000000..4f2426fec --- /dev/null +++ b/apps/simple-trading-app/src/app/hooks/__generated__/marketPositions.ts @@ -0,0 +1,76 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: MarketPositions +// ==================================================== + +export interface MarketPositions_party_positionsConnection_edges_node_market_accounts { + __typename: "Account"; + /** + * Balance as string - current account balance (approx. as balances can be updated several times per second) + */ + balance: string; +} + +export interface MarketPositions_party_positionsConnection_edges_node_market { + __typename: "Market"; + /** + * Market ID + */ + id: string; + /** + * Get account for a party or market + */ + accounts: MarketPositions_party_positionsConnection_edges_node_market_accounts[] | null; +} + +export interface MarketPositions_party_positionsConnection_edges_node { + __typename: "Position"; + /** + * Open volume (uint64) + */ + openVolume: string; + /** + * Market relating to this position + */ + market: MarketPositions_party_positionsConnection_edges_node_market; +} + +export interface MarketPositions_party_positionsConnection_edges { + __typename: "PositionEdge"; + node: MarketPositions_party_positionsConnection_edges_node; +} + +export interface MarketPositions_party_positionsConnection { + __typename: "PositionConnection"; + /** + * The positions in this connection + */ + edges: MarketPositions_party_positionsConnection_edges[] | null; +} + +export interface MarketPositions_party { + __typename: "Party"; + /** + * Party identifier + */ + id: string; + /** + * Trading positions relating to a party + */ + positionsConnection: MarketPositions_party_positionsConnection; +} + +export interface MarketPositions { + /** + * An entity that is trading on the VEGA network + */ + party: MarketPositions_party | null; +} + +export interface MarketPositionsVariables { + partyId: string; +} diff --git a/apps/simple-trading-app/src/app/hooks/use-market-positions.ts b/apps/simple-trading-app/src/app/hooks/use-market-positions.ts new file mode 100644 index 000000000..f80c1cc83 --- /dev/null +++ b/apps/simple-trading-app/src/app/hooks/use-market-positions.ts @@ -0,0 +1,70 @@ +import { gql, useQuery } from '@apollo/client'; +import { BigNumber } from 'bignumber.js'; +import type { + MarketPositions, + MarketPositionsVariables, +} from './__generated__/marketPositions'; + +const MARKET_POSITIONS_QUERY = gql` + query MarketPositions($partyId: ID!) { + party(id: $partyId) { + id + positionsConnection { + edges { + node { + openVolume + market { + id + accounts { + balance + } + } + } + } + } + } + } +`; + +interface Props { + marketId: string; + partyId: string; +} + +type PositionMargin = { + openVolume: BigNumber; + balanceSum: BigNumber; +} | null; + +export default ({ marketId, partyId }: Props): PositionMargin => { + const { data } = useQuery( + MARKET_POSITIONS_QUERY, + { + pollInterval: 15000, + variables: { partyId }, + skip: !partyId, + } + ); + + const markets = + data?.party?.positionsConnection?.edges + ?.filter((nodes) => nodes.node.market.id === marketId) + .map((nodes) => nodes.node) || []; + + return markets.length + ? markets.reduce( + (agg, item) => { + const balance = item.market.accounts?.reduce( + (acagg, account) => acagg.plus(account.balance || 0), + new BigNumber(0) + ); + if (balance) { + agg.balanceSum = agg.balanceSum.plus(balance); + agg.openVolume = agg.openVolume.plus(item.openVolume); + } + return agg; + }, + { openVolume: new BigNumber(0), balanceSum: new BigNumber(0) } + ) + : null; +}; diff --git a/apps/simple-trading-app/src/app/hooks/use-order-closeout.ts b/apps/simple-trading-app/src/app/hooks/use-order-closeout.ts new file mode 100644 index 000000000..aade7e728 --- /dev/null +++ b/apps/simple-trading-app/src/app/hooks/use-order-closeout.ts @@ -0,0 +1,39 @@ +import { BigNumber } from 'bignumber.js'; +import type { Order } from '@vegaprotocol/orders'; +import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket'; +import type { PartyBalanceQuery } from '../components/deal-ticket/__generated__/PartyBalanceQuery'; +import { useSettlementAccount } from './use-settlement-account'; +import { VegaWalletOrderSide } from '@vegaprotocol/wallet'; +import { addDecimal, formatNumber } from '@vegaprotocol/react-helpers'; + +interface Props { + order: Order; + market: DealTicketQuery_market; + partyData?: PartyBalanceQuery; +} + +const useOrderCloseOut = ({ order, market, partyData }: Props): string => { + const account = useSettlementAccount( + market.tradableInstrument.instrument.product.settlementAsset.id, + partyData?.party?.accounts || [] + ); + if (account?.balance && market.depth.lastTrade) { + const price = new BigNumber( + addDecimal(market.depth.lastTrade.price, market.decimalPlaces) + ); + const balance = new BigNumber( + addDecimal(account.balance, account.asset.decimals) + ); + const { size, side } = order; + const bigOne = new BigNumber(1); + return formatNumber( + side === VegaWalletOrderSide.Buy + ? bigOne.minus(balance.div(price.times(size))).times(price) + : bigOne.plus(balance.div(price.times(size))).times(price), + market.decimalPlaces + ); + } + return ' - '; +}; + +export default useOrderCloseOut; diff --git a/apps/simple-trading-app/src/app/hooks/use-order-margin.ts b/apps/simple-trading-app/src/app/hooks/use-order-margin.ts new file mode 100644 index 000000000..81f5073f9 --- /dev/null +++ b/apps/simple-trading-app/src/app/hooks/use-order-margin.ts @@ -0,0 +1,112 @@ +import type { Order } from '@vegaprotocol/orders'; +import { gql, useQuery } from '@apollo/client'; +import type { + EstimateOrder, + EstimateOrderVariables, +} from './__generated__/estimateOrder'; +import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket'; +import { OrderTimeInForce, OrderType, Side } from '@vegaprotocol/types'; +import { + VegaWalletOrderSide, + VegaWalletOrderTimeInForce, + VegaWalletOrderType, +} from '@vegaprotocol/wallet'; +import { addDecimal, formatNumber } from '@vegaprotocol/react-helpers'; +import useMarketPositions from './use-market-positions'; +import { BigNumber } from 'bignumber.js'; + +export const ESTIMATE_ORDER_QUERY = gql` + query EstimateOrder( + $marketId: ID! + $partyId: ID! + $price: String + $size: String! + $side: Side! + $timeInForce: OrderTimeInForce! + $expiration: String + $type: OrderType! + ) { + estimateOrder( + marketId: $marketId + partyId: $partyId + price: $price + size: $size + side: $side + timeInForce: $timeInForce + expiration: $expiration + type: $type + ) { + totalFeeAmount + marginLevels { + initialLevel + } + } + } +`; + +interface Props { + order: Order; + market: DealTicketQuery_market; + partyId: string; +} + +const times: Record = { + [VegaWalletOrderTimeInForce.GTC]: OrderTimeInForce.GTC, + [VegaWalletOrderTimeInForce.GTT]: OrderTimeInForce.GTT, + [VegaWalletOrderTimeInForce.IOC]: OrderTimeInForce.IOC, + [VegaWalletOrderTimeInForce.FOK]: OrderTimeInForce.FOK, + [VegaWalletOrderTimeInForce.GFN]: OrderTimeInForce.GFN, + [VegaWalletOrderTimeInForce.GFA]: OrderTimeInForce.GFA, +}; + +const types: Record = { + [VegaWalletOrderType.Market]: OrderType.Market, + [VegaWalletOrderType.Limit]: OrderType.Limit, +}; + +const useOrderMargin = ({ order, market, partyId }: Props) => { + const marketPositions = useMarketPositions({ marketId: market.id, partyId }); + console.log('marketPositions', marketPositions); + console.log( + 'marketPositions.openVolume', + marketPositions?.openVolume.toNumber() + ); + console.log( + 'marketPositions.balanceSum', + marketPositions?.balanceSum.toNumber() + ); + const { data } = useQuery( + ESTIMATE_ORDER_QUERY, + { + variables: { + marketId: market.id, + partyId, + price: market.depth.lastTrade?.price, + size: order.size + (marketPositions?.openVolume.toNumber() || 0), + side: order.side === VegaWalletOrderSide.Buy ? Side.Buy : Side.Sell, + timeInForce: times[order.timeInForce], + type: types[order.type], + }, + skip: !partyId || !market.id || !order.size, + } + ); + if (data?.estimateOrder.marginLevels.initialLevel) { + return formatNumber( + Math.max( + 0, + new BigNumber( + addDecimal( + data.estimateOrder.marginLevels.initialLevel, + market.decimalPlaces + ) + ) + .minus(marketPositions?.balanceSum.toNumber() || 0) + .toNumber() + ), + market.decimalPlaces + ); + } + return ' - '; +}; + +export default useOrderMargin; diff --git a/libs/deal-ticket/src/components/deal-ticket-manager.tsx b/libs/deal-ticket/src/components/deal-ticket-manager.tsx index 76dbd130d..01d0d2e86 100644 --- a/libs/deal-ticket/src/components/deal-ticket-manager.tsx +++ b/libs/deal-ticket/src/components/deal-ticket-manager.tsx @@ -44,7 +44,7 @@ export const DealTicketManager = ({ ); }; -const getDialogTitle = (status?: OrderStatus): string | undefined => { +export const getDialogTitle = (status?: OrderStatus): string | undefined => { if (!status) { return; } @@ -63,7 +63,7 @@ const getDialogTitle = (status?: OrderStatus): string | undefined => { } }; -const getDialogIntent = (status?: OrderStatus): Intent | undefined => { +export const getDialogIntent = (status?: OrderStatus): Intent | undefined => { if (!status) { return; } @@ -81,7 +81,7 @@ const getDialogIntent = (status?: OrderStatus): Intent | undefined => { } }; -const getDialogIcon = (status?: OrderStatus): ReactNode | undefined => { +export const getDialogIcon = (status?: OrderStatus): ReactNode | undefined => { if (!status) { return; } diff --git a/libs/tailwindcss-config/src/vega-custom-classes-lite.js b/libs/tailwindcss-config/src/vega-custom-classes-lite.js index 470774b1b..0e8a38a82 100644 --- a/libs/tailwindcss-config/src/vega-custom-classes-lite.js +++ b/libs/tailwindcss-config/src/vega-custom-classes-lite.js @@ -45,8 +45,6 @@ const vegaCustomClassesLite = plugin(function ({ addUtilities }) { backgroundColor: 'rgba(0, 143, 74, 0.1)', border: `1px solid ${theme.colors.darkerGreen}`, color: theme.colors.darkerGreen, - paddingTop: '0.5rem', - paddingBottom: '0.5rem', '&:hover': { backgroundColor: theme.colors.darkerGreen, color: theme.colors.white.DEFAULT, @@ -68,8 +66,6 @@ const vegaCustomClassesLite = plugin(function ({ addUtilities }) { '.sellButton': { textTransform: 'uppercase', textDecoration: 'none', - paddingTop: '0.5rem', - paddingBottom: '0.5rem', backgroundColor: 'rgba(255, 8, 126, 0.1)', border: `1px solid ${theme.colors.pink}`, color: theme.colors.pink,