Chore/657 refactor wallet and orders libs (#709)

* feat: 470 edit orders hook and @vegaprotocol/vegawallet-service-api-client@0.4.14

* fix: 470 add methods for dialog intent and title

* fix: #657 rename order-list lib to orders

* chore: #657 move hooks to orders lib

* chore: #657 vega tx dialog used for order cancellation and order submission

* chore: #657 use client subscribe and unsubscribe on reset, refactor vegatxdialog

* fix: #657 revert script src=./env-config.js ending

* fix: #657 format project.json

* Update project.json

* fix: #657 cancel all subs and async tasks in useffect cleanup function

* feat: #657 styling updates on vega order dialog

* fix: #657 rename set dialog open and awaiting confirmation dialog update

* fix: #657 updates on cancel order id check

* fix:  #657 fix vega tx dialog test

* fix:  #657 fix cypress trading-deal-tciket test

* fix:  #657 fix data-testid market test

* Update libs/orders/README.md

Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>

* Update libs/wallet/src/vega-order-transaction-dialog/vega-order-transaction-dialog.tsx

Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>

* Update libs/wallet/src/vega-transaction-dialog/vega-transaction-dialog.tsx

Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>

* Update libs/wallet/src/vega-order-transaction-dialog/vega-order-transaction-dialog.tsx

Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>

* Update libs/wallet/src/vega-order-transaction-dialog/vega-order-transaction-dialog.tsx

Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>

* fix: #657 remove the magic string and use the ordertype enum from types package

* fix: #657 guarantee that order.id is present at this point or we need to determine the id of the order

* fix: #657 fix translation in dialog

* fix: #657 rename wallet types, delete ticket query, set finalized order null in submit

* fix: #657 fix deal ticket steps test

* fix: #657 remove settings.json

* fix: #657 use order submit in orders lib

* fix: #463 final modal links to block explorer

* fix: #745 long/short instead of buy/sell

* fix: #657 use only one vega tx dialog

* fix: #657 keep ref of subscription and unsubscribe

* fix:  #657 hide cancelled orders

* fix: #657 sub only when id set

* fix: WIP: trying to unsub when order updated

* fix: #745 long/short instead of buy/sell

* fix: ensure observable defined

* fix: #657 remove redundant test

* Update libs/orders/src/lib/order-hooks/use-order-submit.ts

* fix: failing test due to resizeobserver loop limit exceeded

* fix: lint

* fix: #657 fix test resize observer loop limit exceeded

Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: Joe <joe@vega.xyz>
This commit is contained in:
m.ray 2022-07-13 16:23:46 +02:00 committed by GitHub
parent 0291c37cfb
commit 07abc2b1eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
108 changed files with 1360 additions and 1187 deletions

View File

@ -2,25 +2,28 @@ import * as React from 'react';
import { useForm, Controller } from 'react-hook-form';
import Box from '@mui/material/Box';
import { Stepper } from '../stepper';
import type { DealTicketQuery_market, Order } from '@vegaprotocol/deal-ticket';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import { Button, InputError } from '@vegaprotocol/ui-toolkit';
import {
ExpirySelector,
SideSelector,
TimeInForceSelector,
TypeSelector,
getDefaultOrder,
useOrderValidation,
useOrderSubmit,
DealTicketAmount,
MarketSelector,
} from '@vegaprotocol/deal-ticket';
import type { Order } from '@vegaprotocol/orders';
import {
OrderTimeInForce,
OrderType,
VegaWalletOrderTimeInForce as OrderTimeInForce,
VegaWalletOrderType as OrderType,
VegaTxStatus,
} from '@vegaprotocol/wallet';
import { t, addDecimal, toDecimal } from '@vegaprotocol/react-helpers';
import {
getDefaultOrder,
useOrderValidation,
useOrderSubmit,
} from '@vegaprotocol/orders';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import MarketNameRenderer from '../simple-market-list/simple-market-renderer';

View File

@ -18,21 +18,25 @@ describe('accounts', () => {
cy.getByTestId('tab-accounts').should('be.visible');
cy.getByTestId('tab-accounts')
.should('be.visible')
.get(`[row-id='General-tEURO-null']`)
.find('[col-id="asset.symbol"]')
.should('have.text', 'tEURO');
cy.getByTestId('tab-accounts')
.should('be.visible')
.get(`[row-id='General-tEURO-null']`)
.find('[col-id="type"]')
.should('have.text', 'General');
cy.getByTestId('tab-accounts')
.should('be.visible')
.get(`[row-id='General-tEURO-null']`)
.find('[col-id="market.name"]')
.should('have.text', '—');
cy.getByTestId('tab-accounts')
.should('be.visible')
.get(`[row-id='General-tEURO-null']`)
.find('[col-id="balance"]')
.should('have.text', '1,000.00000');

View File

@ -33,7 +33,7 @@ describe('deal ticket orders', () => {
const orderTIFDropDown = 'order-tif';
const placeOrderBtn = 'place-order';
const orderStatusHeader = 'order-status-header';
const orderTransactionHash = 'tx-hash';
const orderTransactionHash = 'tx-block-explorer';
before(() => {
cy.mockGQL((req) => {

View File

@ -19,6 +19,7 @@ describe('positions', () => {
cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions')
.get('[col-id="market.tradableInstrument.instrument.code"]')
.should('be.visible')
.each(($marketSymbol) => {
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
});

View File

@ -1,6 +1,6 @@
import merge from 'lodash/merge';
import type { PartialDeep } from 'type-fest';
import type { Orders, Orders_party_orders } from '@vegaprotocol/order-list';
import type { Orders, Orders_party_orders } from '@vegaprotocol/orders';
import {
OrderStatus,
OrderTimeInForce,

View File

@ -3,7 +3,7 @@
// @generated
// This file was automatically generated and should not be edited.
import { MarketTradingMode } from "@vegaprotocol/types";
import { MarketTradingMode, MarketState } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: MarketsLanding
@ -27,6 +27,10 @@ export interface MarketsLanding_markets {
* Current mode of execution of the market
*/
tradingMode: MarketTradingMode;
/**
* Current state of the market
*/
state: MarketState;
/**
* timestamps for state changes in the market
*/

View File

@ -1,7 +1,7 @@
import { gql, useQuery } from '@apollo/client';
import { MarketTradingMode } from '@vegaprotocol/types';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import sortBy from 'lodash/sortBy';
import orderBy from 'lodash/orderBy';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useGlobalStore } from '../stores';
@ -12,6 +12,7 @@ const MARKETS_QUERY = gql`
markets {
id
tradingMode
state
marketTimestamps {
open
}
@ -20,13 +21,13 @@ const MARKETS_QUERY = gql`
`;
const marketList = ({ markets }: MarketsLanding) =>
sortBy(
orderBy(
markets?.filter(
({ marketTimestamps, tradingMode }) =>
marketTimestamps.open && tradingMode === MarketTradingMode.Continuous
) || [],
'marketTimestamps.open',
'id'
['state', 'marketTimestamps.open', 'id'],
['asc', 'asc', 'asc']
);
export function Index() {

View File

@ -81,6 +81,7 @@ const MarketPage = ({ id }: { id?: string }) => {
return (
<PageQueryContainer<Market, MarketVariables>
query={MARKET_QUERY}
data-testid="market"
options={{
variables: {
marketId,

View File

@ -6,7 +6,7 @@ import {
DealTicketContainer,
MarketInfoContainer,
} from '@vegaprotocol/deal-ticket';
import { OrderListContainer } from '@vegaprotocol/order-list';
import { OrderListContainer } from '@vegaprotocol/orders';
import { TradesContainer } from '@vegaprotocol/trades';
import { PositionsContainer } from '@vegaprotocol/positions';
import { OrderbookContainer } from '@vegaprotocol/market-depth';

View File

@ -1,7 +1,7 @@
import { Web3Container } from '../../components/web3-container';
import { t } from '@vegaprotocol/react-helpers';
import { PositionsContainer } from '@vegaprotocol/positions';
import { OrderListContainer } from '@vegaprotocol/order-list';
import { OrderListContainer } from '@vegaprotocol/orders';
import { AccountsContainer } from '@vegaprotocol/accounts';
import { AnchorButton, Tab, Tabs } from '@vegaprotocol/ui-toolkit';
import { WithdrawalsContainer } from './withdrawals/withdrawals-container';

View File

@ -1,11 +1,11 @@
import type { UseFormRegister } from 'react-hook-form';
import { OrderType } from '@vegaprotocol/wallet';
import type { Order } from '../utils/get-default-order';
import { VegaWalletOrderType } from '@vegaprotocol/wallet';
import type { Order } from '@vegaprotocol/orders';
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
export interface DealTicketAmountProps {
orderType: OrderType;
orderType: VegaWalletOrderType;
step: number;
register: UseFormRegister<Order>;
quoteName: string;
@ -17,9 +17,9 @@ export const DealTicketAmount = ({
...props
}: DealTicketAmountProps) => {
switch (orderType) {
case OrderType.Market:
case VegaWalletOrderType.Market:
return <DealTicketMarketAmount {...props} />;
case OrderType.Limit:
case VegaWalletOrderType.Limit:
return <DealTicketLimitAmount {...props} />;
default: {
throw new Error('Invalid ticket type');

View File

@ -1,6 +1,6 @@
import { FormGroup, Input } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/react-helpers';
import { validateSize } from '../utils/validate-size';
import { validateSize } from '@vegaprotocol/orders';
import type { DealTicketAmountProps } from './deal-ticket-amount';
export type DealTicketLimitAmountProps = Omit<

View File

@ -1,11 +1,10 @@
import type { ReactNode } from 'react';
import { useEffect, useState } from 'react';
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { OrderStatus } from '@vegaprotocol/types';
import { VegaOrderTransactionDialog, VegaTxStatus } from '@vegaprotocol/wallet';
import { useState } from 'react';
import { VegaTransactionDialog, VegaTxStatus } from '@vegaprotocol/wallet';
import { DealTicket } from './deal-ticket';
import { useOrderSubmit } from '../hooks/use-order-submit';
import type { DealTicketQuery_market } from './__generated__/DealTicketQuery';
import { useOrderSubmit } from '@vegaprotocol/orders';
import { OrderStatus } from '@vegaprotocol/types';
export interface DealTicketManagerProps {
market: DealTicketQuery_market;
@ -18,43 +17,20 @@ export const DealTicketManager = ({
}: DealTicketManagerProps) => {
const [orderDialogOpen, setOrderDialogOpen] = useState(false);
const { submit, transaction, finalizedOrder, reset } = useOrderSubmit(market);
const getDialogIntent = (status: VegaTxStatus) => {
if (finalizedOrder) {
if (
finalizedOrder.status === OrderStatus.Active ||
finalizedOrder.status === OrderStatus.Filled ||
finalizedOrder.status === OrderStatus.PartiallyFilled
) {
return Intent.Success;
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';
}
if (finalizedOrder.status === OrderStatus.Parked) {
return Intent.Warning;
}
return Intent.Danger;
}
if (status === VegaTxStatus.Requested) {
return Intent.Warning;
}
if (status === VegaTxStatus.Error) {
return Intent.Danger;
}
return Intent.None;
};
useEffect(() => {
if (transaction.status !== VegaTxStatus.Default || finalizedOrder) {
setOrderDialogOpen(true);
} else {
setOrderDialogOpen(false);
}
}, [finalizedOrder, transaction.status]);
return (
<>
{children || (
@ -69,23 +45,15 @@ export const DealTicketManager = ({
}
/>
)}
<Dialog
open={orderDialogOpen}
onChange={(isOpen) => {
setOrderDialogOpen(isOpen);
// If closing reset
if (!isOpen) {
reset();
}
}}
intent={getDialogIntent(transaction.status)}
>
<VegaOrderTransactionDialog
transaction={transaction}
<VegaTransactionDialog
key={`submit-order-dialog-${transaction.txHash}`}
orderDialogOpen={orderDialogOpen}
setOrderDialogOpen={setOrderDialogOpen}
finalizedOrder={finalizedOrder}
transaction={transaction}
reset={reset}
title={getDialogTitle(finalizedOrder?.status)}
/>
</Dialog>
</>
);
};

View File

@ -1,6 +1,6 @@
import { FormGroup, Input } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/react-helpers';
import { validateSize } from '../utils/validate-size';
import { validateSize } from '@vegaprotocol/orders';
import type { DealTicketAmountProps } from './deal-ticket-amount';
export type DealTicketMarketAmountProps = Omit<

View File

@ -1,7 +1,7 @@
import {
VegaWalletContext,
OrderTimeInForce,
OrderType,
VegaWalletOrderTimeInForce,
VegaWalletOrderType,
} from '@vegaprotocol/wallet';
import { addDecimal } from '@vegaprotocol/react-helpers';
import { fireEvent, render, screen, act } from '@testing-library/react';
@ -64,7 +64,7 @@ it('Displays ticket defaults', () => {
// Assert defaults are used
expect(
screen.getByTestId(`order-type-${OrderType.Market}-selected`)
screen.getByTestId(`order-type-${VegaWalletOrderType.Market}-selected`)
).toBeInTheDocument();
expect(
screen.queryByTestId('order-side-SIDE_BUY-selected')
@ -75,7 +75,9 @@ it('Displays ticket defaults', () => {
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
String(1 / Math.pow(10, market.positionDecimalPlaces))
);
expect(screen.getByTestId('order-tif')).toHaveValue(OrderTimeInForce.IOC);
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.IOC
);
// Assert last price is shown
expect(screen.getByTestId('last-price')).toHaveTextContent(
@ -101,9 +103,11 @@ it('Can edit deal ticket', async () => {
expect(screen.getByTestId('order-size')).toHaveDisplayValue('200');
fireEvent.change(screen.getByTestId('order-tif'), {
target: { value: OrderTimeInForce.IOC },
target: { value: VegaWalletOrderTimeInForce.IOC },
});
expect(screen.getByTestId('order-tif')).toHaveValue(OrderTimeInForce.IOC);
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.IOC
);
// Switch to limit order
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
@ -113,7 +117,7 @@ it('Can edit deal ticket', async () => {
// Check all TIF options shown
expect(screen.getByTestId('order-tif').children).toHaveLength(
Object.keys(OrderTimeInForce).length
Object.keys(VegaWalletOrderTimeInForce).length
);
});
@ -130,26 +134,34 @@ it('Handles TIF select box dependent on order type', () => {
// Switch to limit order and check all TIF options shown
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
expect(screen.getByTestId('order-tif').children).toHaveLength(
Object.keys(OrderTimeInForce).length
Object.keys(VegaWalletOrderTimeInForce).length
);
// Change to GTC
fireEvent.change(screen.getByTestId('order-tif'), {
target: { value: OrderTimeInForce.GTC },
target: { value: VegaWalletOrderTimeInForce.GTC },
});
expect(screen.getByTestId('order-tif')).toHaveValue(OrderTimeInForce.GTC);
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.GTC
);
// Switch back to market order and TIF should now be IOC
fireEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
expect(screen.getByTestId('order-tif')).toHaveValue(OrderTimeInForce.IOC);
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.IOC
);
// Switch tif to FOK
fireEvent.change(screen.getByTestId('order-tif'), {
target: { value: OrderTimeInForce.FOK },
target: { value: VegaWalletOrderTimeInForce.FOK },
});
expect(screen.getByTestId('order-tif')).toHaveValue(OrderTimeInForce.FOK);
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.FOK
);
// Change back to limit and check we are still on FOK
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
expect(screen.getByTestId('order-tif')).toHaveValue(OrderTimeInForce.FOK);
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.FOK
);
});

View File

@ -1,17 +1,19 @@
import { useCallback } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { OrderType, OrderTimeInForce } from '@vegaprotocol/wallet';
import {
VegaWalletOrderType,
VegaWalletOrderTimeInForce,
} from '@vegaprotocol/wallet';
import { t, addDecimal, toDecimal } from '@vegaprotocol/react-helpers';
import { Button, InputError } from '@vegaprotocol/ui-toolkit';
import { TypeSelector } from './type-selector';
import { SideSelector } from './side-selector';
import { DealTicketAmount } from './deal-ticket-amount';
import { TimeInForceSelector } from './time-in-force-selector';
import { useOrderValidation } from '../hooks/use-order-validation';
import type { DealTicketQuery_market } from './__generated__/DealTicketQuery';
import type { Order } from '../utils/get-default-order';
import { getDefaultOrder } from '../utils/get-default-order';
import { ExpirySelector } from './expiry-selector';
import type { Order } from '@vegaprotocol/orders';
import { getDefaultOrder, useOrderValidation } from '@vegaprotocol/orders';
export type TransactionStatus = 'default' | 'pending';
@ -97,8 +99,8 @@ export const DealTicket = ({
/>
)}
/>
{orderType === OrderType.Limit &&
orderTimeInForce === OrderTimeInForce.GTT && (
{orderType === VegaWalletOrderType.Limit &&
orderTimeInForce === VegaWalletOrderTimeInForce.GTT && (
<Controller
name="expiration"
control={control}

View File

@ -1,16 +1,16 @@
import { FormGroup } from '@vegaprotocol/ui-toolkit';
import { OrderSide } from '@vegaprotocol/wallet';
import { VegaWalletOrderSide } from '@vegaprotocol/wallet';
import { Toggle } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/react-helpers';
interface SideSelectorProps {
value: OrderSide;
onSelect: (side: OrderSide) => void;
value: VegaWalletOrderSide;
onSelect: (side: VegaWalletOrderSide) => void;
}
export const SideSelector = ({ value, onSelect }: SideSelectorProps) => {
const toggles = Object.entries(OrderSide).map(([label, value]) => ({
label,
const toggles = Object.entries(VegaWalletOrderSide).map(([label, value]) => ({
label: label === 'Buy' ? 'Long' : 'Short',
value,
}));
@ -21,7 +21,7 @@ export const SideSelector = ({ value, onSelect }: SideSelectorProps) => {
name="order-side"
toggles={toggles}
checkedValue={value}
onChange={(e) => onSelect(e.target.value as OrderSide)}
onChange={(e) => onSelect(e.target.value as VegaWalletOrderSide)}
/>
</FormGroup>
);

View File

@ -1,11 +1,14 @@
import { FormGroup, Select } from '@vegaprotocol/ui-toolkit';
import { OrderTimeInForce, OrderType } from '@vegaprotocol/wallet';
import {
VegaWalletOrderTimeInForce,
VegaWalletOrderType,
} from '@vegaprotocol/wallet';
import { t } from '@vegaprotocol/react-helpers';
interface TimeInForceSelectorProps {
value: OrderTimeInForce;
orderType: OrderType;
onSelect: (tif: OrderTimeInForce) => void;
value: VegaWalletOrderTimeInForce;
orderType: VegaWalletOrderType;
onSelect: (tif: VegaWalletOrderTimeInForce) => void;
}
export const TimeInForceSelector = ({
@ -14,12 +17,12 @@ export const TimeInForceSelector = ({
onSelect,
}: TimeInForceSelectorProps) => {
const options =
orderType === OrderType.Limit
? Object.entries(OrderTimeInForce)
: Object.entries(OrderTimeInForce).filter(
orderType === VegaWalletOrderType.Limit
? Object.entries(VegaWalletOrderTimeInForce)
: Object.entries(VegaWalletOrderTimeInForce).filter(
([_, timeInForce]) =>
timeInForce === OrderTimeInForce.FOK ||
timeInForce === OrderTimeInForce.IOC
timeInForce === VegaWalletOrderTimeInForce.FOK ||
timeInForce === VegaWalletOrderTimeInForce.IOC
);
return (
@ -27,7 +30,7 @@ export const TimeInForceSelector = ({
<Select
id="select-time-in-force"
value={value}
onChange={(e) => onSelect(e.target.value as OrderTimeInForce)}
onChange={(e) => onSelect(e.target.value as VegaWalletOrderTimeInForce)}
className="w-full"
data-testid="order-tif"
>

View File

@ -1,14 +1,14 @@
import { FormGroup } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/react-helpers';
import { OrderType } from '@vegaprotocol/wallet';
import { VegaWalletOrderType } from '@vegaprotocol/wallet';
import { Toggle } from '@vegaprotocol/ui-toolkit';
interface TypeSelectorProps {
value: OrderType;
onSelect: (type: OrderType) => void;
value: VegaWalletOrderType;
onSelect: (type: VegaWalletOrderType) => void;
}
const toggles = Object.entries(OrderType).map(([label, value]) => ({
const toggles = Object.entries(VegaWalletOrderType).map(([label, value]) => ({
label,
value,
}));
@ -21,7 +21,7 @@ export const TypeSelector = ({ value, onSelect }: TypeSelectorProps) => {
name="order-type"
toggles={toggles}
checkedValue={value}
onChange={(e) => onSelect(e.target.value as OrderType)}
onChange={(e) => onSelect(e.target.value as VegaWalletOrderType)}
/>
</FormGroup>
);

View File

@ -1,114 +0,0 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { BusEventType, OrderType, OrderStatus, OrderRejectionReason } from "@vegaprotocol/types";
// ====================================================
// GraphQL subscription operation: OrderEvent
// ====================================================
export interface OrderEvent_busEvents_event_TimeUpdate {
__typename: "TimeUpdate" | "MarketEvent" | "TransferResponses" | "PositionResolution" | "Trade" | "Account" | "Party" | "MarginLevels" | "Proposal" | "Vote" | "MarketData" | "NodeSignature" | "LossSocialization" | "SettlePosition" | "Market" | "Asset" | "MarketTick" | "SettleDistressed" | "AuctionEvent" | "RiskFactor" | "Deposit" | "Withdrawal" | "OracleSpec" | "LiquidityProvision";
}
export interface OrderEvent_busEvents_event_Order_market {
__typename: "Market";
/**
* Market full name
*/
name: string;
/**
* 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 )
* 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 {
__typename: "Order";
/**
* Type the order type (defaults to PARTY)
*/
type: OrderType | null;
/**
* Hash of the order data
*/
id: string;
/**
* The status of an order, for example 'Active'
*/
status: OrderStatus;
/**
* Reason for the order to be rejected
*/
rejectionReason: OrderRejectionReason | null;
/**
* RFC3339Nano formatted date and time for when the order was created (timestamp)
*/
createdAt: string;
/**
* Total number of contracts that may be bought or sold (immutable) (uint64)
*/
size: string;
/**
* The worst price the order will trade at (e.g. buy for price or less, sell for price or more) (uint64)
*/
price: string;
/**
* The market the order is trading on (probably stored internally as a hash of the market details)
*/
market: OrderEvent_busEvents_event_Order_market | null;
}
export type OrderEvent_busEvents_event = OrderEvent_busEvents_event_TimeUpdate | OrderEvent_busEvents_event_Order;
export interface OrderEvent_busEvents {
__typename: "BusEvent";
/**
* the id for this event
*/
eventId: string;
/**
* the block hash
*/
block: string;
/**
* the type of event we're dealing with
*/
type: BusEventType;
/**
* the payload - the wrapped event
*/
event: OrderEvent_busEvents_event;
}
export interface OrderEvent {
/**
* Subscribe to event data from the event bus
*/
busEvents: OrderEvent_busEvents[] | null;
}
export interface OrderEventVariables {
partyId: string;
}

View File

@ -1,176 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react-hooks';
import type { Order } from '../utils/get-default-order';
import type {
VegaKeyExtended,
VegaWalletContextShape,
} from '@vegaprotocol/wallet';
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
import { OrderSide, OrderTimeInForce, OrderType } from '@vegaprotocol/wallet';
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import { useOrderSubmit } from './use-order-submit';
import type { DealTicketQuery_market } from '../components/__generated__/DealTicketQuery';
const defaultMarket: DealTicketQuery_market = {
__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 defaultWalletContext = {
keypair: null,
keypairs: [],
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(),
disconnect: jest.fn(),
selectPublicKey: jest.fn(),
connector: null,
};
function setup(
context?: Partial<VegaWalletContextShape>,
market = defaultMarket
) {
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider>
<VegaWalletContext.Provider
value={{ ...defaultWalletContext, ...context }}
>
{children}
</VegaWalletContext.Provider>
</MockedProvider>
);
return renderHook(() => useOrderSubmit(market), { wrapper });
}
it('Has the correct default state', () => {
const { result } = setup();
expect(typeof result.current.submit).toEqual('function');
expect(typeof result.current.reset).toEqual('function');
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
expect(result.current.transaction.txHash).toEqual(null);
expect(result.current.transaction.error).toEqual(null);
});
it('Should not sendTx if no keypair', async () => {
const mockSendTx = jest.fn();
const { result } = setup({ sendTx: mockSendTx, keypairs: [], keypair: null });
await act(async () => {
result.current.submit({} as Order);
});
expect(mockSendTx).not.toHaveBeenCalled();
});
it('Should not sendTx side is not specified', async () => {
const mockSendTx = jest.fn();
const keypair = {
pub: '0x123',
} as VegaKeyExtended;
const { result } = setup({
sendTx: mockSendTx,
keypairs: [keypair],
keypair,
});
await act(async () => {
result.current.submit({} as Order);
});
expect(mockSendTx).not.toHaveBeenCalled();
});
it('Create an Id if a signature is returned', async () => {
const signature =
'597a7706491e6523c091bab1e4d655b62c45a224e80f6cd92ac366aa5dd9a070cc7dd3c6919cb07b81334b876c662dd43bdbe5e827c8baa17a089feb654fab0b';
const expectedId =
'2fe09b0e2e6ed35f8883802629c7d609d3cc2fc9ce3cec0b7824a0d581bd3747';
const successObj = {
tx: {
inputData: 'input-data',
signature: {
algo: 'algo',
version: 1,
value: signature,
},
},
txHash: '0x123',
};
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve(successObj));
const keypair = {
pub: '0x123',
} as VegaKeyExtended;
const { result } = setup({
sendTx: mockSendTx,
keypairs: [keypair],
keypair,
});
await act(async () => {
result.current.submit({
type: OrderType.Market,
side: OrderSide.Buy,
size: '1',
timeInForce: OrderTimeInForce.FOK,
});
});
expect(result.current.id).toEqual(expectedId);
});
it('Should submit a correctly formatted order', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
const keypair = {
pub: '0x123',
} as VegaKeyExtended;
const { result } = setup(
{
sendTx: mockSendTx,
keypairs: [keypair],
keypair,
},
defaultMarket
);
const order: Order = {
type: OrderType.Limit,
size: '10',
timeInForce: OrderTimeInForce.GTT,
side: OrderSide.Buy,
price: '1234567.89',
expiration: new Date('2022-01-01'),
};
await act(async () => {
result.current.submit(order);
});
expect(mockSendTx).toHaveBeenCalledWith({
pubKey: keypair.pub,
propagate: true,
orderSubmission: {
type: OrderType.Limit,
marketId: defaultMarket.id, // Market provided from hook arugment
size: '100', // size adjusted based on positionDecimalPlaces
side: OrderSide.Buy,
timeInForce: OrderTimeInForce.GTT,
price: '123456789', // Decimal removed
expiresAt: order.expiration?.getTime() + '000000', // Nanoseconds appened
},
});
});

View File

@ -1,102 +0,0 @@
import { useCallback, useState } from 'react';
import { useSubscription } from '@apollo/client';
import type { Order } from '../utils/get-default-order';
import type {
OrderEvent,
OrderEventVariables,
OrderEvent_busEvents_event_Order,
} from '@vegaprotocol/wallet';
import {
OrderType,
useVegaWallet,
ORDER_EVENT_SUB,
} from '@vegaprotocol/wallet';
import { determineId, removeDecimal } from '@vegaprotocol/react-helpers';
import { useVegaTransaction } from '@vegaprotocol/wallet';
import type { DealTicketQuery_market } from '../components/__generated__/DealTicketQuery';
export const useOrderSubmit = (market: DealTicketQuery_market) => {
const { keypair } = useVegaWallet();
const { send, transaction, reset: resetTransaction } = useVegaTransaction();
const [id, setId] = useState('');
const [finalizedOrder, setFinalizedOrder] =
useState<OrderEvent_busEvents_event_Order | null>(null);
// Start a subscription looking for the newly created order
useSubscription<OrderEvent, OrderEventVariables>(ORDER_EVENT_SUB, {
variables: { partyId: keypair?.pub || '' },
skip: !id,
onSubscriptionData: ({ subscriptionData }) => {
if (!subscriptionData.data?.busEvents?.length) {
return;
}
// No types available for the subscription result
const matchingOrderEvent = subscriptionData.data.busEvents.find((e) => {
if (e.event.__typename !== 'Order') {
return false;
}
return e.event.id === id;
});
if (
matchingOrderEvent &&
matchingOrderEvent.event.__typename === 'Order'
) {
setFinalizedOrder(matchingOrderEvent.event);
resetTransaction();
}
},
});
const submit = useCallback(
async (order: Order) => {
if (!keypair || !order.side) {
return;
}
setFinalizedOrder(null);
const res = await send({
pubKey: keypair.pub,
propagate: true,
orderSubmission: {
marketId: market.id,
price:
order.type === OrderType.Limit && order.price
? removeDecimal(order.price, market.decimalPlaces)
: undefined,
size: removeDecimal(order.size, market.positionDecimalPlaces),
type: order.type,
side: order.side,
timeInForce: order.timeInForce,
expiresAt: order.expiration
? // Wallet expects timestamp in nanoseconds, we don't have that level of accuracy so
// just append 6 zeroes
order.expiration.getTime().toString() + '000000'
: undefined,
},
});
if (res?.signature) {
setId(determineId(res.signature));
}
},
[market, keypair, send]
);
const reset = useCallback(() => {
resetTransaction();
setFinalizedOrder(null);
setId('');
}, [resetTransaction]);
return {
transaction,
finalizedOrder,
id,
submit,
reset,
};
};

View File

@ -1,4 +1 @@
export * from './components';
export * from './utils/get-default-order';
export * from './hooks/use-order-submit';
export * from './hooks/use-order-validation';

View File

@ -1,28 +0,0 @@
import { OrderTimeInForce, OrderType, OrderSide } from '@vegaprotocol/wallet';
import { toDecimal } from '@vegaprotocol/react-helpers';
import type { DealTicketQuery_market } from '../components/__generated__/DealTicketQuery';
export type Order =
| {
size: string;
type: OrderType.Market;
timeInForce: OrderTimeInForce;
side: OrderSide;
price?: never;
expiration?: never;
}
| {
size: string;
type: OrderType.Limit;
timeInForce: OrderTimeInForce;
side: OrderSide;
price?: string;
expiration?: Date;
};
export const getDefaultOrder = (market: DealTicketQuery_market): Order => ({
type: OrderType.Market,
side: OrderSide.Buy,
timeInForce: OrderTimeInForce.IOC,
size: String(toDecimal(market.positionDecimalPlaces)),
});

View File

@ -3,7 +3,7 @@
// @generated
// This file was automatically generated and should not be edited.
import { Interval } from "@vegaprotocol/types";
import { Interval, MarketState } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: MarketList
@ -108,6 +108,10 @@ export interface MarketList_markets {
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
*/
decimalPlaces: number;
/**
* Current state of the market
*/
state: MarketState;
/**
* marketData for the given market
*/

View File

@ -55,6 +55,7 @@ export const MARKET_LIST_QUERY = gql`
markets {
id
decimalPlaces
state
data {
market {
id

View File

@ -1,4 +1,4 @@
import sortBy from 'lodash/sortBy';
import orderBy from 'lodash/orderBy';
import type {
MarketList,
MarketList_markets,
@ -10,7 +10,7 @@ export const lastPrice = ({ candles }: MarketList_markets) =>
: undefined;
export const mapDataToMarketList = ({ markets }: MarketList) =>
sortBy(
orderBy(
markets?.map((m) => {
return {
id: m.id,
@ -24,8 +24,9 @@ export const mapDataToMarketList = ({ markets }: MarketList) =>
close: m.marketTimestamps.close
? new Date(m.marketTimestamps.close).getTime()
: null,
state: m.state,
};
}) || [],
'open',
'id'
['state', 'open', 'id'],
['asc', 'asc', 'asc']
);

View File

@ -1,7 +0,0 @@
# order-list
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test order-list` to execute the unit tests via [Jest](https://jestjs.io).

View File

@ -1,4 +0,0 @@
{
"name": "@vegaprotocol/order-list",
"version": "0.0.1"
}

View File

@ -1,65 +0,0 @@
import { render, screen } from '@testing-library/react';
import { OrderStatus, OrderType } from '@vegaprotocol/types';
import type { VegaTxState, Order } from '@vegaprotocol/wallet';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import type { CancelDialogProps } from './cancel-dialog';
import { CancelDialog } from './cancel-dialog';
describe('CancelDialog', () => {
let defaultProps: CancelDialogProps;
beforeEach(() => {
defaultProps = {
orderDialogOpen: true,
setOrderDialogOpen: () => false,
transaction: {
status: VegaTxStatus.Default,
error: null,
txHash: null,
signature: null,
},
finalizedOrder: {
status: OrderStatus.Cancelled,
rejectionReason: null,
size: '10',
price: '1000',
market: null,
type: OrderType.Limit,
},
reset: jest.fn(),
};
});
it('should render when an order is successfully cancelled', () => {
render(<CancelDialog {...defaultProps} />);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Order cancelled'
);
});
it('should render when an order is not successfully cancelled', () => {
const transaction: VegaTxState = {
status: VegaTxStatus.Default,
error: null,
txHash: null,
signature: null,
};
const finalizedOrder: Order = {
status: OrderStatus.Active,
rejectionReason: null,
size: '10',
price: '1000',
market: null,
type: OrderType.Limit,
};
const propsForTest = {
transaction,
finalizedOrder,
};
render(<CancelDialog {...defaultProps} {...propsForTest} />);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Cancellation failed'
);
});
});

View File

@ -1,65 +0,0 @@
import { OrderStatus } from '@vegaprotocol/types';
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import type { Order, VegaTxState } from '@vegaprotocol/wallet';
import { VegaTxStatus, VegaOrderTransactionDialog } from '@vegaprotocol/wallet';
import { useEffect } from 'react';
export interface CancelDialogProps {
orderDialogOpen: boolean;
setOrderDialogOpen: (isOpen: boolean) => void;
finalizedOrder: Order | null;
transaction: VegaTxState;
reset: () => void;
}
export const CancelDialog = ({
orderDialogOpen,
setOrderDialogOpen,
finalizedOrder,
transaction,
reset,
}: CancelDialogProps) => {
const getDialogIntent = () => {
if (finalizedOrder) {
if (finalizedOrder.status === OrderStatus.Cancelled) {
return Intent.Success;
}
return Intent.Danger;
}
return Intent.None;
};
useEffect(() => {
if (transaction.status !== VegaTxStatus.Default || finalizedOrder) {
setOrderDialogOpen(true);
} else {
setOrderDialogOpen(false);
}
}, [finalizedOrder, setOrderDialogOpen, transaction.status]);
return (
<Dialog
open={orderDialogOpen}
onChange={(isOpen) => {
setOrderDialogOpen(isOpen);
// If closing reset
if (!isOpen) {
reset();
}
}}
intent={getDialogIntent()}
>
<VegaOrderTransactionDialog
transaction={transaction}
finalizedOrder={finalizedOrder}
title={
finalizedOrder?.status === OrderStatus.Cancelled
? 'Order cancelled'
: 'Cancellation failed'
}
/>
</Dialog>
);
};

View File

@ -1 +0,0 @@
export * from './cancel-dialog';

View File

@ -1 +0,0 @@
export * from './components';

7
libs/orders/README.md Normal file
View File

@ -0,0 +1,7 @@
# Orders
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test orders` to execute the unit tests via [Jest](https://jestjs.io).

View File

@ -1,10 +1,10 @@
module.exports = {
displayName: 'order-list',
displayName: 'orders',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/order-list',
coverageDirectory: '../../coverage/libs/orders',
setupFilesAfterEnv: ['./src/setup-tests.ts'],
};

4
libs/orders/package.json Normal file
View File

@ -0,0 +1,4 @@
{
"name": "@vegaprotocol/orders",
"version": "0.0.1"
}

View File

@ -1,6 +1,6 @@
{
"root": "libs/order-list",
"sourceRoot": "libs/order-list/src",
"root": "libs/orders",
"sourceRoot": "libs/orders/src",
"projectType": "library",
"tags": [],
"targets": {
@ -8,16 +8,16 @@
"executor": "@nrwl/web:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/order-list",
"tsConfig": "libs/order-list/tsconfig.lib.json",
"project": "libs/order-list/package.json",
"entryFile": "libs/order-list/src/index.ts",
"outputPath": "dist/libs/orders",
"tsConfig": "libs/orders/tsconfig.lib.json",
"project": "libs/orders/package.json",
"entryFile": "libs/orders/src/index.ts",
"external": ["react/jsx-runtime"],
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
"compiler": "babel",
"assets": [
{
"glob": "libs/order-list/README.md",
"glob": "libs/orders/README.md",
"input": ".",
"output": "."
}
@ -28,14 +28,14 @@
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/order-list/**/*.{ts,tsx,js,jsx}"]
"lintFilePatterns": ["libs/orders/**/*.{ts,tsx,js,jsx}"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/libs/order-list"],
"outputs": ["coverage/libs/orders"],
"options": {
"jestConfig": "libs/order-list/jest.config.js",
"jestConfig": "libs/orders/jest.config.js",
"passWithNoTests": true
}
},
@ -45,7 +45,7 @@
"uiFramework": "@storybook/react",
"port": 4400,
"config": {
"configFolder": "libs/order-list/.storybook"
"configFolder": "libs/orders/.storybook"
}
},
"configurations": {
@ -59,9 +59,9 @@
"outputs": ["{options.outputPath}"],
"options": {
"uiFramework": "@storybook/react",
"outputPath": "dist/storybook/order-list",
"outputPath": "dist/storybook/orders",
"config": {
"configFolder": "libs/order-list/.storybook"
"configFolder": "libs/orders/.storybook"
}
},
"configurations": {

View File

@ -1,5 +1,4 @@
export * from './__generated__';
export * from './cancel-order-dialog';
export * from './mocks';
export * from './order-data-provider';
export * from './order-list';

View File

@ -22,7 +22,7 @@ export const generateOrders = (override?: PartialDeep<Orders>): Orders => {
return merge(defaultResult, override);
};
export const generateOrder = (partialOrder: Partial<Orders_party_orders>) =>
export const generateOrder = (partialOrder?: Partial<Orders_party_orders>) =>
merge(
{
__typename: 'Order',

View File

@ -70,6 +70,7 @@ export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
return sortOrders(data);
}, [data]);
// We can set <OrderList showCancelled={false} to hide cancelled orders
return (
<AsyncRenderer loading={loading} error={error} data={orders}>
<OrderList ref={gridRef} data={orders} />

View File

@ -2,7 +2,7 @@ import { act, render, screen } from '@testing-library/react';
import { addDecimal, getDateTimeFormat } from '@vegaprotocol/react-helpers';
import type { Orders_party_orders } from '../__generated__/Orders';
import { OrderStatus, OrderRejectionReason } from '@vegaprotocol/types';
import { OrderList } from './order-list';
import { OrderListTable } from './order-list';
import type { PartialDeep } from 'type-fest';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
@ -16,13 +16,13 @@ const generateJsx = (
return (
<MockedProvider>
<VegaWalletContext.Provider value={context as VegaWalletContextShape}>
<OrderList data={orders} />
<OrderListTable data={orders} cancel={jest.fn()} />
</VegaWalletContext.Provider>
</MockedProvider>
);
};
describe('OrderList', () => {
describe('OrderListTable', () => {
it('should show no orders message', async () => {
await act(async () => {
render(generateJsx([]));

View File

@ -1,10 +1,9 @@
import type { Story, Meta } from '@storybook/react';
import { OrderType, OrderStatus } from '@vegaprotocol/types';
import { OrderList, OrderListTable } from './order-list';
import { CancelDialog } from '../cancel-order-dialog';
import { useState } from 'react';
import type { VegaTxState, Order } from '@vegaprotocol/wallet';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import type { Order, VegaTxState } from '@vegaprotocol/wallet';
import { VegaTransactionDialog, VegaTxStatus } from '@vegaprotocol/wallet';
import { generateOrdersArray } from '../mocks';
export default {
@ -38,7 +37,7 @@ const Template2: Story = (args) => {
rejectionReason: null,
size: '10',
price: '1000',
market: null,
market: { name: 'ETH/DAI (30 Jun 2022)', decimalPlaces: 5 },
type: OrderType.Limit,
};
const reset = () => null;
@ -47,12 +46,13 @@ const Template2: Story = (args) => {
<div style={{ height: 1000 }}>
<OrderListTable data={args.data} cancel={cancel} />
</div>
<CancelDialog
<VegaTransactionDialog
orderDialogOpen={open}
setOrderDialogOpen={setOpen}
finalizedOrder={finalizedOrder}
transaction={transaction}
reset={reset}
title={'Order cancelled'}
/>
</>
);

View File

@ -9,27 +9,45 @@ import type {
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import { forwardRef, useState } from 'react';
import { useOrderCancel } from '@vegaprotocol/wallet';
import { CancelDialog } from '../cancel-order-dialog/cancel-dialog';
import BigNumber from 'bignumber.js';
import { useOrderCancel } from '../../order-hooks/use-order-cancel';
import { VegaTransactionDialog } from '@vegaprotocol/wallet';
interface OrderListProps {
data: Orders_party_orders[] | null;
showCancelled?: boolean;
}
export const OrderList = forwardRef<AgGridReact, OrderListProps>(
({ data }, ref) => {
const [orderDialogOpen, setOrderDialogOpen] = useState(false);
const { transaction, finalizedOrder, reset, cancel } = useOrderCancel();
({ data, showCancelled = true }, ref) => {
const [cancelOrderDialogOpen, setCancelOrderDialogOpen] = useState(false);
const { transaction, updatedOrder, reset, cancel } = useOrderCancel();
const ordersData = showCancelled
? data
: data?.filter((o) => o.status !== OrderStatus.Cancelled) || null;
const getDialogTitle = (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';
}
};
return (
<>
<OrderListTable data={data} cancel={cancel} ref={ref} />
<CancelDialog
orderDialogOpen={orderDialogOpen}
setOrderDialogOpen={setOrderDialogOpen}
finalizedOrder={finalizedOrder}
<OrderListTable data={ordersData} cancel={cancel} ref={ref} />
<VegaTransactionDialog
key={`cancel-order-dialog-${transaction.txHash}`}
orderDialogOpen={cancelOrderDialogOpen}
setOrderDialogOpen={setCancelOrderDialogOpen}
finalizedOrder={updatedOrder}
transaction={transaction}
reset={reset}
title={getDialogTitle(updatedOrder?.status)}
/>
</>
);

View File

@ -0,0 +1,4 @@
export * from './components';
export * from './order-hooks';
export * from './utils';
export * from './market';

View File

@ -0,0 +1,13 @@
/* 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;
}

View File

@ -3,7 +3,7 @@
// @generated
// This file was automatically generated and should not be edited.
import { BusEventType, OrderType, OrderStatus, OrderRejectionReason } from "@vegaprotocol/types";
import { BusEventType, OrderType, OrderStatus, OrderRejectionReason, OrderTimeInForce, Side } from "@vegaprotocol/types";
// ====================================================
// GraphQL subscription operation: OrderEvent
@ -68,6 +68,14 @@ export interface OrderEvent_busEvents_event_Order {
* The worst price the order will trade at (e.g. buy for price or less, sell for price or more) (uint64)
*/
price: string;
/**
* The timeInForce of order (determines how and if it executes, and whether it persists on the book)
*/
timeInForce: OrderTimeInForce;
/**
* Whether the order is to buy or sell
*/
side: Side;
/**
* The market the order is trading on (probably stored internally as a hash of the market details)
*/

View File

@ -1,3 +1,5 @@
export * from './__generated__';
export * from './order-event-query';
export * from './use-order-cancel';
export * from './use-order-submit';
export * from './use-order-validation';

View File

@ -13,6 +13,8 @@ export const ORDER_EVENT_SUB = gql`
createdAt
size
price
timeInForce
side
market {
name
decimalPlaces

View File

@ -1,12 +1,19 @@
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 type { VegaKeyExtended, VegaWalletContextShape } from '../context';
import { VegaWalletContext } from '../context';
import { VegaTxStatus } from '../use-vega-transaction';
import type { Order } from '../vega-order-transaction-dialog';
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
import type {
VegaKeyExtended,
VegaWalletContextShape,
} from '@vegaprotocol/wallet';
import { useOrderCancel } from './use-order-cancel';
import type {
OrderEvent,
OrderEvent_busEvents,
} from './__generated__/OrderEvent';
import { ORDER_EVENT_SUB } from './order-event-query';
const defaultMarket = {
__typename: 'Market',
@ -46,8 +53,79 @@ const defaultWalletContext = {
};
function setup(context?: Partial<VegaWalletContextShape>) {
const mocks: MockedResponse<OrderEvent> = {
request: {
query: ORDER_EVENT_SUB,
variables: {
partyId: context?.keypair?.pub || '',
},
},
result: {
data: {
busEvents: [
{
type: 'Order',
event: {
type: 'Limit',
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: 'Active',
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: 'GTC',
side: 'Buy',
market: {
name: 'UNIDAI Monthly (30 Jun 2022)',
decimalPlaces: 5,
__typename: 'Market',
},
__typename: 'Order',
},
__typename: 'BusEvent',
} as OrderEvent_busEvents,
],
},
},
};
const filterMocks: MockedResponse<OrderEvent> = {
request: {
query: ORDER_EVENT_SUB,
variables: {
partyId: context?.keypair?.pub || '',
},
},
result: {
data: {
busEvents: [
{
type: 'Order',
event: {
type: 'Limit',
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: 'Active',
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: 'GTC',
side: 'Buy',
market: {
name: 'UNIDAI Monthly (30 Jun 2022)',
decimalPlaces: 5,
__typename: 'Market',
},
__typename: 'Order',
},
__typename: 'BusEvent',
} as OrderEvent_busEvents,
],
},
},
};
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider>
<MockedProvider mocks={[mocks, filterMocks]}>
<VegaWalletContext.Provider
value={{ ...defaultWalletContext, ...context }}
>
@ -55,6 +133,7 @@ function setup(context?: Partial<VegaWalletContextShape>) {
</VegaWalletContext.Provider>
</MockedProvider>
);
return renderHook(() => useOrderCancel(), { wrapper });
}
@ -70,7 +149,7 @@ describe('useOrderCancel', () => {
it('should not sendTx if no keypair', async () => {
const mockSendTx = jest.fn();
const order: Order = {
const order = {
type: OrderType.Market,
size: '10',
price: '1234567.89',
@ -94,7 +173,7 @@ describe('useOrderCancel', () => {
const keypair = {
pub: '0x123',
} as VegaKeyExtended;
const order: Order = {
const order = {
type: OrderType.Limit,
size: '10',
price: '1234567.89',

View File

@ -0,0 +1,117 @@
import { useCallback, useEffect, useRef, 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 * as Sentry from '@sentry/react';
import { OrderStatus } from '@vegaprotocol/types';
import { determineId } from '@vegaprotocol/react-helpers';
import type { Subscription } from 'zen-observable-ts';
export const useOrderCancel = () => {
const { keypair } = useVegaWallet();
const { send, transaction, reset: resetTransaction } = useVegaTransaction();
const [updatedOrder, setUpdatedOrder] =
useState<OrderEvent_busEvents_event_Order | null>(null);
const client = useApolloClient();
const subRef = useRef<Subscription | null>(null);
useEffect(() => {
return () => {
subRef.current?.unsubscribe();
setUpdatedOrder(null);
resetTransaction();
};
}, [resetTransaction]);
const reset = useCallback(() => {
resetTransaction();
setUpdatedOrder(null);
subRef.current?.unsubscribe();
}, [resetTransaction]);
const cancel = useCallback(
async (order) => {
if (!keypair) {
return;
}
if (
[
OrderStatus.Cancelled,
OrderStatus.Rejected,
OrderStatus.Expired,
OrderStatus.Filled,
OrderStatus.Stopped,
].includes(order.status)
) {
return;
}
setUpdatedOrder(null);
try {
const res = await send({
pubKey: keypair.pub,
propagate: true,
orderCancellation: {
orderId: order.id,
marketId: order.market.id,
},
});
if (res?.signature) {
const resId = order.id ?? determineId(res.signature);
setUpdatedOrder(null);
// setId(resId);
if (resId) {
// Start a subscription looking for the newly created order
subRef.current = client
.subscribe<OrderEvent, OrderEventVariables>({
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;
} catch (e) {
Sentry.captureException(e);
return;
}
},
[client, keypair, send]
);
return {
transaction,
updatedOrder,
cancel,
reset,
};
};

View File

@ -0,0 +1,224 @@
import { act, renderHook } from '@testing-library/react-hooks';
import type { Order } from '../utils';
import type {
VegaKeyExtended,
VegaWalletContextShape,
} from '@vegaprotocol/wallet';
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
import {
VegaWalletOrderSide,
VegaWalletOrderTimeInForce,
VegaWalletOrderType,
} from '@vegaprotocol/wallet';
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import { useOrderSubmit } from './use-order-submit';
import type {
OrderEvent,
OrderEvent_busEvents,
} from './__generated__/OrderEvent';
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';
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',
},
},
} as Market;
const defaultWalletContext = {
keypair: null,
keypairs: [],
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(),
disconnect: jest.fn(),
selectPublicKey: jest.fn(),
connector: null,
};
function setup(
context?: Partial<VegaWalletContextShape>,
market = defaultMarket
) {
const mocks: MockedResponse<OrderEvent> = {
request: {
query: ORDER_EVENT_SUB,
variables: {
partyId: context?.keypair?.pub || '',
},
},
result: {
data: {
busEvents: [
{
type: 'Order',
event: {
type: 'Limit',
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: 'Active',
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: 'GTC',
side: 'Buy',
market: {
name: 'UNIDAI Monthly (30 Jun 2022)',
decimalPlaces: 5,
__typename: 'Market',
},
__typename: 'Order',
},
__typename: 'BusEvent',
} as OrderEvent_busEvents,
],
},
},
};
const filterMocks: MockedResponse<OrderEvent> = {
request: {
query: ORDER_EVENT_SUB,
variables: {
partyId: context?.keypair?.pub || '',
},
},
result: {
data: {
busEvents: [
{
type: 'Order',
event: {
type: 'Limit',
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
status: 'Active',
rejectionReason: null,
createdAt: '2022-07-05T14:25:47.815283706Z',
size: '10',
price: '300000',
timeInForce: 'GTC',
side: 'Buy',
market: {
name: 'UNIDAI Monthly (30 Jun 2022)',
decimalPlaces: 5,
__typename: 'Market',
},
__typename: 'Order',
},
__typename: 'BusEvent',
} as OrderEvent_busEvents,
],
},
},
};
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={[mocks, filterMocks]}>
<VegaWalletContext.Provider
value={{ ...defaultWalletContext, ...context }}
>
{children}
</VegaWalletContext.Provider>
</MockedProvider>
);
return renderHook(() => useOrderSubmit(market), { wrapper });
}
describe('useOrderSubmit', () => {
it('should submit a correctly formatted order', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
const keypair = {
pub: '0x123',
} as VegaKeyExtended;
const { result } = setup({
sendTx: mockSendTx,
keypairs: [keypair],
keypair,
});
const order: Order = {
type: VegaWalletOrderType.Limit,
size: '10',
timeInForce: VegaWalletOrderTimeInForce.GTT,
side: VegaWalletOrderSide.Buy,
price: '1234567.89',
expiration: new Date('2022-01-01'),
};
await act(async () => {
result.current.submit(order);
});
expect(mockSendTx).toHaveBeenCalledWith({
pubKey: keypair.pub,
propagate: true,
orderSubmission: {
type: VegaWalletOrderType.Limit,
marketId: defaultMarket.id, // Market provided from hook argument
size: '100', // size adjusted based on positionDecimalPlaces
side: VegaWalletOrderSide.Buy,
timeInForce: VegaWalletOrderTimeInForce.GTT,
price: '123456789', // Decimal removed
expiresAt: order.expiration?.getTime() + '000000', // Nanoseconds append
},
});
});
it('has the correct default state', () => {
const { result } = setup();
expect(typeof result.current.submit).toEqual('function');
expect(typeof result.current.reset).toEqual('function');
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
expect(result.current.transaction.txHash).toEqual(null);
expect(result.current.transaction.error).toEqual(null);
});
it('should not sendTx if no keypair', async () => {
const mockSendTx = jest.fn();
const { result } = setup({
sendTx: mockSendTx,
keypairs: [],
keypair: null,
});
await act(async () => {
result.current.submit({} as Order);
});
expect(mockSendTx).not.toHaveBeenCalled();
});
it('should not sendTx side is not specified', async () => {
const mockSendTx = jest.fn();
const keypair = {
pub: '0x123',
} as VegaKeyExtended;
const { result } = setup({
sendTx: mockSendTx,
keypairs: [keypair],
keypair,
});
await act(async () => {
result.current.submit({} as Order);
});
expect(mockSendTx).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,123 @@
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 type {
OrderEvent,
OrderEventVariables,
OrderEvent_busEvents_event_Order,
} from './__generated__';
import { VegaWalletOrderType, useVegaWallet } from '@vegaprotocol/wallet';
import { determineId, removeDecimal } 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';
export const useOrderSubmit = (market: Market) => {
const { keypair } = useVegaWallet();
const { send, transaction, reset: resetTransaction } = useVegaTransaction();
const [finalizedOrder, setFinalizedOrder] =
useState<OrderEvent_busEvents_event_Order | null>(null);
const client = useApolloClient();
const subRef = useRef<Subscription | null>(null);
const reset = useCallback(() => {
resetTransaction();
setFinalizedOrder(null);
subRef.current?.unsubscribe();
}, [resetTransaction]);
useEffect(() => {
return () => {
resetTransaction();
setFinalizedOrder(null);
subRef.current?.unsubscribe();
};
}, [resetTransaction]);
const submit = useCallback(
async (order: Order) => {
if (!keypair || !order.side) {
return;
}
setFinalizedOrder(null);
try {
const res = await send({
pubKey: keypair.pub,
propagate: true,
orderSubmission: {
marketId: market.id,
price:
order.type === VegaWalletOrderType.Limit && order.price
? removeDecimal(order.price, market.decimalPlaces)
: undefined,
size: removeDecimal(order.size, market.positionDecimalPlaces),
type: order.type,
side: order.side,
timeInForce: order.timeInForce,
expiresAt: order.expiration
? // Wallet expects timestamp in nanoseconds, we don't have that level of accuracy so
// just append 6 zeroes
order.expiration.getTime().toString() + '000000'
: undefined,
},
});
if (res?.signature) {
const resId = determineId(res.signature);
if (resId) {
// Start a subscription looking for the newly created order
subRef.current = client
.subscribe<OrderEvent, OrderEventVariables>({
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();
}
});
}
}
return res;
} catch (e) {
Sentry.captureException(e);
return;
}
},
[
client,
keypair,
send,
market.id,
market.decimalPlaces,
market.positionDecimalPlaces,
]
);
return {
transaction,
finalizedOrder,
submit,
reset,
};
};

View File

@ -1,7 +1,7 @@
import { renderHook } from '@testing-library/react-hooks';
import {
OrderTimeInForce,
OrderType,
VegaWalletOrderTimeInForce,
VegaWalletOrderType,
useVegaWallet,
} from '@vegaprotocol/wallet';
import type {
@ -11,12 +11,12 @@ import type {
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import type { ValidationProps } from './use-order-validation';
import { useOrderValidation } from './use-order-validation';
import type { DealTicketQuery_market } from '../__generated__/DealTicketQuery';
import { ERROR_SIZE_DECIMAL } from '../utils/validate-size';
import type { Market } from '../market';
jest.mock('@vegaprotocol/wallet');
const market: DealTicketQuery_market = {
const market: Market = {
__typename: 'Market',
id: 'market-id',
decimalPlaces: 2,
@ -59,8 +59,8 @@ const defaultWalletContext = {
const defaultOrder = {
market,
step: 0.1,
orderType: OrderType.Market,
orderTimeInForce: OrderTimeInForce.FOK,
orderType: VegaWalletOrderType.Market,
orderTimeInForce: VegaWalletOrderTimeInForce.FOK,
};
const ERROR = {
@ -71,7 +71,7 @@ const ERROR = {
MARKET_WAITING: 'Market is not active yet',
MARKET_CONTINUOUS_LIMIT:
'Only limit orders are permitted when market is in auction',
MARKET_COUNTINUOUS_TIF:
MARKET_CONTINUOUS_TIF:
'Only GTT, GTC and GFA are permitted when market is in auction',
FIELD_SIZE_REQ: 'An amount needs to be provided',
FIELD_SIZE_MIN: `The amount cannot be lower than "${defaultOrder.step}"`,
@ -135,7 +135,7 @@ it.each`
async ({ tradingMode, errorMessage }) => {
const { result } = setup({
market: { ...defaultOrder.market, tradingMode },
orderType: OrderType.Market,
orderType: VegaWalletOrderType.Market,
});
expect(result.current).toEqual(errorMessage);
}
@ -143,21 +143,21 @@ it.each`
it.each`
tradingMode | orderTimeInForce | errorMessage
${MarketTradingMode.BatchAuction} | ${OrderTimeInForce.FOK} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.MonitoringAuction} | ${OrderTimeInForce.FOK} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.OpeningAuction} | ${OrderTimeInForce.FOK} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.BatchAuction} | ${OrderTimeInForce.IOC} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.MonitoringAuction} | ${OrderTimeInForce.IOC} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.OpeningAuction} | ${OrderTimeInForce.IOC} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.BatchAuction} | ${OrderTimeInForce.GFN} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.MonitoringAuction} | ${OrderTimeInForce.GFN} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.OpeningAuction} | ${OrderTimeInForce.GFN} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.BatchAuction} | ${VegaWalletOrderTimeInForce.FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
${MarketTradingMode.MonitoringAuction} | ${VegaWalletOrderTimeInForce.FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
${MarketTradingMode.OpeningAuction} | ${VegaWalletOrderTimeInForce.FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
${MarketTradingMode.BatchAuction} | ${VegaWalletOrderTimeInForce.IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
${MarketTradingMode.MonitoringAuction} | ${VegaWalletOrderTimeInForce.IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
${MarketTradingMode.OpeningAuction} | ${VegaWalletOrderTimeInForce.IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
${MarketTradingMode.BatchAuction} | ${VegaWalletOrderTimeInForce.GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
${MarketTradingMode.MonitoringAuction} | ${VegaWalletOrderTimeInForce.GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
${MarketTradingMode.OpeningAuction} | ${VegaWalletOrderTimeInForce.GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
`(
'Returns an error message when submitting a limit order with a "$orderTimeInForce" value to a "$tradingMode" market',
async ({ tradingMode, orderTimeInForce, errorMessage }) => {
const { result } = setup({
market: { ...defaultOrder.market, tradingMode },
orderType: OrderType.Limit,
orderType: VegaWalletOrderType.Limit,
orderTimeInForce,
});
expect(result.current).toEqual(errorMessage);

View File

@ -1,19 +1,19 @@
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,
OrderTimeInForce,
OrderType,
VegaWalletOrderTimeInForce as OrderTimeInForce,
VegaWalletOrderType as OrderType,
} from '@vegaprotocol/wallet';
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import type { Order } from '../utils/get-default-order';
import type { Market } from '../market';
import { ERROR_SIZE_DECIMAL } from '../utils/validate-size';
import type { DealTicketQuery_market } from '../components/__generated__/DealTicketQuery';
export type ValidationProps = {
step: number;
market: DealTicketQuery_market;
market: Market;
orderType: OrderType;
orderTimeInForce: OrderTimeInForce;
fieldErrors?: FieldErrors<Order>;

View File

@ -0,0 +1,32 @@
import {
VegaWalletOrderTimeInForce,
VegaWalletOrderType,
VegaWalletOrderSide,
} from '@vegaprotocol/wallet';
import { toDecimal } from '@vegaprotocol/react-helpers';
import type { Market } from '../market';
export type Order =
| {
size: string;
type: VegaWalletOrderType.Market;
timeInForce: VegaWalletOrderTimeInForce;
side: VegaWalletOrderSide;
price?: never;
expiration?: never;
}
| {
size: string;
type: VegaWalletOrderType.Limit;
timeInForce: VegaWalletOrderTimeInForce;
side: VegaWalletOrderSide;
price?: string;
expiration?: Date;
};
export const getDefaultOrder = (market: Market): Order => ({
type: VegaWalletOrderType.Market,
side: VegaWalletOrderSide.Buy,
timeInForce: VegaWalletOrderTimeInForce.IOC,
size: String(toDecimal(market.positionDecimalPlaces)),
});

View File

@ -0,0 +1,2 @@
export * from './get-default-order';
export * from './validate-size';

View File

@ -7,7 +7,7 @@ import {
import { LocalStorage } from '@vegaprotocol/react-helpers';
import { WALLET_CONFIG } from '../storage-keys';
import type { VegaConnector } from './vega-connector';
import type { TransactionSubmission } from '../types';
import type { TransactionSubmission } from '../wallet-types';
// Perhaps there should be a default ConnectorConfig that others can extend off. Do all connectors
// need to use local storage, I don't think so...

View File

@ -2,7 +2,7 @@ import type {
VegaKey,
TransactionResponse,
} from '@vegaprotocol/vegawallet-service-api-client';
import type { TransactionSubmission } from '../types';
import type { TransactionSubmission } from '../wallet-types';
type ErrorResponse =
| {

View File

@ -4,7 +4,7 @@ import type {
} from '@vegaprotocol/vegawallet-service-api-client';
import { createContext } from 'react';
import type { VegaConnector } from './connectors';
import type { TransactionSubmission } from './types';
import type { TransactionSubmission } from './wallet-types';
export type SendTxError =
| {

View File

@ -2,11 +2,10 @@ export * from './context';
export * from './use-vega-wallet';
export * from './connectors';
export * from './storage-keys';
export * from './types';
export * from './wallet-types';
export * from './use-vega-transaction';
export * from './use-eager-connect';
export * from './manage-dialog';
export * from './vega-order-transaction-dialog';
export * from './vega-transaction-dialog';
export * from './provider';
export * from './order-hooks';
export * from './connect-dialog';

View File

@ -0,0 +1 @@
export * from './manage-dialog';

View File

@ -1,9 +1,9 @@
/* eslint-disable jest/no-conditional-expect */
import { fireEvent, render, screen, within } from '@testing-library/react';
import { VegaWalletContext } from './context';
import type { VegaWalletContextShape, VegaKeyExtended } from './context';
import type { VegaManageDialogProps } from './manage-dialog';
import { VegaManageDialog } from './manage-dialog';
import { VegaWalletContext } from '../context';
import type { VegaWalletContextShape, VegaKeyExtended } from '../context';
import type { VegaManageDialogProps } from '.';
import { VegaManageDialog } from '.';
let props: VegaManageDialogProps;
let context: Partial<VegaWalletContextShape>;

View File

@ -6,7 +6,7 @@ import {
Intent,
Icon,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '.';
import { useVegaWallet } from '..';
export interface VegaManageDialogProps {
dialogOpen: boolean;

View File

@ -1,83 +0,0 @@
import { useCallback, useState } from 'react';
import { determineId } from '@vegaprotocol/react-helpers';
import { useVegaTransaction } from '../use-vega-transaction';
import { useVegaWallet } from '../use-vega-wallet';
import { useSubscription } from '@apollo/client';
import type {
OrderEvent,
OrderEventVariables,
OrderEvent_busEvents_event_Order,
} from './__generated__/OrderEvent';
import { ORDER_EVENT_SUB } from './order-event-query';
import * as Sentry from '@sentry/react';
export const useOrderCancel = () => {
const { keypair } = useVegaWallet();
const { send, transaction, reset: resetTransaction } = useVegaTransaction();
const [updatedOrder, setUpdatedOrder] =
useState<OrderEvent_busEvents_event_Order | null>(null);
const [id, setId] = useState('');
// Start a subscription looking for the newly created order
useSubscription<OrderEvent, OrderEventVariables>(ORDER_EVENT_SUB, {
variables: { partyId: keypair?.pub || '' },
skip: !id,
onSubscriptionData: ({ subscriptionData }) => {
if (!subscriptionData.data?.busEvents?.length) {
return;
}
// No types available for the subscription result
const matchingOrderEvent = subscriptionData.data.busEvents[0].event;
if (matchingOrderEvent && matchingOrderEvent.__typename === 'Order') {
setUpdatedOrder(matchingOrderEvent);
resetTransaction();
}
},
});
const cancel = useCallback(
async (order) => {
if (!keypair) {
return;
}
setUpdatedOrder(null);
try {
const res = await send({
pubKey: keypair.pub,
propagate: true,
orderCancellation: {
orderId: order.id,
marketId: order.market.id,
},
});
if (res?.signature) {
setId(determineId(res.signature));
}
return res;
} catch (e) {
Sentry.captureException(e);
return;
}
},
[keypair, send]
);
const reset = useCallback(() => {
resetTransaction();
setUpdatedOrder(null);
setId('');
}, [resetTransaction]);
return {
transaction,
finalizedOrder: updatedOrder,
id,
cancel,
reset,
};
};

View File

@ -5,7 +5,7 @@ import type { VegaKeyExtended, VegaWalletContextShape } from '.';
import type { VegaConnector } from './connectors/vega-connector';
import { VegaWalletContext } from './context';
import { WALLET_KEY } from './storage-keys';
import type { TransactionSubmission } from './types';
import type { TransactionSubmission } from './wallet-types';
interface VegaWalletProviderProps {
children: ReactNode;

View File

@ -1,5 +1,5 @@
import { useCallback, useState } from 'react';
import type { TransactionSubmission } from './types';
import type { TransactionSubmission } from './wallet-types';
import { useVegaWallet } from './use-vega-wallet';
import type { SendTxError } from './context';

View File

@ -1 +0,0 @@
export * from './vega-order-transaction-dialog';

View File

@ -1,159 +0,0 @@
import { render, screen } from '@testing-library/react';
import { OrderStatus, OrderType } from '@vegaprotocol/types';
import type { VegaTxState } from '../use-vega-transaction';
import { VegaTxStatus } from '../use-vega-transaction';
import type { Order } from './vega-order-transaction-dialog';
import { VegaOrderTransactionDialog } from './vega-order-transaction-dialog';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_EXPLORER_URL: 'https://test.explorer.vega.network',
}),
}));
describe('VegaOrderTransactionDialog', () => {
it('should render when an order is successful', () => {
const transaction: VegaTxState = {
status: VegaTxStatus.Default,
error: null,
txHash: null,
signature: null,
};
const finalizedOrder: Order = {
status: OrderStatus.Active,
rejectionReason: null,
size: '10',
price: '1000',
market: null,
type: OrderType.Limit,
};
render(
<VegaOrderTransactionDialog
finalizedOrder={finalizedOrder}
transaction={transaction}
/>
);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Order placed'
);
});
it('should render when transaction is requested', () => {
const transaction: VegaTxState = {
status: VegaTxStatus.Requested,
error: null,
txHash: null,
signature: null,
};
const finalizedOrder: Order = {
status: OrderStatus.Active,
rejectionReason: null,
size: '10',
price: '1000',
market: null,
type: OrderType.Limit,
};
render(
<VegaOrderTransactionDialog
finalizedOrder={finalizedOrder}
transaction={transaction}
/>
);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Confirm transaction in wallet'
);
});
it('should render when transaction has error', () => {
const transaction: VegaTxState = {
status: VegaTxStatus.Error,
error: null,
txHash: null,
signature: null,
};
const finalizedOrder: Order = {
status: OrderStatus.Active,
rejectionReason: null,
size: '10',
price: '1000',
market: null,
type: OrderType.Limit,
};
render(
<VegaOrderTransactionDialog
finalizedOrder={finalizedOrder}
transaction={transaction}
/>
);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Order rejected by wallet'
);
});
it('should render when an order is rejected', () => {
const transaction: VegaTxState = {
status: VegaTxStatus.Default,
error: null,
txHash: null,
signature: null,
};
const finalizedOrder: Order = {
status: OrderStatus.Rejected,
rejectionReason: null,
size: '10',
price: '1000',
market: null,
type: OrderType.Limit,
};
render(
<VegaOrderTransactionDialog
finalizedOrder={finalizedOrder}
transaction={transaction}
/>
);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Order failed'
);
});
it('should render when pending consensus', () => {
const transaction: VegaTxState = {
status: VegaTxStatus.Error,
error: null,
txHash: null,
signature: null,
};
render(
<VegaOrderTransactionDialog
finalizedOrder={null}
transaction={transaction}
/>
);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Order rejected by wallet'
);
});
it('should render awaiting network confirmation and add link to tx in block explorer', () => {
const transaction: VegaTxState = {
status: VegaTxStatus.Default,
error: null,
txHash: 'TxHash',
signature: null,
};
render(
<VegaOrderTransactionDialog
finalizedOrder={null}
transaction={transaction}
/>
);
expect(screen.getByTestId('order-status-header')).toHaveTextContent(
'Awaiting network confirmation'
);
expect(screen.getByTestId('tx-hash')).toHaveTextContent('TxHash');
expect(screen.getByTestId('tx-hash')).toHaveAttribute(
'href',
'https://test.explorer.vega.network/txs/0xTxHash'
);
});
});

View File

@ -1,160 +0,0 @@
import { Icon, Loader } from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import {
addDecimal,
addDecimalsFormatNumber,
t,
} from '@vegaprotocol/react-helpers';
import type { VegaTxState } from '../use-vega-transaction';
import { VegaTxStatus } from '../use-vega-transaction';
import { useEnvironment } from '@vegaprotocol/environment';
export interface Market {
name: string;
positionDecimalPlaces?: number;
decimalPlaces: number;
}
export interface Order {
status: string;
rejectionReason: string | null;
size: string;
price: string;
market: Market | null;
type: string | null;
}
interface VegaOrderTransactionDialogProps {
transaction: VegaTxState;
finalizedOrder: Order | null;
title?: string;
}
export const VegaOrderTransactionDialog = ({
transaction,
finalizedOrder,
title = 'Order placed',
}: VegaOrderTransactionDialogProps) => {
const { VEGA_EXPLORER_URL } = useEnvironment();
// Rejected by wallet
if (transaction.status === VegaTxStatus.Requested) {
return (
<OrderDialogWrapper
title="Confirm transaction in wallet"
icon={<Icon name="hand-up" size={20} />}
>
<p>
Please open your wallet application and confirm or reject the
transaction
</p>
</OrderDialogWrapper>
);
}
// Transaction error
if (transaction.status === VegaTxStatus.Error) {
return (
<OrderDialogWrapper
title="Order rejected by wallet"
icon={<Icon name="warning-sign" size={20} />}
>
{transaction.error && (
<pre className="text-ui break-all whitespace-pre-wrap">
{JSON.stringify(transaction.error, null, 2)}
</pre>
)}
</OrderDialogWrapper>
);
}
// Pending consensus
if (!finalizedOrder) {
return (
<OrderDialogWrapper
title="Awaiting network confirmation"
icon={<Loader size="small" />}
>
{transaction.txHash && (
<p className="break-all">
Tx hash: &nbsp;
<a
className="underline"
data-testid="tx-hash"
href={`${VEGA_EXPLORER_URL}/txs/0x${transaction.txHash}`}
target="_blank"
rel="noreferrer"
>
{transaction.txHash}
</a>
</p>
)}
</OrderDialogWrapper>
);
}
// Order on network but was rejected
if (finalizedOrder.status === 'Rejected') {
return (
<OrderDialogWrapper
title="Order failed"
icon={<Icon name="warning-sign" size={20} />}
>
<p data-testid="error-reason">
{t(`Reason: ${finalizedOrder.rejectionReason}`)}
</p>
</OrderDialogWrapper>
);
}
return (
<OrderDialogWrapper title={title} icon={<Icon name="tick" size={20} />}>
<p>{t(`Status: ${finalizedOrder.status}`)}</p>
{finalizedOrder.market && (
<p>{t(`Market: ${finalizedOrder.market.name}`)}</p>
)}
<p>{t(`Type: ${finalizedOrder.type}`)}</p>
<p>
{t(
`Amount: ${addDecimal(
finalizedOrder.size,
finalizedOrder.market?.positionDecimalPlaces || 0
)}`
)}
</p>
{finalizedOrder.type === 'Limit' && finalizedOrder.market && (
<p>
{t(
`Price: ${addDecimalsFormatNumber(
finalizedOrder.price,
finalizedOrder.market.decimalPlaces
)}`
)}
</p>
)}
</OrderDialogWrapper>
);
};
interface OrderDialogWrapperProps {
children: ReactNode;
icon: ReactNode;
title: string;
}
const OrderDialogWrapper = ({
children,
icon,
title,
}: OrderDialogWrapperProps) => {
return (
<div className="flex gap-12 max-w-full">
<div className="pt-8 fill-current">{icon}</div>
<div data-testid="order-wrapper" className="flex-1">
<h1 data-testid="order-status-header" className="text-h4">
{title}
</h1>
{children}
</div>
</div>
);
};

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