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:
parent
cc51007e6a
commit
0473412487
@ -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
|
||||
|
||||
|
@ -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', () => {
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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';
|
||||
|
@ -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',
|
||||
|
@ -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) {
|
||||
|
@ -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 />
|
||||
|
28
libs/order-list/.storybook/main.js
Normal file
28
libs/order-list/.storybook/main.js
Normal 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;
|
||||
},
|
||||
};
|
1
libs/order-list/.storybook/preview-head.html
Normal file
1
libs/order-list/.storybook/preview-head.html
Normal file
@ -0,0 +1 @@
|
||||
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
|
11
libs/order-list/.storybook/preview.js
Normal file
11
libs/order-list/.storybook/preview.js
Normal 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' },
|
||||
],
|
||||
},
|
||||
};
|
19
libs/order-list/.storybook/tsconfig.json
Normal file
19
libs/order-list/.storybook/tsconfig.json
Normal 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"]
|
||||
}
|
10
libs/order-list/postcss.config.js
Normal file
10
libs/order-list/postcss.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
const { join } = require('path');
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
config: join(__dirname, 'tailwind.config.js'),
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1 @@
|
||||
export * from './lib/order-list';
|
||||
export * from './lib/order-list-container';
|
||||
export * from './lib/__generated__/Orders';
|
||||
export * from './lib';
|
||||
|
3
libs/order-list/src/lib/components/__generated__/index.ts
generated
Normal file
3
libs/order-list/src/lib/components/__generated__/index.ts
generated
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './OrderFields';
|
||||
export * from './OrderSub';
|
||||
export * from './Orders';
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
@ -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>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './cancel-dialog';
|
7
libs/order-list/src/lib/components/index.ts
Normal file
7
libs/order-list/src/lib/components/index.ts
Normal 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';
|
156
libs/order-list/src/lib/components/mocks/generate-orders.ts
Normal file
156
libs/order-list/src/lib/components/mocks/generate-orders.ts
Normal 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()];
|
||||
};
|
1
libs/order-list/src/lib/components/mocks/index.ts
Normal file
1
libs/order-list/src/lib/components/mocks/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './generate-orders';
|
121
libs/order-list/src/lib/components/order-data-provider/__generated__/OrderFields.ts
generated
Normal file
121
libs/order-list/src/lib/components/order-data-provider/__generated__/OrderFields.ts
generated
Normal 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;
|
||||
}
|
132
libs/order-list/src/lib/components/order-data-provider/__generated__/OrderSub.ts
generated
Normal file
132
libs/order-list/src/lib/components/order-data-provider/__generated__/OrderSub.ts
generated
Normal 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;
|
||||
}
|
144
libs/order-list/src/lib/components/order-data-provider/__generated__/Orders.ts
generated
Normal file
144
libs/order-list/src/lib/components/order-data-provider/__generated__/Orders.ts
generated
Normal 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;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './order-data-provider';
|
@ -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',
|
@ -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';
|
||||
|
@ -0,0 +1 @@
|
||||
export * from './order-list-manager';
|
@ -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');
|
@ -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,
|
||||
});
|
2
libs/order-list/src/lib/components/order-list/index.ts
Normal file
2
libs/order-list/src/lib/components/order-list/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './order-list.stories';
|
||||
export * from './order-list';
|
@ -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}`
|
||||
);
|
||||
});
|
||||
});
|
@ -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(),
|
||||
};
|
@ -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>
|
||||
);
|
||||
}
|
1
libs/order-list/src/lib/index.ts
Normal file
1
libs/order-list/src/lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './components';
|
@ -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}`
|
||||
);
|
||||
});
|
3
libs/order-list/src/styles.scss
Normal file
3
libs/order-list/src/styles.scss
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
17
libs/order-list/tailwind.config.js
Normal file
17
libs/order-list/tailwind.config.js
Normal 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],
|
||||
};
|
@ -20,6 +20,9 @@
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
},
|
||||
{
|
||||
"path": "./.storybook/tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"ignorePatterns": ["!**/*", "__generated__"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
|
@ -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;
|
@ -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 };
|
1
libs/wallet/src/connect-dialog/index.ts
Normal file
1
libs/wallet/src/connect-dialog/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './connect-dialog';
|
@ -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';
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
29
libs/wallet/src/connectors/vega-connector.ts
Normal file
29
libs/wallet/src/connectors/vega-connector.ts
Normal 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>;
|
||||
}
|
@ -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';
|
||||
|
100
libs/wallet/src/order-hooks/__generated__/OrderEvent.ts
generated
Normal file
100
libs/wallet/src/order-hooks/__generated__/OrderEvent.ts
generated
Normal 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;
|
||||
}
|
1
libs/wallet/src/order-hooks/__generated__/index.ts
generated
Normal file
1
libs/wallet/src/order-hooks/__generated__/index.ts
generated
Normal file
@ -0,0 +1 @@
|
||||
export * from './OrderEvent';
|
3
libs/wallet/src/order-hooks/index.ts
Normal file
3
libs/wallet/src/order-hooks/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './__generated__';
|
||||
export * from './order-event-query';
|
||||
export * from './use-order-cancel';
|
24
libs/wallet/src/order-hooks/order-event-query.ts
Normal file
24
libs/wallet/src/order-hooks/order-event-query.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
123
libs/wallet/src/order-hooks/use-order-cancel.spec.tsx
Normal file
123
libs/wallet/src/order-hooks/use-order-cancel.spec.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
83
libs/wallet/src/order-hooks/use-order-cancel.tsx
Normal file
83
libs/wallet/src/order-hooks/use-order-cancel.tsx
Normal 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,
|
||||
};
|
||||
};
|
@ -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';
|
||||
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
1
libs/wallet/src/vega-order-transaction-dialog/index.ts
Normal file
1
libs/wallet/src/vega-order-transaction-dialog/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './vega-order-transaction-dialog';
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
@ -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>
|
Loading…
Reference in New Issue
Block a user