diff --git a/.github/workflows/capsule-cypress-manual-trigger.yml b/.github/workflows/capsule-cypress-manual-trigger.yml index 5856baa6a..da45e8d33 100644 --- a/.github/workflows/capsule-cypress-manual-trigger.yml +++ b/.github/workflows/capsule-cypress-manual-trigger.yml @@ -76,7 +76,7 @@ jobs: uses: actions/checkout@v2 with: repository: vegaprotocol/vega - ref: develop + ref: tm35.9 token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }} path: './vega' diff --git a/.github/workflows/capsule-cypress-nightly.yml b/.github/workflows/capsule-cypress-nightly.yml index ab9de0041..c13468c35 100644 --- a/.github/workflows/capsule-cypress-nightly.yml +++ b/.github/workflows/capsule-cypress-nightly.yml @@ -60,7 +60,7 @@ jobs: uses: actions/checkout@v2 with: repository: vegaprotocol/vega - ref: develop + ref: tm35.9 token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }} path: './vega' diff --git a/.github/workflows/capsule-cypress.yml b/.github/workflows/capsule-cypress.yml index ee233b9bd..55f665da4 100644 --- a/.github/workflows/capsule-cypress.yml +++ b/.github/workflows/capsule-cypress.yml @@ -62,7 +62,7 @@ jobs: uses: actions/checkout@v2 with: repository: vegaprotocol/vega - ref: develop + ref: tm35.9 token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }} path: './vega' diff --git a/apps/static/src/assets/mainnet-tranches.json b/apps/static/src/assets/mainnet-tranches.json index 291cf9750..46c380540 100644 --- a/apps/static/src/assets/mainnet-tranches.json +++ b/apps/static/src/assets/mainnet-tranches.json @@ -71,7 +71,7 @@ "tranche_end": "2023-12-05T00:00:00.000Z", "total_added": "129999.45", "total_removed": "0", - "locked_amount": "117840.572930536554817215", + "locked_amount": "117781.42860621642597834", "deposits": [ { "amount": "129999.45", @@ -521,7 +521,7 @@ "tranche_end": "2023-04-05T00:00:00.000Z", "total_added": "97499.58", "total_removed": "0", - "locked_amount": "58812.826321860914641464", + "locked_amount": "58754.811249245894970078", "deposits": [ { "amount": "97499.58", @@ -554,7 +554,7 @@ "tranche_end": "2023-04-05T00:00:00.000Z", "total_added": "135173.4239508", "total_removed": "0", - "locked_amount": "80386.97997110440758523282236", + "locked_amount": "80307.68338272410733775343328", "deposits": [ { "amount": "135173.4239508", @@ -587,7 +587,7 @@ "tranche_end": "2023-04-05T00:00:00.000Z", "total_added": "32499.86", "total_removed": "0", - "locked_amount": "24741.540390421406951588", + "locked_amount": "24717.13445123898281348", "deposits": [ { "amount": "32499.86", @@ -620,7 +620,7 @@ "tranche_end": "2023-04-05T00:00:00.000Z", "total_added": "10833.29", "total_removed": "0", - "locked_amount": "8053.131310857162538307", + "locked_amount": "8045.18741448291105483", "deposits": [ { "amount": "10833.29", @@ -728,7 +728,7 @@ "tranche_end": "2022-11-01T00:00:00.000Z", "total_added": "22500", "total_removed": "0", - "locked_amount": "11952.567368659422", + "locked_amount": "11922.08021965579725", "deposits": [ { "amount": "15000", @@ -814,7 +814,7 @@ "tranche_end": "2023-06-02T00:00:00.000Z", "total_added": "1939928.38", "total_removed": "179856.049568108351", - "locked_amount": "1651572.322335181521838498", + "locked_amount": "1650247.233635486323584786", "deposits": [ { "amount": "1852091.69", @@ -1889,7 +1889,7 @@ "tranche_end": "2022-09-30T00:00:00.000Z", "total_added": "60916.66666633337", "total_removed": "19238.601152747179372649", - "locked_amount": "10269.2129282834758842594571179728", + "locked_amount": "10230.270445001801107383171081515", "deposits": [ { "amount": "2833.333333", @@ -5472,7 +5472,7 @@ "tranche_end": "2022-09-03T00:00:00.000Z", "total_added": "24994.000000000000000003", "total_removed": "8288.51201225622", - "locked_amount": "2653.16033625063463650031845566971080675", + "locked_amount": "2636.08791888635262012031640648782343994", "deposits": [ { "amount": "2", @@ -16340,7 +16340,7 @@ "tranche_end": "2023-06-05T00:00:00.000Z", "total_added": "3732368.4671", "total_removed": "144843.5352821052414", - "locked_amount": "2562392.9677488748404815744", + "locked_amount": "2560356.7677643286045575861", "deposits": [ { "amount": "1998.95815", @@ -17085,8 +17085,8 @@ "tranche_start": "2022-06-05T00:00:00.000Z", "tranche_end": "2023-12-05T00:00:00.000Z", "total_added": "15788853.065470999700000001", - "total_removed": "67165.84665031243008", - "locked_amount": "14312118.1755089040001202814589791706839687", + "total_removed": "67542.001079865509505", + "locked_amount": "14304934.9063000952150225143657484624182212", "deposits": [ { "amount": "16249.93", @@ -17585,6 +17585,11 @@ "user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b", "tx": "0x0793c3710f6544750cb143acaba9589f8ffd99dc4687a48d52bb2f2cad354063" }, + { + "amount": "376.154429553079425", + "user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b", + "tx": "0x4303f2bca5e794329a9dd8b34e0bf4f29234f1fa5a920c48a8203b62529aba13" + }, { "amount": "325.1056930428465", "user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b", @@ -17852,6 +17857,12 @@ "tranche_id": 2, "tx": "0x0793c3710f6544750cb143acaba9589f8ffd99dc4687a48d52bb2f2cad354063" }, + { + "amount": "376.154429553079425", + "user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b", + "tranche_id": 2, + "tx": "0x4303f2bca5e794329a9dd8b34e0bf4f29234f1fa5a920c48a8203b62529aba13" + }, { "amount": "325.1056930428465", "user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b", @@ -18076,8 +18087,8 @@ } ], "total_tokens": "194999.1675", - "withdrawn_tokens": "17928.2581854017484", - "remaining_tokens": "177070.9093145982516" + "withdrawn_tokens": "18304.412614954827825", + "remaining_tokens": "176694.754885045172175" }, { "address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c", @@ -19516,8 +19527,8 @@ "tranche_start": "2021-11-05T00:00:00.000Z", "tranche_end": "2023-05-05T00:00:00.000Z", "total_added": "14597706.0446472999", - "total_removed": "2420741.791196523902160047", - "locked_amount": "7559404.42469080867089369345769758", + "total_removed": "2421946.596295493754033047", + "locked_amount": "7552738.75038636143539133718743172", "deposits": [ { "amount": "129284.449", @@ -19731,6 +19742,11 @@ "user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b", "tx": "0xf442b7c239f587c91f7b8eaef820290f1345f0e782edae48a409436e4e7d0602" }, + { + "amount": "1204.805098969851873", + "user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b", + "tx": "0x20171309054732628e1b86149642280cde77c3318a2ef37ec6f4a40ff8126228" + }, { "amount": "600.79246609513746875", "user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b", @@ -21563,6 +21579,12 @@ "tranche_id": 3, "tx": "0xf442b7c239f587c91f7b8eaef820290f1345f0e782edae48a409436e4e7d0602" }, + { + "amount": "1204.805098969851873", + "user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b", + "tranche_id": 3, + "tx": "0x20171309054732628e1b86149642280cde77c3318a2ef37ec6f4a40ff8126228" + }, { "amount": "600.79246609513746875", "user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b", @@ -23029,8 +23051,8 @@ } ], "total_tokens": "359123.469575", - "withdrawn_tokens": "171998.81424794535946375", - "remaining_tokens": "187124.65532705464053625" + "withdrawn_tokens": "173203.61934691521133675", + "remaining_tokens": "185919.85022808478866325" }, { "address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB", @@ -24094,7 +24116,7 @@ "tranche_end": "2023-04-05T00:00:00.000Z", "total_added": "5778205.3912159303", "total_removed": "1580907.252363731354286516", - "locked_amount": "2669863.003552475928351608462877838", + "locked_amount": "2667229.35532112569565824029632815", "deposits": [ { "amount": "552496.6455", @@ -25338,7 +25360,7 @@ "tranche_end": "2023-06-05T00:00:00.000Z", "total_added": "472355.6199999996", "total_removed": "580.0445766491", - "locked_amount": "406025.81300246413755963702486048", + "locked_amount": "405703.16547549753099609727650936", "deposits": [ { "amount": "3000", diff --git a/apps/static/src/assets/stagnet1-tranches.json b/apps/static/src/assets/stagnet1-tranches.json index d90cbebd6..51a28955f 100644 --- a/apps/static/src/assets/stagnet1-tranches.json +++ b/apps/static/src/assets/stagnet1-tranches.json @@ -38,7 +38,7 @@ "tranche_end": "2022-11-26T13:48:10.000Z", "total_added": "100", "total_removed": "0", - "locked_amount": "33.786475773718927", + "locked_amount": "33.7181665398275", "deposits": [ { "amount": "100", @@ -242,7 +242,7 @@ "tranche_end": "2022-10-12T00:53:20.000Z", "total_added": "1100", "total_removed": "673.04388635", - "locked_amount": "234.41318810248603", + "locked_amount": "233.661786529680311", "deposits": [ { "amount": "1000", diff --git a/apps/static/src/assets/testnet-tranches.json b/apps/static/src/assets/testnet-tranches.json index 49b5130bf..0cde69f5a 100644 --- a/apps/static/src/assets/testnet-tranches.json +++ b/apps/static/src/assets/testnet-tranches.json @@ -69,7 +69,7 @@ "tranche_end": "2022-10-12T00:53:20.000Z", "total_added": "1010.000000000000000001", "total_removed": "668.4622323651", - "locked_amount": "215.2339272577371730002131028982749873", + "locked_amount": "214.54400399543374010021241980593607301", "deposits": [ { "amount": "1000", diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts index 6f20e96ec..f467d0652 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts @@ -28,13 +28,6 @@ const mockTx = { }; describe('deal ticket orders', () => { - const orderSizeField = 'order-size'; - const orderPriceField = 'order-price'; - const orderTIFDropDown = 'order-tif'; - const placeOrderBtn = 'place-order'; - const orderStatusHeader = 'order-status-header'; - const orderTransactionHash = 'tx-block-explorer'; - before(() => { cy.mockGQL((req) => { mockTradingPage(req, MarketState.Active); @@ -107,7 +100,15 @@ describe('deal ticket orders', () => { }); const testOrder = (order: Order, expected?: Partial) => { + const orderSizeField = 'order-size'; + const orderPriceField = 'order-price'; + const orderTIFDropDown = 'order-tif'; + const placeOrderBtn = 'place-order'; + const dialogTitle = 'dialog-title'; + const orderTransactionHash = 'tx-block-explorer'; + const { type, side, size, price, timeInForce, expiresAt } = order; + cy.get(`[name="order-type"][value="${type}"`).click({ force: true }); // force as input is hidden and displayed as a button cy.get(`[name="order-side"][value="${side}"`).click({ force: true }); cy.getByTestId(orderSizeField).clear().type(size); @@ -139,7 +140,7 @@ describe('deal ticket orders', () => { ...expectedOrder, }, }); - cy.getByTestId(orderStatusHeader).should( + cy.getByTestId(dialogTitle).should( 'have.text', 'Awaiting network confirmation' ); diff --git a/apps/trading-e2e/src/support/mocks/generate-market-list.ts b/apps/trading-e2e/src/support/mocks/generate-market-list.ts index a449ce274..e7055a737 100644 --- a/apps/trading-e2e/src/support/mocks/generate-market-list.ts +++ b/apps/trading-e2e/src/support/mocks/generate-market-list.ts @@ -1,6 +1,7 @@ import merge from 'lodash/merge'; import type { PartialDeep } from 'type-fest'; import type { MarketList, MarketList_markets } from '@vegaprotocol/market-list'; +import { MarketState, MarketTradingMode } from '@vegaprotocol/types'; export const generateMarketList = ( override?: PartialDeep @@ -9,6 +10,8 @@ export const generateMarketList = ( { id: 'market-0', decimalPlaces: 5, + state: MarketState.Active, + tradingMode: MarketTradingMode.Continuous, data: { market: { id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35', @@ -40,6 +43,8 @@ export const generateMarketList = ( { id: 'market-1', decimalPlaces: 2, + state: MarketState.Suspended, + tradingMode: MarketTradingMode.MonitoringAuction, data: { market: { id: '34d95e10faa00c21d19d382d6d7e6fc9722a96985369f0caec041b0f44b775ed', diff --git a/apps/trading-e2e/src/support/mocks/generate-market.ts b/apps/trading-e2e/src/support/mocks/generate-market.ts index f8905a491..91a9d4b79 100644 --- a/apps/trading-e2e/src/support/mocks/generate-market.ts +++ b/apps/trading-e2e/src/support/mocks/generate-market.ts @@ -1,5 +1,9 @@ import merge from 'lodash/merge'; -import { MarketState, MarketTradingMode } from '@vegaprotocol/types'; +import { + AuctionTrigger, + MarketState, + MarketTradingMode, +} from '@vegaprotocol/types'; import type { PartialDeep } from 'type-fest'; // eslint-disable-next-line @nrwl/nx/enforce-module-boundaries import type { Market } from '../../../../trading/pages/markets/__generated__/Market'; @@ -12,6 +16,7 @@ export const generateMarket = (override?: PartialDeep): Market => { tradingMode: MarketTradingMode.Continuous, state: MarketState.Active, decimalPlaces: 5, + positionDecimalPlaces: 0, data: { market: { id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35', @@ -23,6 +28,7 @@ export const generateMarket = (override?: PartialDeep): Market => { bestOfferVolume: '100', bestStaticBidVolume: '482', bestStaticOfferVolume: '2188', + trigger: AuctionTrigger.Unspecified, __typename: 'MarketData', }, tradableInstrument: { @@ -40,6 +46,16 @@ export const generateMarket = (override?: PartialDeep): Market => { ], __typename: 'InstrumentMetadata', }, + product: { + quoteName: 'BTCUSD Monthly', + settlementAsset: { + id: '000', + symbol: 'USD', + name: 'United States Dollar', + __typename: 'Asset', + }, + __typename: 'Future', + }, __typename: 'Instrument', }, __typename: 'TradableInstrument', diff --git a/apps/trading-e2e/src/support/mocks/generate-markets.ts b/apps/trading-e2e/src/support/mocks/generate-markets.ts index 21e358fd8..8c4d4653d 100644 --- a/apps/trading-e2e/src/support/mocks/generate-markets.ts +++ b/apps/trading-e2e/src/support/mocks/generate-markets.ts @@ -1,5 +1,9 @@ import merge from 'lodash/merge'; -import { MarketState, MarketTradingMode } from '@vegaprotocol/types'; +import { + AuctionTrigger, + MarketState, + MarketTradingMode, +} from '@vegaprotocol/types'; import type { PartialDeep } from 'type-fest'; import type { Markets, Markets_markets } from '@vegaprotocol/market-list'; @@ -19,6 +23,7 @@ export const generateMarkets = (override?: PartialDeep): Markets => { bestBidPrice: '0', bestOfferPrice: '0', markPrice: '4612690058', + trigger: AuctionTrigger.Unspecified, __typename: 'MarketData', }, tradableInstrument: { @@ -51,6 +56,7 @@ export const generateMarkets = (override?: PartialDeep): Markets => { bestBidPrice: '0', bestOfferPrice: '0', markPrice: '8441', + trigger: AuctionTrigger.Unspecified, __typename: 'MarketData', }, tradableInstrument: { diff --git a/libs/deal-ticket/src/components/__generated__/MarketInfoQuery.ts b/libs/deal-ticket/src/components/__generated__/MarketInfoQuery.ts index a029d366f..f9383bb90 100644 --- a/libs/deal-ticket/src/components/__generated__/MarketInfoQuery.ts +++ b/libs/deal-ticket/src/components/__generated__/MarketInfoQuery.ts @@ -368,14 +368,14 @@ export interface MarketInfoQuery_market { /** * decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct * number denominated in the currency of the Market. (uint64) - * + * * Examples: * Currency Balance decimalPlaces Real Balance * GBP 100 0 GBP 100 * GBP 100 2 GBP 1.00 * GBP 100 4 GBP 0.01 * GBP 1 4 GBP 0.0001 ( 0.01p ) - * + * * GBX (pence) 100 0 GBP 1.00 (100p ) * GBX (pence) 100 2 GBP 0.01 ( 1p ) * GBX (pence) 100 4 GBP 0.0001 ( 0.01p ) diff --git a/libs/deal-ticket/src/components/deal-ticket-manager.tsx b/libs/deal-ticket/src/components/deal-ticket-manager.tsx index b202f6c6f..76dbd130d 100644 --- a/libs/deal-ticket/src/components/deal-ticket-manager.tsx +++ b/libs/deal-ticket/src/components/deal-ticket-manager.tsx @@ -1,10 +1,11 @@ import type { ReactNode } from 'react'; -import { useState } from 'react'; -import { VegaTransactionDialog, VegaTxStatus } from '@vegaprotocol/wallet'; +import { VegaTxStatus } from '@vegaprotocol/wallet'; import { DealTicket } from './deal-ticket'; import type { DealTicketQuery_market } from './__generated__/DealTicketQuery'; -import { useOrderSubmit } from '@vegaprotocol/orders'; +import { useOrderSubmit, OrderFeedback } from '@vegaprotocol/orders'; import { OrderStatus } from '@vegaprotocol/types'; +import { Icon, Intent } from '@vegaprotocol/ui-toolkit'; +import { t } from '@vegaprotocol/react-helpers'; export interface DealTicketManagerProps { market: DealTicketQuery_market; @@ -15,28 +16,15 @@ export const DealTicketManager = ({ market, children, }: DealTicketManagerProps) => { - const [orderDialogOpen, setOrderDialogOpen] = useState(false); - const { submit, transaction, finalizedOrder, reset } = useOrderSubmit(market); - const getDialogTitle = (status?: string) => { - switch (status) { - case OrderStatus.Active: - return 'Order submitted'; - case OrderStatus.Filled: - return 'Order filled'; - case OrderStatus.PartiallyFilled: - return 'Order partially filled'; - case OrderStatus.Parked: - return 'Order parked'; - default: - return 'Submission failed'; - } - }; + const { submit, transaction, finalizedOrder, TransactionDialog } = + useOrderSubmit(market); + return ( <> {children || ( submit(order)} transactionStatus={ transaction.status === VegaTxStatus.Requested || transaction.status === VegaTxStatus.Pending @@ -45,15 +33,68 @@ export const DealTicketManager = ({ } /> )} - + intent={getDialogIntent(finalizedOrder?.status)} + icon={getDialogIcon(finalizedOrder?.status)} + > + + ); }; + +const getDialogTitle = (status?: OrderStatus): string | undefined => { + if (!status) { + return; + } + + switch (status) { + case OrderStatus.Active: + return t('Order submitted'); + case OrderStatus.Filled: + return t('Order filled'); + case OrderStatus.PartiallyFilled: + return t('Order partially filled'); + case OrderStatus.Parked: + return t('Order parked'); + default: + return t('Submission failed'); + } +}; + +const getDialogIntent = (status?: OrderStatus): Intent | undefined => { + if (!status) { + return; + } + + switch (status) { + case OrderStatus.Parked: + case OrderStatus.Expired: + return Intent.Warning; + case OrderStatus.Rejected: + case OrderStatus.Stopped: + case OrderStatus.Cancelled: + return Intent.Danger; + default: + return; + } +}; + +const getDialogIcon = (status?: OrderStatus): ReactNode | undefined => { + if (!status) { + return; + } + + switch (status) { + case OrderStatus.Parked: + case OrderStatus.Expired: + return ; + case OrderStatus.Rejected: + case OrderStatus.Stopped: + case OrderStatus.Cancelled: + return ; + default: + return; + } +}; diff --git a/libs/orders/src/lib/components/index.ts b/libs/orders/src/lib/components/index.ts index dff9b29da..993d9042a 100644 --- a/libs/orders/src/lib/components/index.ts +++ b/libs/orders/src/lib/components/index.ts @@ -1,4 +1,6 @@ export * from './order-data-provider'; +export * from './order-feedback'; export * from './order-list'; export * from './order-list-manager'; export * from './order-list-container'; +export * from './mocks/generate-orders'; diff --git a/libs/orders/src/lib/components/mocks/generate-orders.ts b/libs/orders/src/lib/components/mocks/generate-orders.ts index b28b5eb40..3e41ee971 100644 --- a/libs/orders/src/lib/components/mocks/generate-orders.ts +++ b/libs/orders/src/lib/components/mocks/generate-orders.ts @@ -6,42 +6,42 @@ import { Side, } from '@vegaprotocol/types'; import type { Orders_party_ordersConnection_edges_node } from '../'; +import type { PartialDeep } from 'type-fest'; export const generateOrder = ( - partialOrder: Partial -) => - merge( - { - __typename: 'Order', - id: 'order-id2', - market: { - __typename: 'Market', - id: 'market-id', - name: 'market-name', - decimalPlaces: 2, - positionDecimalPlaces: 2, - tradableInstrument: { - __typename: 'TradableInstrument', - instrument: { - __typename: 'Instrument', - code: 'instrument-code', - }, + partialOrder?: PartialDeep +) => { + const order: Orders_party_ordersConnection_edges_node = { + __typename: 'Order', + id: 'order-id2', + market: { + __typename: 'Market', + id: 'market-id', + name: 'market-name', + decimalPlaces: 2, + positionDecimalPlaces: 2, + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + code: 'instrument-code', }, }, - size: '10', - type: OrderType.Market, - status: OrderStatus.Active, - side: Side.Buy, - remaining: '5', - price: '', - timeInForce: OrderTimeInForce.IOC, - createdAt: new Date().toISOString(), - updatedAt: null, - expiresAt: null, - rejectionReason: null, - } as Orders_party_ordersConnection_edges_node, - partialOrder - ); + }, + size: '10', + type: OrderType.Market, + status: OrderStatus.Active, + side: Side.Buy, + remaining: '5', + price: '', + timeInForce: OrderTimeInForce.IOC, + createdAt: new Date().toISOString(), + updatedAt: null, + expiresAt: null, + rejectionReason: null, + }; + return merge(order, partialOrder); +}; export const limitOrder = generateOrder({ id: 'limit-order', diff --git a/libs/orders/src/lib/components/order-feedback/index.ts b/libs/orders/src/lib/components/order-feedback/index.ts new file mode 100644 index 000000000..cfb0f1b36 --- /dev/null +++ b/libs/orders/src/lib/components/order-feedback/index.ts @@ -0,0 +1 @@ +export * from './order-feedback'; diff --git a/libs/orders/src/lib/components/order-feedback/order-feedback.spec.tsx b/libs/orders/src/lib/components/order-feedback/order-feedback.spec.tsx new file mode 100644 index 000000000..aeac88fff --- /dev/null +++ b/libs/orders/src/lib/components/order-feedback/order-feedback.spec.tsx @@ -0,0 +1,87 @@ +import { render, screen } from '@testing-library/react'; +import { formatLabel } from '@vegaprotocol/react-helpers'; +import { + OrderRejectionReason, + OrderStatus, + OrderType, + Side, +} from '@vegaprotocol/types'; +import { VegaTxStatus } from '@vegaprotocol/wallet'; +import { generateOrder } from '../mocks/generate-orders'; +import type { OrderFeedbackProps } from './order-feedback'; +import { OrderFeedback } from './order-feedback'; + +jest.mock('@vegaprotocol/environment', () => ({ + useEnvironment: () => ({ + VEGA_EXPLORER_URL: 'https://test.explorer.vega.network', + }), +})); + +describe('OrderFeedback', () => { + let props: OrderFeedbackProps; + + beforeEach(() => { + props = { + transaction: { + status: VegaTxStatus.Complete, + error: null, + txHash: 'tx-hash', + signature: null, + }, + order: null, + }; + }); + + it('renders null if no order provided', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it('renders error reason', () => { + const orderFields = { + status: OrderStatus.Rejected, + rejectionReason: OrderRejectionReason.OrderAmendFailure, + }; + const order = generateOrder(orderFields); + render(); + expect(screen.getByTestId('error-reason')).toHaveTextContent( + `Reason: ${formatLabel(orderFields.rejectionReason)}` + ); + }); + + it('should render order details when order is placed successfully', () => { + const order = generateOrder({ + type: OrderType.Limit, + price: '100', + size: '200', + side: Side.Buy, + market: { + decimalPlaces: 2, + positionDecimalPlaces: 0, + }, + }); + render(); + expect(screen.getByTestId('order-confirmed')).toBeInTheDocument(); + expect(screen.getByTestId('tx-block-explorer')).toHaveTextContent( + // eslint-disable-next-line + props.transaction.txHash! + ); + expect(screen.getByTestId('tx-block-explorer')).toHaveTextContent( + // eslint-disable-next-line + props.transaction.txHash! + ); + expect(screen.getByText('Market').nextElementSibling).toHaveTextContent( + // eslint-disable-next-line + order.market!.name + ); + expect(screen.getByText('Status').nextElementSibling).toHaveTextContent( + order.status + ); + expect(screen.getByText('Price').nextElementSibling).toHaveTextContent( + '1.00' + ); + expect(screen.getByText('Amount').nextElementSibling).toHaveTextContent( + `+ 200` + ); + }); +}); diff --git a/libs/orders/src/lib/components/order-feedback/order-feedback.tsx b/libs/orders/src/lib/components/order-feedback/order-feedback.tsx new file mode 100644 index 000000000..3ab9c483e --- /dev/null +++ b/libs/orders/src/lib/components/order-feedback/order-feedback.tsx @@ -0,0 +1,116 @@ +import { useEnvironment } from '@vegaprotocol/environment'; +import type { OrderEvent_busEvents_event_Order } from '../../order-hooks/__generated__'; +import { + addDecimalsFormatNumber, + formatLabel, + t, +} from '@vegaprotocol/react-helpers'; +import { OrderStatus, OrderType, Side } from '@vegaprotocol/types'; +import type { VegaTxState } from '@vegaprotocol/wallet'; + +export interface OrderFeedbackProps { + transaction: VegaTxState; + order: OrderEvent_busEvents_event_Order | null; +} + +export const OrderFeedback = ({ transaction, order }: OrderFeedbackProps) => { + const { VEGA_EXPLORER_URL } = useEnvironment(); + const labelClass = 'font-bold'; + if (!order) return null; + + // Order on network but was rejected + if (order.status === OrderStatus.Rejected) { + return ( +

+ {order.rejectionReason && + t(`Reason: ${formatLabel(order.rejectionReason)}`)} +

+ ); + } + + if (order.status === OrderStatus.Cancelled) { + return ( +
+
+ {order.market && ( +
+

{t(`Market`)}

+

{t(`${order.market.name}`)}

+
+ )} +
+
+ {transaction.txHash && ( +
+

{t('Transaction')}

+ + {transaction.txHash} + +
+ )} +
+
+ ); + } + + return ( +
+
+ {order.market && ( +
+

{t(`Market`)}

+

{t(`${order.market.name}`)}

+
+ )} +
+

{t(`Status`)}

+

{t(`${order.status}`)}

+
+ {order.type === OrderType.Limit && order.market && ( +
+

{t(`Price`)}

+

+ {addDecimalsFormatNumber(order.price, order.market.decimalPlaces)} +

+
+ )} +
+

{t(`Amount`)}

+

+ {`${order.side === Side.Buy ? '+' : '-'} ${addDecimalsFormatNumber( + order.size, + order.market?.positionDecimalPlaces ?? 0 + )} + `} +

+
+
+
+ {transaction.txHash && ( +
+

{t('Transaction')}

+ + {transaction.txHash} + +
+ )} +
+
+ ); +}; diff --git a/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx b/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx index 94bf5fd76..4e378bf8b 100644 --- a/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx +++ b/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx @@ -4,16 +4,22 @@ import { addDecimalsFormatNumber, } from '@vegaprotocol/react-helpers'; import { OrderType } from '@vegaprotocol/types'; -import { FormGroup, Input, InputError, Button } from '@vegaprotocol/ui-toolkit'; +import { + FormGroup, + Input, + InputError, + Button, + Dialog, + Icon, +} from '@vegaprotocol/ui-toolkit'; import { useForm } from 'react-hook-form'; -import Icon from 'react-syntax-highlighter'; -import { OrderDialogWrapper } from '@vegaprotocol/wallet'; -import type { Order } from '@vegaprotocol/wallet'; +import type { OrderFields } from '../order-data-provider'; interface OrderEditDialogProps { - title: string; - order: Order | null; - edit: (body: Order) => Promise; + isOpen: boolean; + onChange: (isOpen: boolean) => void; + order: OrderFields | null; + onSubmit: (fields: FormFields) => void; } interface FormFields { @@ -21,9 +27,10 @@ interface FormFields { } export const OrderEditDialog = ({ + isOpen, + onChange, order, - title, - edit, + onSubmit, }: OrderEditDialogProps) => { const headerClassName = 'text-h5 font-bold text-black dark:text-white'; const { @@ -37,9 +44,16 @@ export const OrderEditDialog = ({ : '', }, }); + if (!order) return null; + return ( - }> + } + >
{order.market && (
@@ -49,7 +63,7 @@ export const OrderEditDialog = ({ )} {order.type === OrderType.Limit && order.market && (
-

{t(`Last price`)}

+

{t(`Current price`)}

{addDecimalsFormatNumber(order.price, order.market.decimalPlaces)}

@@ -71,15 +85,7 @@ export const OrderEditDialog = ({
-
{ - await edit({ - ...order, - price: data.entryPrice, - }); - })} - data-testid="edit-order" - > +
- +
); }; diff --git a/libs/orders/src/lib/components/order-list/order-list.stories.tsx b/libs/orders/src/lib/components/order-list/order-list.stories.tsx index 8fbc6ad10..5853e3a85 100644 --- a/libs/orders/src/lib/components/order-list/order-list.stories.tsx +++ b/libs/orders/src/lib/components/order-list/order-list.stories.tsx @@ -1,10 +1,11 @@ import type { Story, Meta } from '@storybook/react'; -import { OrderType, OrderStatus, OrderTimeInForce } from '@vegaprotocol/types'; import { OrderList, OrderListTable } from './order-list'; import { useState } from 'react'; -import type { Order, VegaTxState } from '@vegaprotocol/wallet'; +import type { VegaTxState } from '@vegaprotocol/wallet'; import { VegaTransactionDialog, VegaTxStatus } from '@vegaprotocol/wallet'; import { generateOrdersArray } from '../mocks'; +import { OrderEditDialog } from './order-edit-dialog'; +import type { OrderFields } from '../order-data-provider'; export default { component: OrderList, @@ -18,9 +19,6 @@ const Template: Story = (args) => { { - return; - }} setEditOrder={() => { return; }} @@ -31,47 +29,43 @@ const Template: Story = (args) => { const Template2: Story = (args) => { const [open, setOpen] = useState(false); + const [editOrder, setEditOrder] = useState(null); const cancel = () => { setOpen(!open); return Promise.resolve(); }; const transaction: VegaTxState = { - status: VegaTxStatus.Default, + status: VegaTxStatus.Requested, error: null, txHash: null, signature: null, + dialogOpen: false, }; - const finalizedOrder: Order = { - status: OrderStatus.Cancelled, - rejectionReason: null, - size: '10', - price: '1000', - market: { name: 'ETH/DAI (30 Jun 2022)', decimalPlaces: 5 }, - type: OrderType.Limit, - timeInForce: OrderTimeInForce.GTC, - }; - const reset = () => null; return ( <>
{ - return; - }} - setEditOrder={() => { - return; + setEditOrder={(order) => { + setEditOrder(order); }} />
+ { + if (!isOpen) setEditOrder(null); + }} + order={editOrder} + onSubmit={(fields) => { + return; + }} /> ); 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 2593872ec..c3963e0bb 100644 --- a/libs/orders/src/lib/components/order-list/order-list.tsx +++ b/libs/orders/src/lib/components/order-list/order-list.tsx @@ -5,7 +5,11 @@ import { getDateTimeFormat, t, } from '@vegaprotocol/react-helpers'; -import { AgGridDynamic as AgGrid, Button } from '@vegaprotocol/ui-toolkit'; +import { + AgGridDynamic as AgGrid, + Button, + Intent, +} from '@vegaprotocol/ui-toolkit'; import type { ICellRendererParams, ValueFormatterParams, @@ -21,83 +25,61 @@ import type { Orders_party_ordersConnection_edges_node } from '../'; import BigNumber from 'bignumber.js'; import { useOrderCancel } from '../../order-hooks/use-order-cancel'; -import { VegaTransactionDialog } from '@vegaprotocol/wallet'; import { useOrderEdit } from '../../order-hooks/use-order-edit'; import { OrderEditDialog } from './order-edit-dialog'; +import type { OrderFields } from '../order-data-provider/__generated__'; +import { OrderFeedback } from '../order-feedback'; type OrderListProps = AgGridReactProps | AgReactUiProps; export const OrderList = forwardRef( (props, ref) => { - const [cancelOrderDialogOpen, setCancelOrderDialogOpen] = useState(false); - const [editOrderDialogOpen, setEditOrderDialogOpen] = useState(false); - const [editOrder, setEditOrder] = - useState(null); + const [editOrder, setEditOrder] = useState(null); + const orderCancel = useOrderCancel(); + const orderEdit = useOrderEdit(editOrder); - const { transaction, updatedOrder, reset, cancel } = useOrderCancel(); - const { - transaction: editTransaction, - updatedOrder: editedOrder, - reset: resetEdit, - edit, - } = useOrderEdit(); - const getCancelDialogTitle = (status?: string) => { - switch (status) { - case OrderStatus.Cancelled: - return 'Order cancelled'; - case OrderStatus.Rejected: - return 'Order rejected'; - case OrderStatus.Expired: - return 'Order expired'; - default: - return 'Cancellation failed'; - } - }; - const getEditDialogTitle = () => - editedOrder - ? t( - `Order ${ - editOrder?.market?.tradableInstrument.instrument.code ?? '' - } updated` - ) - : t( - `Edit ${ - editOrder?.market?.tradableInstrument.instrument.code ?? '' - } order` - ); return ( <> { + if (!order.market) return; + orderCancel.cancel({ + orderId: order.id, + marketId: order.market.id, + }); + }} ref={ref} - setEditOrderDialogOpen={setEditOrderDialogOpen} setEditOrder={setEditOrder} /> - - - - + + + + + { + if (!isOpen) setEditOrder(null); + }} + order={editOrder} + onSubmit={(fields) => { + setEditOrder(null); + orderEdit.edit({ price: fields.entryPrice }); + }} + /> ); } @@ -111,15 +93,12 @@ type OrderListTableValueFormatterParams = Omit< }; type OrderListTableProps = (AgGridReactProps | AgReactUiProps) & { - cancel: (body?: unknown) => Promise; - setEditOrderDialogOpen: (value: boolean) => void; - setEditOrder: ( - order: Orders_party_ordersConnection_edges_node | null - ) => void; + cancel: (order: OrderFields) => void; + setEditOrder: (order: OrderFields) => void; }; export const OrderListTable = forwardRef( - ({ cancel, setEditOrderDialogOpen, setEditOrder, ...props }, ref) => { + ({ cancel, setEditOrder, ...props }, ref) => { return ( ( { - if ( - ![ - OrderStatus.Cancelled, - OrderStatus.Rejected, - OrderStatus.Expired, - OrderStatus.Filled, - OrderStatus.Stopped, - ].includes(data.status) - ) { + if (!data) return null; + if (isOrderActive(data.status)) { return ( ); } + return null; }} /> { - if ( - ![ - OrderStatus.Cancelled, - OrderStatus.Rejected, - OrderStatus.Expired, - OrderStatus.Filled, - OrderStatus.Stopped, - ].includes(data.status) - ) { + if (!data) return null; + if (isOrderActive(data.status)) { return ( - ); } + return null; }} /> @@ -315,3 +276,61 @@ export const OrderListTable = forwardRef( ); } ); + +/** + * Check if an order is active to determine if it can be edited or cancelled + */ +const isOrderActive = (status: OrderStatus) => { + return ![ + OrderStatus.Cancelled, + OrderStatus.Rejected, + OrderStatus.Expired, + OrderStatus.Filled, + OrderStatus.Stopped, + ].includes(status); +}; + +const getEditDialogTitle = (status?: OrderStatus): string | undefined => { + if (!status) { + return; + } + + switch (status) { + case OrderStatus.Active: + return t('Order updated'); + case OrderStatus.Filled: + return t('Order filled'); + case OrderStatus.PartiallyFilled: + return t('Order partially filled'); + case OrderStatus.Parked: + return t('Order parked'); + default: + return t('Submission failed'); + } +}; + +const getCancelDialogIntent = (status?: OrderStatus): Intent | undefined => { + if (!status) { + return; + } + + switch (status) { + case OrderStatus.Cancelled: + return Intent.Success; + default: + return Intent.Danger; + } +}; + +const getCancelDialogTitle = (status?: OrderStatus): string | undefined => { + if (!status) { + return; + } + + switch (status) { + case OrderStatus.Cancelled: + return t('Order cancelled'); + default: + return t('Order cancellation failed'); + } +}; diff --git a/libs/orders/src/lib/index.ts b/libs/orders/src/lib/index.ts index c5b6ba5bb..35daed11d 100644 --- a/libs/orders/src/lib/index.ts +++ b/libs/orders/src/lib/index.ts @@ -1,4 +1,3 @@ export * from './components'; export * from './order-hooks'; export * from './utils'; -export * from './market'; diff --git a/libs/orders/src/lib/market.ts b/libs/orders/src/lib/market.ts deleted file mode 100644 index 381af1d46..000000000 --- a/libs/orders/src/lib/market.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import type { MarketState, MarketTradingMode } from '@vegaprotocol/types'; - -export interface Market { - __typename?: string; - id: string; - positionDecimalPlaces: number; - state: MarketState; - decimalPlaces: number; - tradingMode: MarketTradingMode; - tradableInstrument?: any; - depth?: any; -} diff --git a/libs/orders/src/lib/order-hooks/__generated__/OrderEvent.ts b/libs/orders/src/lib/order-hooks/__generated__/OrderEvent.ts index d633ec796..807c70496 100644 --- a/libs/orders/src/lib/order-hooks/__generated__/OrderEvent.ts +++ b/libs/orders/src/lib/order-hooks/__generated__/OrderEvent.ts @@ -40,6 +40,12 @@ export interface OrderEvent_busEvents_event_Order_market { * GBX (pence) 1 4 GBP 0.000001 ( 0.0001p) */ decimalPlaces: number; + /** + * positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64). + * i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes. + * 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market. + */ + positionDecimalPlaces: number; } export interface OrderEvent_busEvents_event_Order { @@ -76,6 +82,10 @@ export interface OrderEvent_busEvents_event_Order { * The timeInForce of order (determines how and if it executes, and whether it persists on the book) */ timeInForce: OrderTimeInForce; + /** + * Expiration time of this order (ISO-8601 RFC3339+Nano formatted date) + */ + expiresAt: string | null; /** * Whether the order is to buy or sell */ diff --git a/libs/orders/src/lib/order-hooks/order-event-query.ts b/libs/orders/src/lib/order-hooks/order-event-query.ts index 8b07cb111..33059f125 100644 --- a/libs/orders/src/lib/order-hooks/order-event-query.ts +++ b/libs/orders/src/lib/order-hooks/order-event-query.ts @@ -20,6 +20,7 @@ export const ORDER_EVENT_SUB = gql` id name decimalPlaces + positionDecimalPlaces } } } diff --git a/libs/orders/src/lib/order-hooks/use-order-cancel.spec.tsx b/libs/orders/src/lib/order-hooks/use-order-cancel.spec.tsx index 45584650b..3991f91f0 100644 --- a/libs/orders/src/lib/order-hooks/use-order-cancel.spec.tsx +++ b/libs/orders/src/lib/order-hooks/use-order-cancel.spec.tsx @@ -1,7 +1,6 @@ import type { MockedResponse } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing'; import { act, renderHook } from '@testing-library/react-hooks'; -import { MarketState, MarketTradingMode, OrderType } from '@vegaprotocol/types'; import type { ReactNode } from 'react'; import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet'; import type { @@ -15,33 +14,6 @@ import type { } from './__generated__/OrderEvent'; import { ORDER_EVENT_SUB } from './order-event-query'; -const defaultMarket = { - __typename: 'Market', - id: 'market-id', - decimalPlaces: 2, - positionDecimalPlaces: 1, - tradingMode: MarketTradingMode.Continuous, - state: MarketState.Active, - name: 'market-name', - tradableInstrument: { - __typename: 'TradableInstrument', - instrument: { - __typename: 'Instrument', - product: { - __typename: 'Future', - quoteName: 'quote-name', - }, - }, - }, - depth: { - __typename: 'MarketDepth', - lastTrade: { - __typename: 'Trade', - price: '100', - }, - }, -}; - const defaultWalletContext = { keypair: null, keypairs: [], @@ -147,23 +119,15 @@ describe('useOrderCancel', () => { expect(result.current.transaction.error).toEqual(null); }); - it('should not sendTx if no keypair', async () => { + it('should not sendTx if no keypair', () => { const mockSendTx = jest.fn(); - const order = { - type: OrderType.Market, - size: '10', - price: '1234567.89', - status: '', - rejectionReason: null, - market: defaultMarket, - }; const { result } = setup({ sendTx: mockSendTx, keypairs: [], keypair: null, }); - await act(async () => { - result.current.cancel(order); + act(() => { + result.current.cancel({ orderId: 'order-id', marketId: 'market-id' }); }); expect(mockSendTx).not.toHaveBeenCalled(); }); @@ -173,30 +137,24 @@ describe('useOrderCancel', () => { const keypair = { pub: '0x123', } as VegaKeyExtended; - const order = { - type: OrderType.Limit, - size: '10', - price: '1234567.89', - status: '', - rejectionReason: null, - market: defaultMarket, - }; const { result } = setup({ sendTx: mockSendTx, keypairs: [keypair], keypair, }); - await act(async () => { - result.current.cancel(order); + const args = { + orderId: 'order-id', + marketId: 'market-id', + }; + act(() => { + result.current.cancel(args); }); expect(mockSendTx).toHaveBeenCalledWith({ pubKey: keypair.pub, propagate: true, - orderCancellation: { - marketId: 'market-id', - }, + orderCancellation: args, }); }); }); diff --git a/libs/orders/src/lib/order-hooks/use-order-cancel.tsx b/libs/orders/src/lib/order-hooks/use-order-cancel.tsx index 12e5222f7..dd8439eeb 100644 --- a/libs/orders/src/lib/order-hooks/use-order-cancel.tsx +++ b/libs/orders/src/lib/order-hooks/use-order-cancel.tsx @@ -1,116 +1,68 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useState } from 'react'; import { useVegaWallet, useVegaTransaction } from '@vegaprotocol/wallet'; -import { useApolloClient } from '@apollo/client'; -import type { - OrderEvent, - OrderEventVariables, - OrderEvent_busEvents_event_Order, -} from './__generated__/OrderEvent'; -import { ORDER_EVENT_SUB } from './order-event-query'; +import type { OrderEvent_busEvents_event_Order } from './__generated__/OrderEvent'; import * as Sentry from '@sentry/react'; -import { OrderStatus } from '@vegaprotocol/types'; -import { determineId } from '@vegaprotocol/react-helpers'; -import type { Subscription } from 'zen-observable-ts'; +import { useOrderEvent } from './use-order-event'; + +interface CancelOrderArgs { + orderId: string; + marketId: string; +} export const useOrderCancel = () => { const { keypair } = useVegaWallet(); - const { send, transaction, reset: resetTransaction } = useVegaTransaction(); - const [updatedOrder, setUpdatedOrder] = - useState(null); - const client = useApolloClient(); - const subRef = useRef(null); + const waitForOrderEvent = useOrderEvent(); - useEffect(() => { - return () => { - subRef.current?.unsubscribe(); - setUpdatedOrder(null); - resetTransaction(); - }; - }, [resetTransaction]); + const [cancelledOrder, setCancelledOrder] = + useState(null); + + const { + send, + transaction, + reset: resetTransaction, + setComplete, + TransactionDialog, + } = useVegaTransaction(); const reset = useCallback(() => { resetTransaction(); - setUpdatedOrder(null); - subRef.current?.unsubscribe(); + setCancelledOrder(null); }, [resetTransaction]); const cancel = useCallback( - async (order) => { + async (args: CancelOrderArgs) => { if (!keypair) { return; } - if ( - [ - OrderStatus.Cancelled, - OrderStatus.Rejected, - OrderStatus.Expired, - OrderStatus.Filled, - OrderStatus.Stopped, - ].includes(order.status) - ) { - return; - } - - setUpdatedOrder(null); + setCancelledOrder(null); try { - const res = await send({ + await send({ pubKey: keypair.pub, propagate: true, orderCancellation: { - orderId: order.id, - marketId: order.market.id, + orderId: args.orderId, + marketId: args.marketId, }, }); - if (res?.signature) { - const resId = order.id ?? determineId(res.signature); - setUpdatedOrder(null); - - if (resId) { - // Start a subscription looking for the newly created order - subRef.current = client - .subscribe({ - query: ORDER_EVENT_SUB, - variables: { partyId: keypair?.pub || '' }, - }) - .subscribe(({ data }) => { - if (!data?.busEvents?.length) { - return; - } - - // No types available for the subscription result - const matchingOrderEvent = data.busEvents.find((e) => { - if (e.event.__typename !== 'Order') { - return false; - } - - return e.event.id === resId; - }); - - if ( - matchingOrderEvent && - matchingOrderEvent.event.__typename === 'Order' - ) { - setUpdatedOrder(matchingOrderEvent.event); - subRef.current?.unsubscribe(); - } - }); - } - } - return res; + waitForOrderEvent(args.orderId, keypair.pub, (cancelledOrder) => { + setCancelledOrder(cancelledOrder); + setComplete(); + }); } catch (e) { Sentry.captureException(e); return; } }, - [client, keypair, send] + [keypair, send, setComplete, waitForOrderEvent] ); return { transaction, - updatedOrder, + cancelledOrder, + TransactionDialog, cancel, reset, }; diff --git a/libs/orders/src/lib/order-hooks/use-order-edit.spec.tsx b/libs/orders/src/lib/order-hooks/use-order-edit.spec.tsx index 9676b7494..9f27edba1 100644 --- a/libs/orders/src/lib/order-hooks/use-order-edit.spec.tsx +++ b/libs/orders/src/lib/order-hooks/use-order-edit.spec.tsx @@ -3,11 +3,7 @@ import type { VegaKeyExtended, VegaWalletContextShape, } from '@vegaprotocol/wallet'; -import { - VegaWalletOrderSide, - VegaWalletOrderTimeInForce, - VegaWalletOrderType, -} from '@vegaprotocol/wallet'; +import { VegaWalletOrderTimeInForce } from '@vegaprotocol/wallet'; import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet'; import type { ReactNode } from 'react'; import { useOrderEdit } from './use-order-edit'; @@ -18,15 +14,8 @@ import type { import { ORDER_EVENT_SUB } from './order-event-query'; import type { MockedResponse } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing'; -import { - MarketTradingMode, - MarketState, - OrderTimeInForce, -} from '@vegaprotocol/types'; -import type { - OrderAmendmentBodyOrderAmendment, - OrderAmendmentBody, -} from '@vegaprotocol/vegawallet-service-api-client'; +import type { OrderFields } from '../components'; +import { generateOrder } from '../components'; const defaultWalletContext = { keypair: null, @@ -38,7 +27,7 @@ const defaultWalletContext = { connector: null, }; -function setup(context?: Partial) { +function setup(order: OrderFields, context?: Partial) { const mocks: MockedResponse = { request: { query: ORDER_EVENT_SUB, @@ -119,86 +108,47 @@ function setup(context?: Partial) { ); - return renderHook(() => useOrderEdit(), { wrapper }); + return renderHook(() => useOrderEdit(order), { wrapper }); } -const defaultMarket = { - __typename: 'Market', - id: 'market-id', - decimalPlaces: 2, - positionDecimalPlaces: 1, - tradingMode: MarketTradingMode.Continuous, - state: MarketState.Active, - tradableInstrument: { - __typename: 'TradableInstrument', - instrument: { - __typename: 'Instrument', - product: { - __typename: 'Future', - quoteName: 'quote-name', - }, - }, - }, - depth: { - __typename: 'MarketDepth', - lastTrade: { - __typename: 'Trade', - price: '100', - }, - }, -}; - -const order = { - id: 'order-id', - type: VegaWalletOrderType.Limit, - size: '10', - timeInForce: OrderTimeInForce.GTT, // order timeInForce is transformed to wallet timeInForce - side: VegaWalletOrderSide.Buy, - price: '1234567.89', - expiration: new Date('2022-01-01'), - expiresAt: new Date('2022-01-01'), - status: VegaTxStatus.Pending, - rejectionReason: null, - market: { - id: 'market-id', - decimalPlaces: 2, - name: 'ETHDAI', - positionDecimalPlaces: 2, - }, -}; - describe('useOrderEdit', () => { it('should edit a correctly formatted order', async () => { const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({})); const keypair = { pub: '0x123', } as VegaKeyExtended; - const { result } = setup({ + const order = generateOrder({ + price: '123456789', + market: { decimalPlaces: 2 }, + }); + const { result } = setup(order, { sendTx: mockSendTx, keypairs: [keypair], keypair, }); - await act(async () => { - result.current.edit(order); + act(() => { + result.current.edit({ price: '1234567.89' }); }); expect(mockSendTx).toHaveBeenCalledWith({ pubKey: keypair.pub, propagate: true, orderAmendment: { - orderId: 'order-id', - marketId: defaultMarket.id, // Market provided from hook argument - timeInForce: VegaWalletOrderTimeInForce.GTT, + orderId: order.id, + // eslint-disable-next-line + marketId: order.market!.id, + timeInForce: VegaWalletOrderTimeInForce[order.timeInForce], price: { value: '123456789' }, // Decimal removed sizeDelta: 0, - expiresAt: { value: order.expiration?.getTime() + '000000' }, // Nanoseconds append - } as unknown as OrderAmendmentBodyOrderAmendment, - } as OrderAmendmentBody); + expiresAt: undefined, + }, + }); }); it('has the correct default state', () => { - const { result } = setup(); + const order = generateOrder(); + const { result } = setup(order); expect(typeof result.current.edit).toEqual('function'); expect(typeof result.current.reset).toEqual('function'); expect(result.current.transaction.status).toEqual(VegaTxStatus.Default); @@ -207,8 +157,9 @@ describe('useOrderEdit', () => { }); it('should not sendTx if no keypair', async () => { + const order = generateOrder(); const mockSendTx = jest.fn(); - const { result } = setup({ + const { result } = setup(order, { sendTx: mockSendTx, keypairs: [], keypair: null, diff --git a/libs/orders/src/lib/order-hooks/use-order-edit.tsx b/libs/orders/src/lib/order-hooks/use-order-edit.tsx index 3fde8a2a8..2e1a9e3d0 100644 --- a/libs/orders/src/lib/order-hooks/use-order-edit.tsx +++ b/libs/orders/src/lib/order-hooks/use-order-edit.tsx @@ -1,50 +1,51 @@ -import { useApolloClient } from '@apollo/client'; -import { determineId, removeDecimal } from '@vegaprotocol/react-helpers'; -import { useState, useCallback, useEffect, useRef } from 'react'; -import type { Order } from '@vegaprotocol/wallet'; -import { VegaWalletOrderTimeInForce } from '@vegaprotocol/wallet'; -import { useVegaTransaction, useVegaWallet } from '@vegaprotocol/wallet'; -import { ORDER_EVENT_SUB } from './order-event-query'; -import type { Subscription } from 'zen-observable-ts'; -import type { - OrderEvent_busEvents_event_Order, - OrderEvent, - OrderEventVariables, -} from './__generated__'; +import { removeDecimal, toNanoSeconds } from '@vegaprotocol/react-helpers'; +import { useState, useCallback } from 'react'; +import { + useVegaTransaction, + useVegaWallet, + VegaWalletOrderTimeInForce, +} from '@vegaprotocol/wallet'; +import type { OrderEvent_busEvents_event_Order } from './__generated__'; import * as Sentry from '@sentry/react'; +import type { OrderFields } from '../components'; +import { useOrderEvent } from './use-order-event'; -export const useOrderEdit = () => { +// Can only edit price for now +export interface EditOrderArgs { + price: string; +} + +export const useOrderEdit = (order: OrderFields | null) => { const { keypair } = useVegaWallet(); - const { send, transaction, reset: resetTransaction } = useVegaTransaction(); + const [updatedOrder, setUpdatedOrder] = useState(null); - const client = useApolloClient(); - const subRef = useRef(null); + + const { + send, + transaction, + reset: resetTransaction, + setComplete, + TransactionDialog, + } = useVegaTransaction(); + + const waitForOrderEvent = useOrderEvent(); const reset = useCallback(() => { resetTransaction(); setUpdatedOrder(null); - subRef.current?.unsubscribe(); - }, [resetTransaction]); - - useEffect(() => { - return () => { - resetTransaction(); - setUpdatedOrder(null); - subRef.current?.unsubscribe(); - }; }, [resetTransaction]); const edit = useCallback( - async (order: Order) => { - if (!keypair || !order.market || !order.market.id) { + async (args: EditOrderArgs) => { + if (!keypair || !order || !order.market) { return; } setUpdatedOrder(null); try { - const res = await send({ + await send({ pubKey: keypair.pub, propagate: true, orderAmendment: { @@ -52,70 +53,35 @@ export const useOrderEdit = () => { marketId: order.market.id, // @ts-ignore fix me please! price: { - value: removeDecimal(order.price, order.market?.decimalPlaces), + value: removeDecimal(args.price, order.market.decimalPlaces), }, timeInForce: VegaWalletOrderTimeInForce[order.timeInForce], // @ts-ignore fix me please! sizeDelta: 0, - // @ts-ignore fix me please! expiresAt: order.expiresAt ? { - value: - // Wallet expects timestamp in nanoseconds, - // we don't have that level of accuracy so just append 6 zeroes - new Date(order.expiresAt).getTime().toString() + '000000', + value: toNanoSeconds(new Date(order.expiresAt)), // Wallet expects timestamp in nanoseconds } : undefined, }, }); - if (res?.signature) { - const resId = order.id ?? determineId(res.signature); - setUpdatedOrder(null); - - if (resId) { - // Start a subscription looking for the newly created order - subRef.current = client - .subscribe({ - query: ORDER_EVENT_SUB, - variables: { partyId: keypair?.pub || '' }, - }) - .subscribe(({ data }) => { - if (!data?.busEvents?.length) { - return; - } - - // No types available for the subscription result - const matchingOrderEvent = data.busEvents.find((e) => { - if (e.event.__typename !== 'Order') { - return false; - } - - return e.event.id === resId; - }); - - if ( - matchingOrderEvent && - matchingOrderEvent.event.__typename === 'Order' - ) { - setUpdatedOrder(matchingOrderEvent.event); - subRef.current?.unsubscribe(); - } - }); - } - } - return res; + waitForOrderEvent(order.id, keypair.pub, (updatedOrder) => { + setUpdatedOrder(updatedOrder); + setComplete(); + }); } catch (e) { Sentry.captureException(e); return; } }, - [client, keypair, send] + [keypair, send, order, setComplete, waitForOrderEvent] ); return { transaction, updatedOrder, + TransactionDialog, edit, reset, }; diff --git a/libs/orders/src/lib/order-hooks/use-order-event.ts b/libs/orders/src/lib/order-hooks/use-order-event.ts new file mode 100644 index 000000000..e7b257a75 --- /dev/null +++ b/libs/orders/src/lib/order-hooks/use-order-event.ts @@ -0,0 +1,59 @@ +import { useApolloClient } from '@apollo/client'; +import { useCallback, useEffect, useRef } from 'react'; +import { ORDER_EVENT_SUB } from './order-event-query'; +import type { + OrderEvent, + OrderEventVariables, + OrderEvent_busEvents_event_Order, +} from './__generated__'; +import type { Subscription } from 'zen-observable-ts'; + +export const useOrderEvent = () => { + const client = useApolloClient(); + const subRef = useRef(null); + + const waitForOrderEvent = useCallback( + ( + id: string, + partyId: string, + callback: (order: OrderEvent_busEvents_event_Order) => void + ) => { + subRef.current = client + .subscribe({ + query: ORDER_EVENT_SUB, + variables: { partyId }, + }) + .subscribe(({ data }) => { + if (!data?.busEvents?.length) { + return; + } + + // No types available for the subscription result + const matchingOrderEvent = data.busEvents.find((e) => { + if (e.event.__typename !== 'Order') { + return false; + } + + return e.event.id === id; + }); + + if ( + matchingOrderEvent && + matchingOrderEvent.event.__typename === 'Order' + ) { + callback(matchingOrderEvent.event); + subRef.current?.unsubscribe(); + } + }); + }, + [client] + ); + + useEffect(() => { + return () => { + subRef.current?.unsubscribe(); + }; + }, []); + + return waitForOrderEvent; +}; 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 e981c1df5..2df7ca0ae 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 @@ -21,6 +21,7 @@ import { ORDER_EVENT_SUB } from './order-event-query'; import type { MockedResponse } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing'; import type { Market } from '../market'; +import { toNanoSeconds } from '@vegaprotocol/react-helpers'; const defaultMarket = { __typename: 'Market', @@ -179,7 +180,9 @@ describe('useOrderSubmit', () => { side: VegaWalletOrderSide.Buy, timeInForce: VegaWalletOrderTimeInForce.GTT, price: '123456789', // Decimal removed - expiresAt: order.expiration?.getTime() + '000000', // Nanoseconds append + expiresAt: order.expiration + ? toNanoSeconds(order.expiration) + : 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 index aa92e7dbb..68b0828da 100644 --- a/libs/orders/src/lib/order-hooks/use-order-submit.ts +++ b/libs/orders/src/lib/order-hooks/use-order-submit.ts @@ -1,39 +1,52 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { useApolloClient } from '@apollo/client'; -import type { Order } from '../utils/get-default-order'; -import { ORDER_EVENT_SUB } from './order-event-query'; +import { useCallback, useState } from 'react'; +import type { OrderEvent_busEvents_event_Order } from './__generated__'; import type { - OrderEvent, - OrderEventVariables, - OrderEvent_busEvents_event_Order, -} from './__generated__'; + VegaWalletOrderTimeInForce, + VegaWalletOrderSide, +} from '@vegaprotocol/wallet'; import { VegaWalletOrderType, useVegaWallet } from '@vegaprotocol/wallet'; -import { determineId, removeDecimal } from '@vegaprotocol/react-helpers'; +import { + determineId, + removeDecimal, + toNanoSeconds, +} from '@vegaprotocol/react-helpers'; import { useVegaTransaction } from '@vegaprotocol/wallet'; import * as Sentry from '@sentry/react'; -import type { Market } from '../market'; -import type { Subscription } from 'zen-observable-ts'; +import { useOrderEvent } from './use-order-event'; + +export interface Order { + type: VegaWalletOrderType; + size: string; + side: VegaWalletOrderSide; + timeInForce: VegaWalletOrderTimeInForce; + price?: string; + expiration?: Date; +} + +export interface Market { + id: string; + decimalPlaces: number; + positionDecimalPlaces: number; +} export const useOrderSubmit = (market: Market) => { const { keypair } = useVegaWallet(); - const { send, transaction, reset: resetTransaction } = useVegaTransaction(); + const waitForOrderEvent = useOrderEvent(); + + const { + send, + transaction, + reset: resetTransaction, + setComplete, + TransactionDialog, + } = useVegaTransaction(); + const [finalizedOrder, setFinalizedOrder] = useState(null); - const client = useApolloClient(); - const subRef = useRef(null); const reset = useCallback(() => { resetTransaction(); setFinalizedOrder(null); - subRef.current?.unsubscribe(); - }, [resetTransaction]); - - useEffect(() => { - return () => { - resetTransaction(); - setFinalizedOrder(null); - subRef.current?.unsubscribe(); - }; }, [resetTransaction]); const submit = useCallback( @@ -43,6 +56,7 @@ export const useOrderSubmit = (market: Market) => { } setFinalizedOrder(null); + try { const res = await send({ pubKey: keypair.pub, @@ -58,9 +72,7 @@ export const useOrderSubmit = (market: Market) => { side: order.side, timeInForce: order.timeInForce, expiresAt: order.expiration - ? // Wallet expects timestamp in nanoseconds, we don't have that level of accuracy so - // just append 6 zeroes - order.expiration.getTime().toString() + '000000' + ? toNanoSeconds(order.expiration) // Wallet expects timestampe in nanoseconds : undefined, }, }); @@ -68,34 +80,10 @@ export const useOrderSubmit = (market: Market) => { if (res?.signature) { const resId = determineId(res.signature); if (resId) { - // Start a subscription looking for the newly created order - subRef.current = client - .subscribe({ - query: ORDER_EVENT_SUB, - variables: { partyId: keypair?.pub || '' }, - }) - .subscribe(({ data }) => { - if (!data?.busEvents?.length) { - return; - } - - // No types available for the subscription result - const matchingOrderEvent = data.busEvents.find((e) => { - if (e.event.__typename !== 'Order') { - return false; - } - - return e.event.id === resId; - }); - - if ( - matchingOrderEvent && - matchingOrderEvent.event.__typename === 'Order' - ) { - setFinalizedOrder(matchingOrderEvent.event); - subRef.current?.unsubscribe(); - } - }); + waitForOrderEvent(resId, keypair.pub, (order) => { + setFinalizedOrder(order); + setComplete(); + }); } } return res; @@ -104,19 +92,13 @@ export const useOrderSubmit = (market: Market) => { return; } }, - [ - client, - keypair, - send, - market.id, - market.decimalPlaces, - market.positionDecimalPlaces, - ] + [keypair, send, market, setComplete, waitForOrderEvent] ); return { transaction, finalizedOrder, + TransactionDialog, submit, reset, }; diff --git a/libs/orders/src/lib/order-hooks/use-order-validation.tsx b/libs/orders/src/lib/order-hooks/use-order-validation.tsx index ed4585b52..e85a006e4 100644 --- a/libs/orders/src/lib/order-hooks/use-order-validation.tsx +++ b/libs/orders/src/lib/order-hooks/use-order-validation.tsx @@ -1,19 +1,22 @@ import type { FieldErrors } from 'react-hook-form'; import { useMemo } from 'react'; import { t } from '@vegaprotocol/react-helpers'; -import type { Order } from '@vegaprotocol/wallet'; import { useVegaWallet, VegaWalletOrderTimeInForce as OrderTimeInForce, VegaWalletOrderType as OrderType, } from '@vegaprotocol/wallet'; import { MarketState, MarketTradingMode } from '@vegaprotocol/types'; -import type { Market } from '../market'; import { ERROR_SIZE_DECIMAL } from '../utils/validate-size'; +import type { Order } from './use-order-submit'; -export type ValidationProps = { +export type ValidationArgs = { step: number; - market: Market; + market: { + state: MarketState; + tradingMode: MarketTradingMode; + positionDecimalPlaces: number; + }; orderType: OrderType; orderTimeInForce: OrderTimeInForce; fieldErrors?: FieldErrors; @@ -34,7 +37,7 @@ export const useOrderValidation = ({ fieldErrors = {}, orderType, orderTimeInForce, -}: ValidationProps) => { +}: ValidationArgs) => { const { keypair } = useVegaWallet(); const { message, isDisabled } = useMemo(() => { diff --git a/libs/orders/src/lib/utils/get-default-order.ts b/libs/orders/src/lib/utils/get-default-order.ts index 5ea2e99c3..c3229704b 100644 --- a/libs/orders/src/lib/utils/get-default-order.ts +++ b/libs/orders/src/lib/utils/get-default-order.ts @@ -4,38 +4,11 @@ import { VegaWalletOrderSide, } from '@vegaprotocol/wallet'; import { toDecimal } from '@vegaprotocol/react-helpers'; -import type { Market } from '../market'; -import type { OrderStatus } from '@vegaprotocol/types'; - -export type Order = - | { - size: string; - type: VegaWalletOrderType.Market; - timeInForce: VegaWalletOrderTimeInForce; - side: VegaWalletOrderSide; - price?: never; - expiration?: never; - rejectionReason: string | null; - status?: OrderStatus; - market?: Market | null; - } - | { - size: string; - type: VegaWalletOrderType.Limit; - timeInForce: VegaWalletOrderTimeInForce; - side: VegaWalletOrderSide; - price?: string; - expiration?: Date; - rejectionReason: string | null; - status?: OrderStatus; - market?: Market | null; - }; +import type { Order, Market } from '../order-hooks'; export const getDefaultOrder = (market: Market): Order => ({ type: VegaWalletOrderType.Market, side: VegaWalletOrderSide.Buy, timeInForce: VegaWalletOrderTimeInForce.IOC, size: String(toDecimal(market.positionDecimalPlaces)), - rejectionReason: null, - market: null, }); diff --git a/libs/react-helpers/src/index.ts b/libs/react-helpers/src/index.ts index e98c785b4..75f83254e 100644 --- a/libs/react-helpers/src/index.ts +++ b/libs/react-helpers/src/index.ts @@ -8,4 +8,5 @@ export * from './lib/i18n'; export * from './lib/pagination'; export * from './lib/remove-0x'; export * from './lib/storage'; +export * from './lib/time'; export * from './lib/validate'; diff --git a/libs/react-helpers/src/lib/time.ts b/libs/react-helpers/src/lib/time.ts new file mode 100644 index 000000000..afc177e7f --- /dev/null +++ b/libs/react-helpers/src/lib/time.ts @@ -0,0 +1,3 @@ +export const toNanoSeconds = (date: Date) => { + return date.getTime().toString() + '000000'; +}; diff --git a/libs/types/src/__generated__/globalTypes.ts b/libs/types/src/__generated__/globalTypes.ts index 7ef4cc642..aaf291df8 100644 --- a/libs/types/src/__generated__/globalTypes.ts +++ b/libs/types/src/__generated__/globalTypes.ts @@ -293,6 +293,16 @@ export enum WithdrawalStatus { Rejected = "Rejected", } +/** + * Pagination constructs to support cursor based pagination in the API + */ +export interface Pagination { + first?: number | null; + after?: string | null; + last?: number | null; + before?: string | null; +} + //============================================================== // END Enums and Input Objects //============================================================== diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index f01cb9e4c..a1cf99672 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -1,3 +1,2 @@ export * from './__generated__'; export * from './candle'; -export * from './pagination'; diff --git a/libs/types/src/pagination.ts b/libs/types/src/pagination.ts deleted file mode 100644 index a178c09a7..000000000 --- a/libs/types/src/pagination.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface Pagination { - first?: number; - after?: string; - last?: number; - before?: string; -} diff --git a/libs/ui-toolkit/src/components/dialog/dialog.tsx b/libs/ui-toolkit/src/components/dialog/dialog.tsx index fd01f9ff7..ba1d8aeb2 100644 --- a/libs/ui-toolkit/src/components/dialog/dialog.tsx +++ b/libs/ui-toolkit/src/components/dialog/dialog.tsx @@ -10,6 +10,7 @@ interface DialogProps { open: boolean; onChange?: (isOpen: boolean) => void; title?: string; + icon?: ReactNode; intent?: Intent; titleClassNames?: string; contentClassNames?: string; @@ -20,6 +21,7 @@ export function Dialog({ open, onChange, title, + icon, intent, titleClassNames, contentClassNames, @@ -50,15 +52,20 @@ export function Dialog({ className="focus:outline-none focus-visible:outline-none" /> - {title && ( -

- {title} -

- )} - {children} +
+ {icon &&
{icon}
} +
+ {title && ( +

+ {title} +

+ )} +
{children}
+
+
diff --git a/libs/wallet/src/manage-dialog/manage-dialog.tsx b/libs/wallet/src/manage-dialog/manage-dialog.tsx index e4851f108..47309264e 100644 --- a/libs/wallet/src/manage-dialog/manage-dialog.tsx +++ b/libs/wallet/src/manage-dialog/manage-dialog.tsx @@ -57,9 +57,10 @@ export const VegaManageDialog = ({ )}
-

+

{truncateByChars(kp.pub, 23, 23)}

+

{kp.pub}