Feat/471 cancel order (#610)

* chore: [#471] update @vegaprotocol/vegawallet-service-api-client to 0.4.12

* fix: [#471] set up storybook in order-list lib and add tailwind

* fix: [#471] organize order list components

* chore: [471] pull theme switcher changes

* feat: [#471] add cancel order button

* feat: [#471] initial impl of use order cancel hook

* fix: [#471] fix format of the price in order list

* fix: #471 fix static assets issue when merging

* fix:  #471 refactor order dialog to vega tx dialog

* fix: #471 move use cancel order hook in wallet lib

* fix: [#471] cancel order dialog and hook refactor

* fix: [#471] remove commented code from storybook preview and fix test

* fix: [#471] update order-list.tsx

* fix: [#471] fix update subscription - show order is cancelled

* fix: [#471] fix eslint error

* chore: [#471] refactoring and add tests for dialogs and cancel hook

* fix: #471 add ref to order list table

* fix: #471 add field for cancel fix test

* fix: #471 rename vega-order-transaction-dialog, error handiling, open dialog on finalized order

* fix: #471 sendTx body mandatory

* fix: #471 use BusEventType.Order to check the typename

* fix: #471 revert using BusEventType.Order to check the typename

* Update libs/wallet/src/order-hooks/use-order-cancel.tsx

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

* fix: #471  fix order-list refactoring and fixes

* fix: #471 generate orders added as a mock in order-list

* fix: #471 reset transaction after order updated

* fix: #471 remove unused import useEffect

* fix: #471 generate mock orders

* fix: #471 revert generate mock orders

* fix: #471 order list price set to display all decimals

* fix: #471 generate orders updates

* fix: #471 remove unused import

* fix: #471 remove __typename from mock orders genOrder

* Update libs/wallet/src/order-hooks/order-event-query.ts

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

* fix: #471 update order event sub and pull master changes

Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>
This commit is contained in:
m.ray 2022-06-29 11:03:20 +02:00 committed by GitHub
parent cc51007e6a
commit 0473412487
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 1661 additions and 302 deletions

View File

@ -16,7 +16,7 @@ The trading interface built based on a component toolkit. It will provide a way
### [Token](./apps/token)
The utlity dApp for interacting with the Vega token and using its' utility. This includes; delegation, nomination, governance and redemption of tokens.
The utility dApp for interacting with the Vega token and using its' utility. This includes; delegation, nomination, governance and redemption of tokens.
### [Explorer](./apps/explorer)
@ -51,7 +51,7 @@ A utility library for connecting to the Ethereum network and interacting with Ve
### [React Helpers](./libs/react-helpers)
Generic react helpers that can be used across multilpe applications, along with other utilities.
Generic react helpers that can be used across multiple applications, along with other utilities.
# 💻 Develop

View File

@ -152,7 +152,7 @@ describe('deal ticket orders', () => {
it.skip('cannot place an order if market is suspended');
it.skip('cannot place an order if size is 0');
it.skip('cannot place an order expiry date is invalid');
it.skip('unsuccessfull order due to no collateral');
it.skip('unsuccessful order due to no collateral');
});
describe('deal ticket validation', () => {

View File

@ -2,9 +2,8 @@ import type { ReactNode } from 'react';
import { useEffect, useState } from 'react';
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { OrderStatus } from '@vegaprotocol/types';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import { VegaOrderTransactionDialog, VegaTxStatus } from '@vegaprotocol/wallet';
import { DealTicket } from './deal-ticket';
import { OrderDialog } from './order-dialog';
import { useOrderSubmit } from '../hooks/use-order-submit';
import type { DealTicketQuery_market } from './__generated__/DealTicketQuery';
@ -49,12 +48,12 @@ export const DealTicketManager = ({
};
useEffect(() => {
if (transaction.status !== VegaTxStatus.Default) {
if (transaction.status !== VegaTxStatus.Default || finalizedOrder) {
setOrderDialogOpen(true);
} else {
setOrderDialogOpen(false);
}
}, [transaction.status]);
}, [finalizedOrder, transaction.status]);
return (
<>
@ -82,7 +81,7 @@ export const DealTicketManager = ({
}}
intent={getDialogIntent(transaction.status)}
>
<OrderDialog
<VegaOrderTransactionDialog
transaction={transaction}
finalizedOrder={finalizedOrder}
/>

View File

@ -7,7 +7,6 @@ export * from './deal-ticket-market-amount';
export * from './deal-ticket';
export * from './expiry-selector';
export * from './info-market';
export * from './order-dialog';
export * from './side-selector';
export * from './time-in-force-selector';
export * from './type-selector';

View File

@ -10,7 +10,7 @@ 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 '../__generated__/DealTicketQuery';
import type { DealTicketQuery_market } from '../components/__generated__/DealTicketQuery';
const defaultMarket: DealTicketQuery_market = {
__typename: 'Market',

View File

@ -1,41 +1,19 @@
import { useCallback, useEffect, useState } from 'react';
import { gql, useSubscription } from '@apollo/client';
import { useCallback, useState } from 'react';
import { useSubscription } from '@apollo/client';
import type { Order } from '../utils/get-default-order';
import { OrderType, useVegaWallet } from '@vegaprotocol/wallet';
import { determineId, removeDecimal } from '@vegaprotocol/react-helpers';
import { useVegaTransaction } from '@vegaprotocol/wallet';
import type { DealTicketQuery_market } from '../components/__generated__/DealTicketQuery';
import type {
OrderEvent,
OrderEventVariables,
OrderEvent_busEvents_event_Order,
} from './__generated__/OrderEvent';
const ORDER_EVENT_SUB = gql`
subscription OrderEvent($partyId: ID!) {
busEvents(partyId: $partyId, batchSize: 0, types: [Order]) {
eventId
block
type
event {
... on Order {
type
id
status
rejectionReason
createdAt
size
price
market {
name
decimalPlaces
positionDecimalPlaces
}
}
}
}
}
`;
} 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();
@ -67,16 +45,11 @@ export const useOrderSubmit = (market: DealTicketQuery_market) => {
matchingOrderEvent.event.__typename === 'Order'
) {
setFinalizedOrder(matchingOrderEvent.event);
resetTransaction();
}
},
});
useEffect(() => {
if (finalizedOrder) {
resetTransaction();
}
}, [finalizedOrder, resetTransaction]);
const submit = useCallback(
async (order: Order) => {
if (!keypair || !order.side) {

View File

@ -18,7 +18,7 @@ export const SelectMarketDialog = ({
open={dialogOpen}
onChange={() => setDialogOpen(false)}
titleClassNames="font-bold font-sans text-3xl tracking-tight mb-0 pl-8"
contentClassNames="w-full md:w-[1120px]"
contentClassNames="w-full lg:w-[1020px]"
>
<div className="h-[200px] w-full">
<MarketsContainer />

View File

@ -0,0 +1,28 @@
const rootMain = require('../../../.storybook/main');
module.exports = {
...rootMain,
core: { ...rootMain.core, builder: 'webpack5' },
stories: [
...rootMain.stories,
'../src/**/*.stories.mdx',
'../src/**/*.stories.@(js|jsx|ts|tsx)',
],
addons: [
...rootMain.addons,
'@nrwl/react/plugins/storybook',
'storybook-addon-themes',
],
webpackFinal: async (config, { configType }) => {
// apply any global webpack configs that might have been specified in .storybook/main.js
if (rootMain.webpackFinal) {
config = await rootMain.webpackFinal(config, { configType });
}
// add your own webpack tweaks if needed
return config;
},
};

View File

@ -0,0 +1 @@
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />

View File

@ -0,0 +1,11 @@
import '../src/styles.scss';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
themes: {
default: 'dark',
list: [
{ name: 'dark', class: ['dark', 'bg-black'], color: '#000' },
{ name: 'light', class: '', color: '#FFF' },
],
},
};

View File

@ -0,0 +1,19 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"outDir": ""
},
"files": [
"../../../node_modules/@nrwl/react/typings/styled-jsx.d.ts",
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": [
"../**/*.spec.ts",
"../**/*.spec.js",
"../**/*.spec.tsx",
"../**/*.spec.jsx"
],
"include": ["../src/**/*", "*.js"]
}

View File

@ -0,0 +1,10 @@
const { join } = require('path');
module.exports = {
plugins: {
tailwindcss: {
config: join(__dirname, 'tailwind.config.js'),
},
autoprefixer: {},
},
};

View File

@ -38,6 +38,37 @@
"jestConfig": "libs/order-list/jest.config.js",
"passWithNoTests": true
}
},
"storybook": {
"executor": "@nrwl/storybook:storybook",
"options": {
"uiFramework": "@storybook/react",
"port": 4400,
"config": {
"configFolder": "libs/order-list/.storybook"
}
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"build-storybook": {
"executor": "@nrwl/storybook:build",
"outputs": ["{options.outputPath}"],
"options": {
"uiFramework": "@storybook/react",
"outputPath": "dist/storybook/order-list",
"config": {
"configFolder": "libs/order-list/.storybook"
}
},
"configurations": {
"ci": {
"quiet": true
}
}
}
}
}

View File

@ -1,3 +1 @@
export * from './lib/order-list';
export * from './lib/order-list-container';
export * from './lib/__generated__/Orders';
export * from './lib';

View File

@ -0,0 +1,3 @@
export * from './OrderFields';
export * from './OrderSub';
export * from './Orders';

View File

@ -0,0 +1,65 @@
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

@ -0,0 +1,65 @@
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

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

View File

@ -0,0 +1,7 @@
export * from './__generated__';
export * from './cancel-order-dialog';
export * from './mocks';
export * from './order-data-provider';
export * from './order-list';
export * from './order-list-manager';
export * from './order-list-container';

View File

@ -0,0 +1,156 @@
import merge from 'lodash/merge';
import type { PartialDeep } from 'type-fest';
import {
OrderStatus,
OrderTimeInForce,
OrderType,
Side,
} from '@vegaprotocol/types';
import type { Orders, Orders_party_orders } from '../__generated__/Orders';
export const generateOrders = (override?: PartialDeep<Orders>): Orders => {
const orders: Orders_party_orders[] = generateOrdersArray();
const defaultResult = {
party: {
id: 'party-id',
orders,
__typename: 'Party',
},
};
return merge(defaultResult, override);
};
export const generateOrder = (partialOrder: Partial<Orders_party_orders>) =>
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',
},
},
},
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_orders,
partialOrder
);
export const limitOrder = generateOrder({
id: 'limit-order',
type: OrderType.Limit,
status: OrderStatus.Active,
timeInForce: OrderTimeInForce.GTT,
createdAt: new Date('2022-3-3').toISOString(),
expiresAt: new Date('2022-3-5').toISOString(),
});
export const marketOrder = generateOrder({
id: 'market-order',
type: OrderType.Market,
status: OrderStatus.Active,
});
export const generateMockOrders = (): Orders_party_orders[] => {
return [
generateOrder({
id: '066468C06549101DAF7BC51099E1412A0067DC08C246B7D8013C9D0CBF1E8EE7',
market: {
__typename: 'Market',
id: 'c9f5acd348796011c075077e4d58d9b7f1689b7c1c8e030a5e886b83aa96923d',
name: 'AAVEDAI Monthly (30 Jun 2022)',
decimalPlaces: 5,
positionDecimalPlaces: 0,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
code: 'AAVEDAI.MF21',
},
},
},
size: '10',
type: OrderType.Limit,
status: OrderStatus.Filled,
side: Side.Buy,
remaining: '0',
price: '20000000',
timeInForce: OrderTimeInForce.GTC,
createdAt: new Date(2020, 1, 1).toISOString(),
}),
generateOrder({
id: '48DB6767E4E4E0F649C5A13ABFADE39F8451C27DA828DAF14B7A1E8E5EBDAD99',
market: {
__typename: 'Market',
id: '5a4b0b9e9c0629f0315ec56fcb7bd444b0c6e4da5ec7677719d502626658a376',
name: 'Tesla Quarterly (30 Jun 2022)',
decimalPlaces: 5,
positionDecimalPlaces: 0,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
code: 'TSLA.QM21',
},
},
},
size: '1',
type: OrderType.Limit,
status: OrderStatus.Filled,
side: Side.Buy,
remaining: '0',
price: '100',
timeInForce: OrderTimeInForce.GTC,
createdAt: new Date().toISOString(),
}),
generateOrder({
id: '4e93702990712c41f6995fcbbd94f60bb372ad12d64dfa7d96d205c49f790336',
market: {
__typename: 'Market',
id: 'c6f4337b31ed57a961969c3ba10297b369d01b9e75a4cbb96db4fc62886444e6',
name: 'BTCUSD Monthly (30 Jun 2022)',
decimalPlaces: 5,
positionDecimalPlaces: 0,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
code: 'BTCUSD.MF21',
},
},
},
size: '1',
type: OrderType.Limit,
status: OrderStatus.Filled,
side: Side.Buy,
remaining: '0',
price: '20000',
timeInForce: OrderTimeInForce.GTC,
createdAt: new Date(2022, 5, 10).toISOString(),
}),
];
};
export const generateOrdersArray = (): Orders_party_orders[] => {
return [marketOrder, limitOrder, ...generateMockOrders()];
};

View File

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

View File

@ -0,0 +1,121 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "@vegaprotocol/types";
// ====================================================
// GraphQL fragment: OrderFields
// ====================================================
export interface OrderFields_market_tradableInstrument_instrument {
__typename: "Instrument";
/**
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
*/
code: string;
}
export interface OrderFields_market_tradableInstrument {
__typename: "TradableInstrument";
/**
* An instance of or reference to a fully specified instrument.
*/
instrument: OrderFields_market_tradableInstrument_instrument;
}
export interface OrderFields_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* 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;
/**
* An instance of or reference to a tradable instrument.
*/
tradableInstrument: OrderFields_market_tradableInstrument;
}
export interface OrderFields {
__typename: "Order";
/**
* Hash of the order data
*/
id: string;
/**
* The market the order is trading on (probably stored internally as a hash of the market details)
*/
market: OrderFields_market | null;
/**
* Type the order type (defaults to PARTY)
*/
type: OrderType | null;
/**
* Whether the order is to buy or sell
*/
side: Side;
/**
* Total number of contracts that may be bought or sold (immutable) (uint64)
*/
size: string;
/**
* The status of an order, for example 'Active'
*/
status: OrderStatus;
/**
* Reason for the order to be rejected
*/
rejectionReason: OrderRejectionReason | null;
/**
* 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;
/**
* Number of contracts remaining of the total that have not yet been bought or sold (uint64)
*/
remaining: string;
/**
* Expiration time of this order (ISO-8601 RFC3339+Nano formatted date)
*/
expiresAt: string | null;
/**
* RFC3339Nano formatted date and time for when the order was created (timestamp)
*/
createdAt: string;
/**
* RFC3339Nano time the order was altered
*/
updatedAt: string | null;
}

View File

@ -0,0 +1,132 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "@vegaprotocol/types";
// ====================================================
// GraphQL subscription operation: OrderSub
// ====================================================
export interface OrderSub_orders_market_tradableInstrument_instrument {
__typename: "Instrument";
/**
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
*/
code: string;
}
export interface OrderSub_orders_market_tradableInstrument {
__typename: "TradableInstrument";
/**
* An instance of or reference to a fully specified instrument.
*/
instrument: OrderSub_orders_market_tradableInstrument_instrument;
}
export interface OrderSub_orders_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* 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;
/**
* An instance of or reference to a tradable instrument.
*/
tradableInstrument: OrderSub_orders_market_tradableInstrument;
}
export interface OrderSub_orders {
__typename: "Order";
/**
* Hash of the order data
*/
id: string;
/**
* The market the order is trading on (probably stored internally as a hash of the market details)
*/
market: OrderSub_orders_market | null;
/**
* Type the order type (defaults to PARTY)
*/
type: OrderType | null;
/**
* Whether the order is to buy or sell
*/
side: Side;
/**
* Total number of contracts that may be bought or sold (immutable) (uint64)
*/
size: string;
/**
* The status of an order, for example 'Active'
*/
status: OrderStatus;
/**
* Reason for the order to be rejected
*/
rejectionReason: OrderRejectionReason | null;
/**
* 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;
/**
* Number of contracts remaining of the total that have not yet been bought or sold (uint64)
*/
remaining: string;
/**
* Expiration time of this order (ISO-8601 RFC3339+Nano formatted date)
*/
expiresAt: string | null;
/**
* RFC3339Nano formatted date and time for when the order was created (timestamp)
*/
createdAt: string;
/**
* RFC3339Nano time the order was altered
*/
updatedAt: string | null;
}
export interface OrderSub {
/**
* Subscribe to orders updates
*/
orders: OrderSub_orders[] | null;
}
export interface OrderSubVariables {
partyId: string;
}

View File

@ -0,0 +1,144 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: Orders
// ====================================================
export interface Orders_party_orders_market_tradableInstrument_instrument {
__typename: "Instrument";
/**
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
*/
code: string;
}
export interface Orders_party_orders_market_tradableInstrument {
__typename: "TradableInstrument";
/**
* An instance of or reference to a fully specified instrument.
*/
instrument: Orders_party_orders_market_tradableInstrument_instrument;
}
export interface Orders_party_orders_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* 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;
/**
* An instance of or reference to a tradable instrument.
*/
tradableInstrument: Orders_party_orders_market_tradableInstrument;
}
export interface Orders_party_orders {
__typename: "Order";
/**
* Hash of the order data
*/
id: string;
/**
* The market the order is trading on (probably stored internally as a hash of the market details)
*/
market: Orders_party_orders_market | null;
/**
* Type the order type (defaults to PARTY)
*/
type: OrderType | null;
/**
* Whether the order is to buy or sell
*/
side: Side;
/**
* Total number of contracts that may be bought or sold (immutable) (uint64)
*/
size: string;
/**
* The status of an order, for example 'Active'
*/
status: OrderStatus;
/**
* Reason for the order to be rejected
*/
rejectionReason: OrderRejectionReason | null;
/**
* 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;
/**
* Number of contracts remaining of the total that have not yet been bought or sold (uint64)
*/
remaining: string;
/**
* Expiration time of this order (ISO-8601 RFC3339+Nano formatted date)
*/
expiresAt: string | null;
/**
* RFC3339Nano formatted date and time for when the order was created (timestamp)
*/
createdAt: string;
/**
* RFC3339Nano time the order was altered
*/
updatedAt: string | null;
}
export interface Orders_party {
__typename: "Party";
/**
* Party identifier
*/
id: string;
/**
* Orders relating to a party
*/
orders: Orders_party_orders[] | null;
}
export interface Orders {
/**
* An entity that is trading on the VEGA network
*/
party: Orders_party | null;
}
export interface OrdersVariables {
partyId: string;
}

View File

@ -0,0 +1 @@
export * from './order-data-provider';

View File

@ -4,8 +4,8 @@ import {
Side,
OrderTimeInForce,
} from '@vegaprotocol/types';
import { sortOrders } from './orders-data-provider';
import type { Orders_party_orders } from './__generated__/Orders';
import type { Orders_party_orders } from '../__generated__/Orders';
import { sortOrders } from './order-data-provider';
const marketOrder: Orders_party_orders = {
__typename: 'Order',

View File

@ -1,9 +1,9 @@
import produce from 'immer';
import { gql } from '@apollo/client';
import { makeDataProvider } from '@vegaprotocol/react-helpers';
import type { OrderFields } from './__generated__/OrderFields';
import type { Orders, Orders_party_orders } from './__generated__/Orders';
import type { OrderSub } from './__generated__/OrderSub';
import type { OrderFields } from '../__generated__/OrderFields';
import type { Orders, Orders_party_orders } from '../__generated__/Orders';
import type { OrderSub } from '../__generated__/OrderSub';
import orderBy from 'lodash/orderBy';
import uniqBy from 'lodash/uniqBy';

View File

@ -0,0 +1 @@
export * from './order-list-manager';

View File

@ -1,8 +1,8 @@
import { render, screen } from '@testing-library/react';
import { OrderListManager } from './order-list-manager';
import * as useDataProviderHook from '@vegaprotocol/react-helpers';
import type { Orders_party_orders } from './__generated__/Orders';
import * as orderListMock from './order-list';
import type { Orders_party_orders } from '../__generated__/Orders';
import * as orderListMock from '../order-list';
import { forwardRef } from 'react';
jest.mock('./order-list');

View File

@ -1,15 +1,15 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { OrderList } from './order-list';
import type { OrderFields } from './__generated__/OrderFields';
import { OrderList } from '../order-list';
import type { OrderFields } from '../__generated__/OrderFields';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import {
ordersDataProvider,
prepareIncomingOrders,
sortOrders,
} from './orders-data-provider';
} from '../order-data-provider';
import { useCallback, useMemo, useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import type { OrderSub_orders } from './__generated__/OrderSub';
import type { OrderSub_orders } from '../__generated__/OrderSub';
import isEqual from 'lodash/isEqual';
interface OrderListManagerProps {
@ -25,10 +25,9 @@ export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
if (!gridRef.current) {
return false;
}
const incoming = prepareIncomingOrders(delta);
const update: OrderFields[] = [];
const updateRows: OrderFields[] = [];
const add: OrderFields[] = [];
incoming.forEach((d) => {
@ -39,17 +38,17 @@ export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
const rowNode = gridRef.current.api.getRowNode(d.id);
if (rowNode) {
if (!isEqual) {
update.push(d);
if (!isEqual(d, rowNode.data)) {
updateRows.push(d);
}
} else {
add.push(d);
}
});
if (update.length || add.length) {
if (updateRows.length || add.length) {
gridRef.current.api.applyTransactionAsync({
update,
update: updateRows,
add,
addIndex: 0,
});

View File

@ -0,0 +1,2 @@
export * from './order-list.stories';
export * from './order-list';

View File

@ -0,0 +1,116 @@
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 type { PartialDeep } from 'type-fest';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { MockedProvider } from '@apollo/client/testing';
import { limitOrder, marketOrder } from '../mocks/generate-orders';
const generateJsx = (
orders: Orders_party_orders[] | null,
context: PartialDeep<VegaWalletContextShape> = { keypair: { pub: '0x123' } }
) => {
return (
<MockedProvider>
<VegaWalletContext.Provider value={context as VegaWalletContextShape}>
<OrderList data={orders} />
</VegaWalletContext.Provider>
</MockedProvider>
);
};
describe('OrderList', () => {
it('should show no orders message', async () => {
await act(async () => {
render(generateJsx([]));
});
expect(screen.getByText('No orders')).toBeInTheDocument();
});
it('should render correct columns', async () => {
await act(async () => {
render(generateJsx([marketOrder, limitOrder]));
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(10);
expect(headers.map((h) => h.textContent?.trim())).toEqual([
'Market',
'Amount',
'Type',
'Status',
'Filled',
'Price',
'Time In Force',
'Created At',
'Updated At',
'Cancel',
]);
});
it('should apply correct formatting for market order', async () => {
await act(async () => {
render(generateJsx([marketOrder]));
});
const cells = screen.getAllByRole('gridcell');
const expectedValues: string[] = [
marketOrder.market?.tradableInstrument.instrument.code || '',
'+0.10',
marketOrder.type || '',
marketOrder.status,
'5',
'-',
marketOrder.timeInForce,
getDateTimeFormat().format(new Date(marketOrder.createdAt)),
'-',
'Cancel',
];
cells.forEach((cell, i) =>
expect(cell).toHaveTextContent(expectedValues[i])
);
});
it('should apply correct formatting applied for GTT limit order', async () => {
await act(async () => {
render(generateJsx([limitOrder]));
});
const cells = screen.getAllByRole('gridcell');
const expectedValues: string[] = [
limitOrder.market?.tradableInstrument.instrument.code || '',
'+0.10',
limitOrder.type || '',
limitOrder.status,
'5',
addDecimal(limitOrder.price, limitOrder.market?.decimalPlaces ?? 0),
`${limitOrder.timeInForce}: ${getDateTimeFormat().format(
new Date(limitOrder.expiresAt ?? '')
)}`,
getDateTimeFormat().format(new Date(limitOrder.createdAt)),
'-',
'Cancel',
];
cells.forEach((cell, i) =>
expect(cell).toHaveTextContent(expectedValues[i])
);
});
it('should apply correct formatting for a rejected order', async () => {
const rejectedOrder = {
...marketOrder,
status: OrderStatus.Rejected,
rejectionReason: OrderRejectionReason.InsufficientAssetBalance,
};
await act(async () => {
render(generateJsx([rejectedOrder]));
});
const cells = screen.getAllByRole('gridcell');
expect(cells[3]).toHaveTextContent(
`${rejectedOrder.status}: ${rejectedOrder.rejectionReason}`
);
});
});

View File

@ -0,0 +1,69 @@
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 { generateOrdersArray } from '../mocks';
export default {
component: OrderList,
title: 'OrderList',
} as Meta;
const Template: Story = (args) => {
const cancel = () => Promise.resolve();
return (
<div style={{ height: 1000 }}>
<OrderListTable data={args.data} cancel={cancel} />
</div>
);
};
const Template2: Story = (args) => {
const [open, setOpen] = useState(false);
const cancel = () => {
setOpen(!open);
return Promise.resolve();
};
const transaction: VegaTxState = {
status: VegaTxStatus.Default,
error: null,
txHash: null,
signature: null,
};
const finalizedOrder: Order = {
status: OrderStatus.Cancelled,
rejectionReason: null,
size: '10',
price: '1000',
market: null,
type: OrderType.Limit,
};
const reset = () => null;
return (
<>
<div style={{ height: 1000 }}>
<OrderListTable data={args.data} cancel={cancel} />
</div>
<CancelDialog
orderDialogOpen={open}
setOrderDialogOpen={setOpen}
finalizedOrder={finalizedOrder}
transaction={transaction}
reset={reset}
/>
</>
);
};
export const Default = Template.bind({});
Default.args = {
data: generateOrdersArray(),
};
export const Modal = Template2.bind({});
Modal.args = {
data: generateOrdersArray(),
};

View File

@ -1,16 +1,16 @@
import { OrderTimeInForce, OrderStatus, Side } from '@vegaprotocol/types';
import type { Orders_party_orders } from './__generated__/Orders';
import {
addDecimal,
formatNumber,
getDateTimeFormat,
t,
} from '@vegaprotocol/react-helpers';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import type { ValueFormatterParams } from 'ag-grid-community';
import type { Orders_party_orders } from '../__generated__/Orders';
import { addDecimal, getDateTimeFormat, t } from '@vegaprotocol/react-helpers';
import { AgGridDynamic as AgGrid, Button } from '@vegaprotocol/ui-toolkit';
import type {
ICellRendererParams,
ValueFormatterParams,
} from 'ag-grid-community';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import { forwardRef } from 'react';
import { forwardRef, useState } from 'react';
import { useOrderCancel } from '@vegaprotocol/wallet';
import { CancelDialog } from '../cancel-order-dialog/cancel-dialog';
import BigNumber from 'bignumber.js';
interface OrderListProps {
@ -19,6 +19,30 @@ interface OrderListProps {
export const OrderList = forwardRef<AgGridReact, OrderListProps>(
({ data }, ref) => {
const [orderDialogOpen, setOrderDialogOpen] = useState(false);
const { transaction, finalizedOrder, reset, cancel } = useOrderCancel();
return (
<>
<OrderListTable data={data} cancel={cancel} ref={ref} />
<CancelDialog
orderDialogOpen={orderDialogOpen}
setOrderDialogOpen={setOrderDialogOpen}
finalizedOrder={finalizedOrder}
transaction={transaction}
reset={reset}
/>
</>
);
}
);
interface OrderListTableProps {
data: Orders_party_orders[] | null;
cancel: (body?: unknown) => Promise<unknown>;
}
export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
({ data, cancel }, ref) => {
return (
<AgGrid
ref={ref}
@ -27,6 +51,7 @@ export const OrderList = forwardRef<AgGridReact, OrderListProps>(
defaultColDef={{ flex: 1, resizable: true }}
style={{ width: '100%', height: '100%' }}
getRowId={({ data }) => data.id}
rowHeight={40}
>
<AgGridColumn
headerName={t('Market')}
@ -76,7 +101,7 @@ export const OrderList = forwardRef<AgGridReact, OrderListProps>(
if (data.type === 'Market') {
return '-';
}
return formatNumber(value, data.market.decimalPlaces);
return addDecimal(value, data.market.decimalPlaces);
}}
/>
<AgGridColumn
@ -104,6 +129,32 @@ export const OrderList = forwardRef<AgGridReact, OrderListProps>(
return value ? getDateTimeFormat().format(new Date(value)) : '-';
}}
/>
<AgGridColumn
field="cancel"
cellRenderer={({ data }: ICellRendererParams) => {
if (
![
OrderStatus.Cancelled,
OrderStatus.Rejected,
OrderStatus.Expired,
OrderStatus.Filled,
OrderStatus.Stopped,
].includes(data.status)
) {
return (
<Button
data-testid="cancel"
onClick={async () => {
await cancel(data);
}}
>
Cancel
</Button>
);
}
return null;
}}
/>
</AgGrid>
);
}

View File

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

View File

@ -1,156 +0,0 @@
import { act, render, screen } from '@testing-library/react';
import { formatNumber, getDateTimeFormat } from '@vegaprotocol/react-helpers';
import type { Orders_party_orders } from './__generated__/Orders';
import {
OrderStatus,
OrderTimeInForce,
OrderType,
Side,
OrderRejectionReason,
} from '@vegaprotocol/types';
import { OrderList } from './order-list';
it('No orders message shown', async () => {
await act(async () => {
render(<OrderList data={[]} />);
});
expect(screen.getByText('No orders')).toBeInTheDocument();
});
const marketOrder: Orders_party_orders = {
__typename: 'Order',
id: 'order-id',
market: {
__typename: 'Market',
id: 'market-id',
name: 'market-name',
decimalPlaces: 2,
positionDecimalPlaces: 0,
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('2022-2-3').toISOString(),
updatedAt: null,
expiresAt: null,
rejectionReason: null,
};
const limitOrder: Orders_party_orders = {
__typename: 'Order',
id: 'order-id',
market: {
__typename: 'Market',
id: 'market-id',
name: 'market-name',
decimalPlaces: 2,
positionDecimalPlaces: 2,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
code: 'instrument-code',
},
},
},
size: '1000',
type: OrderType.Limit,
status: OrderStatus.Active,
side: Side.Sell,
remaining: '500',
price: '12345',
timeInForce: OrderTimeInForce.GTT,
createdAt: new Date('2022-3-3').toISOString(),
expiresAt: new Date('2022-3-5').toISOString(),
updatedAt: null,
rejectionReason: null,
};
it('Correct columns are rendered', async () => {
await act(async () => {
render(<OrderList data={[marketOrder]} />);
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(9);
expect(headers.map((h) => h.textContent?.trim())).toEqual([
'Market',
'Amount',
'Type',
'Status',
'Filled',
'Price',
'Time In Force',
'Created At',
'Updated At',
]);
});
it('Correct formatting applied for market order', async () => {
await act(async () => {
render(<OrderList data={[marketOrder]} />);
});
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
marketOrder.market?.tradableInstrument.instrument.code,
'+10',
marketOrder.type,
marketOrder.status,
'5',
'-',
marketOrder.timeInForce,
getDateTimeFormat().format(new Date(marketOrder.createdAt)),
'-',
];
cells.forEach((cell, i) => expect(cell).toHaveTextContent(expectedValues[i]));
});
it('Correct formatting applied for GTT limit order', async () => {
await act(async () => {
render(<OrderList data={[limitOrder]} />);
});
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
limitOrder.market?.tradableInstrument.instrument.code,
'-10.00',
limitOrder.type,
limitOrder.status,
'5.00',
formatNumber(limitOrder.price, limitOrder.market?.decimalPlaces ?? 0),
`${limitOrder.timeInForce}: ${getDateTimeFormat().format(
new Date(limitOrder.expiresAt ?? '')
)}`,
getDateTimeFormat().format(new Date(limitOrder.createdAt)),
'-',
];
cells.forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);
});
});
it('Correct formatting applied for a rejected order', async () => {
const rejectedOrder = {
...marketOrder,
status: OrderStatus.Rejected,
rejectionReason: OrderRejectionReason.InsufficientAssetBalance,
};
await act(async () => {
render(<OrderList data={[rejectedOrder]} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[3]).toHaveTextContent(
`${rejectedOrder.status}: ${rejectedOrder.rejectionReason}`
);
});

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,17 @@
const { join } = require('path');
const { createGlobPatternsForDependencies } = require('@nrwl/next/tailwind');
const theme = require('../tailwindcss-config/src/theme');
const vegaCustomClasses = require('../tailwindcss-config/src/vega-custom-classes');
module.exports = {
content: [
join(__dirname, 'src/**/*.{ts,tsx,html,mdx}'),
join(__dirname, '.storybook/preview.js'),
...createGlobPatternsForDependencies(__dirname),
],
darkMode: 'class',
theme: {
extend: theme,
},
plugins: [vegaCustomClasses],
};

View File

@ -20,6 +20,9 @@
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./.storybook/tsconfig.json"
}
]
}

View File

@ -16,7 +16,11 @@
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx"
"**/*.test.jsx",
"**/*.stories.ts",
"**/*.stories.js",
"**/*.stories.jsx",
"**/*.stories.tsx"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

View File

@ -43,7 +43,7 @@ interface TradesTableProps {
export const TradesTable = forwardRef<AgGridReact, TradesTableProps>(
({ data }, ref) => {
// Sort intial trades
// Sort initial trades
const trades = useMemo(() => {
if (!data) {
return null;

View File

@ -1,6 +1,6 @@
{
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"ignorePatterns": ["!**/*", "__generated__"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],

View File

@ -5,11 +5,11 @@ import {
screen,
waitFor,
} from '@testing-library/react';
import type { VegaWalletContextShape } from './context';
import { VegaWalletContext } from './context';
import type { VegaWalletContextShape } from '../context';
import { VegaWalletContext } from '../context';
import { VegaConnectDialog } from './connect-dialog';
import type { VegaConnectDialogProps } from '.';
import { RestConnector } from './connectors';
import type { VegaConnectDialogProps } from '..';
import { RestConnector } from '../connectors';
let defaultProps: VegaConnectDialogProps;
let defaultContextValue: VegaWalletContextShape;

View File

@ -1,11 +1,10 @@
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { Dialog } from '@vegaprotocol/ui-toolkit';
import type { VegaConnector } from './connectors';
import { RestConnectorForm } from './rest-connector-form';
import { useEffect } from 'react';
import { RestConnector } from './connectors';
import { useVegaWallet } from './hooks';
import { t } from '@vegaprotocol/react-helpers';
import type { VegaConnector } from '../connectors';
import { RestConnector } from '../connectors';
import { RestConnectorForm } from '../rest-connector-form';
import { useVegaWallet } from '../use-vega-wallet';
export interface VegaConnectDialogProps {
connectors: { [name: string]: VegaConnector };

View File

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

View File

@ -1,30 +1,3 @@
import type {
VegaKey,
TransactionResponse,
} from '@vegaprotocol/vegawallet-service-api-client';
import type { TransactionSubmission } from '../types';
export { RestConnector } from './rest-connector';
type ErrorResponse =
| {
error: string;
}
| {
errors: object;
};
export interface VegaConnector {
/** Description of how to use this connector */
description: string;
/** Connect to wallet and return keys */
connect(): Promise<VegaKey[] | null>;
/** Disconnect from wallet */
disconnect(): Promise<void>;
/** Send a TX to the network. Only support order submission for now */
sendTx: (
body: TransactionSubmission
) => Promise<TransactionResponse | ErrorResponse>;
}
export * from './vega-connector';
export * from './rest-connector';
export * from './injected-connector';

View File

@ -1,4 +1,4 @@
import type { VegaConnector } from '.';
import type { VegaConnector } from './vega-connector';
/**
* Dummy injected connector that we may use when browser wallet is implemented

View File

@ -5,7 +5,7 @@ import {
} from '@vegaprotocol/vegawallet-service-api-client';
import { LocalStorage } from '@vegaprotocol/react-helpers';
import { WALLET_CONFIG } from '../storage-keys';
import type { VegaConnector } from '.';
import type { VegaConnector } from './vega-connector';
import type { TransactionSubmission } from '../types';
// Perhaps there should be a default ConnectorConfig that others can extend off. Do all connectors
@ -95,7 +95,7 @@ export class RestConnector implements VegaConnector {
}
private handleSendTxError(err: unknown) {
const unpexpectedError = { error: 'Something went wrong' };
const unexpectedError = { error: 'Something went wrong' };
if (isServiceError(err)) {
if (err.code === 401) {
@ -105,10 +105,10 @@ export class RestConnector implements VegaConnector {
try {
return JSON.parse(err.body ?? '');
} catch {
return unpexpectedError;
return unexpectedError;
}
} else {
return unpexpectedError;
return unexpectedError;
}
}

View File

@ -0,0 +1,29 @@
import type {
VegaKey,
TransactionResponse,
} from '@vegaprotocol/vegawallet-service-api-client';
import type { TransactionSubmission } from '../types';
type ErrorResponse =
| {
error: string;
}
| {
errors: object;
};
export interface VegaConnector {
/** Description of how to use this connector */
description: string;
/** Connect to wallet and return keys */
connect(): Promise<VegaKey[] | null>;
/** Disconnect from wallet */
disconnect(): Promise<void>;
/** Send a TX to the network. Only support order submission for now */
sendTx: (
body: TransactionSubmission
) => Promise<TransactionResponse | ErrorResponse | null>;
}

View File

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

View File

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

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

View File

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

View File

@ -0,0 +1,24 @@
import { gql } from '@apollo/client';
export const ORDER_EVENT_SUB = gql`
subscription OrderEvent($partyId: ID!) {
busEvents(partyId: $partyId, batchSize: 0, types: [Order]) {
type
event {
... on Order {
type
id
status
rejectionReason
createdAt
size
price
market {
name
decimalPlaces
}
}
}
}
}
`;

View File

@ -0,0 +1,123 @@
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 { useOrderCancel } from './use-order-cancel';
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: [],
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(),
disconnect: jest.fn(),
selectPublicKey: jest.fn(),
connector: null,
};
function setup(context?: Partial<VegaWalletContextShape>) {
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider>
<VegaWalletContext.Provider
value={{ ...defaultWalletContext, ...context }}
>
{children}
</VegaWalletContext.Provider>
</MockedProvider>
);
return renderHook(() => useOrderCancel(), { wrapper });
}
describe('useOrderCancel', () => {
it('has the correct default state', () => {
const { result } = setup();
expect(typeof result.current.cancel).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 order: 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);
});
expect(mockSendTx).not.toHaveBeenCalled();
});
it('should cancel a correctly formatted order', async () => {
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
const keypair = {
pub: '0x123',
} as VegaKeyExtended;
const order: 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);
});
expect(mockSendTx).toHaveBeenCalledWith({
pubKey: keypair.pub,
propagate: true,
orderCancellation: {
marketId: 'market-id',
},
});
});
});

View File

@ -0,0 +1,83 @@
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

@ -1,7 +1,7 @@
import { act, fireEvent, render, screen } from '@testing-library/react';
import type { VegaKey } from '@vegaprotocol/vegawallet-service-api-client';
import { RestConnector } from './connectors';
import { useVegaWallet } from './hooks';
import { useVegaWallet } from './use-vega-wallet';
import { VegaWalletProvider } from './provider';
import { WALLET_KEY } from './storage-keys';

View File

@ -2,7 +2,7 @@ import { LocalStorage, t } from '@vegaprotocol/react-helpers';
import type { ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { VegaKeyExtended, VegaWalletContextShape } from '.';
import type { VegaConnector } from './connectors';
import type { VegaConnector } from './connectors/vega-connector';
import { VegaWalletContext } from './context';
import { WALLET_KEY } from './storage-keys';
import type { TransactionSubmission } from './types';
@ -104,7 +104,7 @@ export const VegaWalletProvider = ({ children }: VegaWalletProviderProps) => {
disconnect,
connector: connector.current,
sendTx,
};
} as VegaWalletContextShape;
}, [keypair, keypairs, setPublicKey, connect, disconnect, connector, sendTx]);
return (

View File

@ -1,5 +1,6 @@
import type {
DelegateSubmissionBody,
OrderCancellationBody,
OrderSubmissionBody,
UndelegateSubmissionBody,
VoteSubmissionBody,
@ -28,6 +29,7 @@ export enum OrderTimeInForce {
// Will make Transaction a union type as other transactions are added
export type TransactionSubmission =
| OrderSubmissionBody
| OrderCancellationBody
| WithdrawSubmissionBody
| VoteSubmissionBody
| DelegateSubmissionBody

View File

@ -1,7 +1,7 @@
import { useVegaWallet, WALLET_CONFIG } from './';
import { useEffect, useState } from 'react';
import { LocalStorage } from '@vegaprotocol/react-helpers';
import type { VegaConnector } from './connectors';
import type { VegaConnector } from './connectors/vega-connector';
export function useEagerConnect(Connectors: {
[connector: string]: VegaConnector;

View File

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

View File

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

View File

@ -0,0 +1,130 @@
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';
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'
);
});
});

View File

@ -5,18 +5,34 @@ import {
addDecimalsFormatNumber,
t,
} from '@vegaprotocol/react-helpers';
import type { VegaTxState } from '@vegaprotocol/wallet';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import type { OrderEvent_busEvents_event_Order } from '../hooks/__generated__/OrderEvent';
import type { VegaTxState } from '../use-vega-transaction';
import { VegaTxStatus } from '../use-vega-transaction';
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 OrderDialogProps {
transaction: VegaTxState;
finalizedOrder: OrderEvent_busEvents_event_Order | null;
finalizedOrder: Order | null;
title?: string;
}
export const OrderDialog = ({
export const VegaOrderTransactionDialog = ({
transaction,
finalizedOrder,
title = 'Order placed',
}: OrderDialogProps) => {
// Rejected by wallet
if (transaction.status === VegaTxStatus.Requested) {
@ -80,10 +96,7 @@ export const OrderDialog = ({
}
return (
<OrderDialogWrapper
title="Order placed"
icon={<Icon name="tick" size={20} />}
>
<OrderDialogWrapper title={title} icon={<Icon name="tick" size={20} />}>
<p>{t(`Status: ${finalizedOrder.status}`)}</p>
{finalizedOrder.market && (
<p>{t(`Market: ${finalizedOrder.market.name}`)}</p>