diff --git a/apps/trading/__generated__/globalTypes.ts b/apps/trading/__generated__/globalTypes.ts
deleted file mode 100644
index 8d9b7dd7a..000000000
--- a/apps/trading/__generated__/globalTypes.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/* tslint:disable */
-/* eslint-disable */
-// @generated
-// This file was automatically generated and should not be edited.
-
-//==============================================================
-// START Enums and Input Objects
-//==============================================================
-
-//==============================================================
-// END Enums and Input Objects
-//==============================================================
diff --git a/apps/trading/apollo.config.js b/apps/trading/apollo.config.js
index b71577567..887eb5abd 100644
--- a/apps/trading/apollo.config.js
+++ b/apps/trading/apollo.config.js
@@ -4,6 +4,6 @@ module.exports = {
name: 'vega',
url: process.env.NX_VEGA_URL,
},
- includes: ['{components,lib,pages}/**/*.{ts,tsx,js,jsx,graphql}'],
+ includes: ['{components,lib,pages,hooks}/**/*.{ts,tsx,js,jsx,graphql}'],
},
};
diff --git a/apps/trading/components/deal-ticket-container/deal-ticket-container.tsx b/apps/trading/components/deal-ticket-container/deal-ticket-container.tsx
new file mode 100644
index 000000000..2f83b19a7
--- /dev/null
+++ b/apps/trading/components/deal-ticket-container/deal-ticket-container.tsx
@@ -0,0 +1,74 @@
+import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
+import { DealTicket } from '@vegaprotocol/deal-ticket';
+import { OrderStatus } from '@vegaprotocol/graphql';
+import { useOrderSubmit } from '../../hooks/use-order-submit';
+import { useEffect, useState } from 'react';
+import { VegaTxStatus } from '../../hooks/use-vega-transaction';
+import { OrderDialog } from './order-dialog';
+
+export const DealTicketContainer = ({ market }) => {
+ const [orderDialogOpen, setOrderDialogOpen] = useState(false);
+ const { submit, transaction, finalizedOrder, reset } = useOrderSubmit(market);
+
+ const getDialogIntent = (status: VegaTxStatus) => {
+ if (finalizedOrder) {
+ if (
+ finalizedOrder.status === OrderStatus.Active ||
+ finalizedOrder.status === OrderStatus.Filled ||
+ finalizedOrder.status === OrderStatus.PartiallyFilled
+ ) {
+ return Intent.Success;
+ }
+
+ if (finalizedOrder.status === OrderStatus.Parked) {
+ return Intent.Warning;
+ }
+
+ return Intent.Danger;
+ }
+
+ if (status === VegaTxStatus.Rejected) {
+ return Intent.Danger;
+ }
+
+ return Intent.Progress;
+ };
+
+ useEffect(() => {
+ if (transaction.status !== VegaTxStatus.Default) {
+ setOrderDialogOpen(true);
+ }
+ }, [transaction.status]);
+
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/apps/trading/components/deal-ticket-container/index.ts b/apps/trading/components/deal-ticket-container/index.ts
new file mode 100644
index 000000000..6d3e9ada5
--- /dev/null
+++ b/apps/trading/components/deal-ticket-container/index.ts
@@ -0,0 +1 @@
+export * from './deal-ticket-container';
diff --git a/apps/trading/components/deal-ticket-container/order-dialog.tsx b/apps/trading/components/deal-ticket-container/order-dialog.tsx
new file mode 100644
index 000000000..09938a2b4
--- /dev/null
+++ b/apps/trading/components/deal-ticket-container/order-dialog.tsx
@@ -0,0 +1,95 @@
+import { Icon, Loader } from '@vegaprotocol/ui-toolkit';
+import { ReactNode } from 'react';
+import {
+ TransactionState,
+ VegaTxStatus,
+} from '../../hooks/use-vega-transaction';
+import { OrderEvent_busEvents_event_Order } from '@vegaprotocol/graphql';
+
+interface OrderDialogProps {
+ transaction: TransactionState;
+ finalizedOrder: OrderEvent_busEvents_event_Order | null;
+}
+
+export const OrderDialog = ({
+ transaction,
+ finalizedOrder,
+}: OrderDialogProps) => {
+ // TODO: When wallets support confirming transactions return UI for 'awaiting confirmation' step
+
+ // Rejected by wallet
+ if (transaction.status === VegaTxStatus.Rejected) {
+ return (
+ }
+ >
+ {transaction.error && (
+
+ {JSON.stringify(transaction.error, null, 2)}
+
+ )}
+
+ );
+ }
+
+ // Pending consensus
+ if (!finalizedOrder) {
+ return (
+ }
+ >
+ {transaction.hash && (
+ Tx hash: {transaction.hash}
+ )}
+
+ );
+ }
+
+ // Order on network but was rejected
+ if (finalizedOrder.status === 'Rejected') {
+ return (
+ }
+ >
+ Reason: {finalizedOrder.rejectionReason}
+
+ );
+ }
+
+ return (
+ }
+ >
+ Status: {finalizedOrder.status}
+ Market: {finalizedOrder.market.name}
+ Amount: {finalizedOrder.size}
+ {finalizedOrder.type === 'Limit' && Price: {finalizedOrder.price}
}
+
+ );
+};
+
+interface OrderDialogWrapperProps {
+ children: ReactNode;
+ icon: ReactNode;
+ title: string;
+}
+
+const OrderDialogWrapper = ({
+ children,
+ icon,
+ title,
+}: OrderDialogWrapperProps) => {
+ return (
+
+
{icon}
+
+
{title}
+ {children}
+
+
+ );
+};
diff --git a/apps/trading/components/page-query-container/index.tsx b/apps/trading/components/page-query-container/index.tsx
index f1e7bf9ae..455302743 100644
--- a/apps/trading/components/page-query-container/index.tsx
+++ b/apps/trading/components/page-query-container/index.tsx
@@ -1,7 +1,7 @@
import { OperationVariables, QueryHookOptions, useQuery } from '@apollo/client';
-import classNames from 'classnames';
import { DocumentNode } from 'graphql';
import { ReactNode } from 'react';
+import { Splash } from '@vegaprotocol/ui-toolkit';
interface PageQueryContainerProps {
query: DocumentNode;
@@ -15,19 +15,13 @@ export const PageQueryContainer = ({
children,
}: PageQueryContainerProps) => {
const { data, loading, error } = useQuery(query, options);
- const splashClasses = classNames(
- 'w-full h-full',
- 'flex items-center justify-center'
- );
if (loading || !data) {
- return Loading...
;
+ return Loading...;
}
if (error) {
- return (
- Something went wrong: {error.message}
- );
+ return Something went wrong: {error.message};
}
return <>{children(data)}>;
diff --git a/apps/trading/hooks/use-order-submit.spec.tsx b/apps/trading/hooks/use-order-submit.spec.tsx
new file mode 100644
index 000000000..2bc3e5979
--- /dev/null
+++ b/apps/trading/hooks/use-order-submit.spec.tsx
@@ -0,0 +1,153 @@
+import { MockedProvider } from '@apollo/client/testing';
+import { act, renderHook } from '@testing-library/react-hooks';
+import { Order } from '@vegaprotocol/deal-ticket';
+import {
+ VegaKeyExtended,
+ VegaWalletContext,
+ VegaWalletContextShape,
+} from '@vegaprotocol/wallet';
+import { OrderSide, OrderTimeInForce, OrderType } from '@vegaprotocol/wallet';
+import { ReactNode } from 'react';
+import { useOrderSubmit } from './use-order-submit';
+import { VegaTxStatus } from './use-vega-transaction';
+
+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,
+ market = { id: 'market-id', decimalPlaces: 2 }
+) {
+ const wrapper = ({ children }: { children: ReactNode }) => (
+
+
+ {children}
+
+
+ );
+ return renderHook(() => useOrderSubmit(market), { wrapper });
+}
+
+test('Has the correct default state', () => {
+ const { result } = setup();
+ expect(typeof result.current.submit).toEqual('function');
+ expect(typeof result.current.reset).toEqual('function');
+ expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
+ expect(result.current.transaction.hash).toEqual(null);
+ expect(result.current.transaction.error).toEqual(null);
+});
+
+test('Should not sendTx if no keypair', async () => {
+ const mockSendTx = jest.fn();
+ const { result } = setup({ sendTx: mockSendTx, keypairs: [], keypair: null });
+ await act(async () => {
+ result.current.submit({} as Order);
+ });
+ expect(mockSendTx).not.toHaveBeenCalled();
+});
+
+test('Should not sendTx side is not specified', async () => {
+ const mockSendTx = jest.fn();
+ const keypair = {
+ pub: '0x123',
+ } as VegaKeyExtended;
+ const { result } = setup({
+ sendTx: mockSendTx,
+ keypairs: [keypair],
+ keypair,
+ });
+ await act(async () => {
+ result.current.submit({} as Order);
+ });
+ expect(mockSendTx).not.toHaveBeenCalled();
+});
+
+test('Create an Id if a signature is returned', async () => {
+ const signature =
+ '597a7706491e6523c091bab1e4d655b62c45a224e80f6cd92ac366aa5dd9a070cc7dd3c6919cb07b81334b876c662dd43bdbe5e827c8baa17a089feb654fab0b';
+ const expectedId =
+ '2FE09B0E2E6ED35F8883802629C7D609D3CC2FC9CE3CEC0B7824A0D581BD3747';
+ const successObj = {
+ tx: {
+ inputData: 'input-data',
+ signature: {
+ algo: 'algo',
+ version: 1,
+ value: signature,
+ },
+ },
+ txHash: '0x123',
+ };
+ const mockSendTx = jest.fn().mockReturnValue(Promise.resolve(successObj));
+ const keypair = {
+ pub: '0x123',
+ } as VegaKeyExtended;
+ const { result } = setup({
+ sendTx: mockSendTx,
+ keypairs: [keypair],
+ keypair,
+ });
+ await act(async () => {
+ result.current.submit({
+ type: OrderType.Market,
+ side: OrderSide.Buy,
+ size: '1',
+ timeInForce: OrderTimeInForce.FOK,
+ });
+ });
+ expect(result.current.id).toEqual(expectedId);
+});
+
+test('Should submit a correctly formatted order', async () => {
+ const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
+ const keypair = {
+ pub: '0x123',
+ } as VegaKeyExtended;
+ const market = {
+ id: 'market-id',
+ decimalPlaces: 2,
+ };
+ const { result } = setup(
+ {
+ sendTx: mockSendTx,
+ keypairs: [keypair],
+ keypair,
+ },
+ market
+ );
+
+ const order = {
+ type: OrderType.Limit,
+ size: '10',
+ timeInForce: OrderTimeInForce.GTT,
+ side: OrderSide.Buy,
+ price: '1234567.89',
+ expiration: new Date('2022-01-01'),
+ };
+ await act(async () => {
+ result.current.submit(order);
+ });
+
+ expect(mockSendTx).toHaveBeenCalledWith({
+ pubKey: keypair.pub,
+ propagate: true,
+ orderSubmission: {
+ type: OrderType.Limit,
+ marketId: market.id, // Market provided from hook arugment
+ size: '10',
+ side: OrderSide.Buy,
+ timeInForce: OrderTimeInForce.GTT,
+ price: '123456789', // Decimal removed
+ expiresAt: order.expiration.getTime() + '000000', // Nanoseconds appened
+ },
+ });
+});
diff --git a/apps/trading/hooks/use-order-submit.ts b/apps/trading/hooks/use-order-submit.ts
new file mode 100644
index 000000000..c81a61062
--- /dev/null
+++ b/apps/trading/hooks/use-order-submit.ts
@@ -0,0 +1,157 @@
+import { useCallback, useEffect, useState } from 'react';
+import { gql, useSubscription } from '@apollo/client';
+import { ethers } from 'ethers';
+import { SHA3 } from 'sha3';
+import { Order } from '@vegaprotocol/deal-ticket';
+import { OrderType, useVegaWallet } from '@vegaprotocol/wallet';
+import { useVegaTransaction } from './use-vega-transaction';
+import {
+ OrderEvent,
+ OrderEventVariables,
+ OrderEvent_busEvents_event_Order,
+} from '@vegaprotocol/graphql';
+import { removeDecimal } from '@vegaprotocol/react-helpers';
+
+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
+ }
+ }
+ }
+ }
+ }
+`;
+
+interface UseOrderSubmitMarket {
+ id: string;
+ decimalPlaces: number;
+}
+
+export const useOrderSubmit = (market: UseOrderSubmitMarket) => {
+ const { keypair } = useVegaWallet();
+ const { send, transaction, reset: resetTransaction } = useVegaTransaction();
+ const [id, setId] = useState('');
+ const [finalizedOrder, setFinalizedOrder] =
+ useState(null);
+
+ // Start a subscription looking for the newly created order
+ useSubscription(ORDER_EVENT_SUB, {
+ variables: { partyId: keypair?.pub || '' },
+ skip: !id,
+ onSubscriptionData: ({ subscriptionData }) => {
+ if (!subscriptionData.data.busEvents.length) {
+ return;
+ }
+
+ // No types available for the subscription result
+ const matchingOrderEvent = subscriptionData.data.busEvents.find((e) => {
+ if (e.event.__typename !== 'Order') {
+ return false;
+ }
+
+ if (e.event.id === id) {
+ return true;
+ }
+
+ return false;
+ });
+
+ if (
+ matchingOrderEvent &&
+ matchingOrderEvent.event.__typename === 'Order'
+ ) {
+ setFinalizedOrder(matchingOrderEvent.event);
+ }
+ },
+ });
+
+ useEffect(() => {
+ if (finalizedOrder) {
+ resetTransaction();
+ }
+ }, [finalizedOrder, resetTransaction]);
+
+ const submit = useCallback(
+ async (order: Order) => {
+ if (!keypair || !order.side) {
+ return;
+ }
+
+ setFinalizedOrder(null);
+
+ const res = await send({
+ pubKey: keypair.pub,
+ propagate: true,
+ orderSubmission: {
+ marketId: market.id,
+ price:
+ order.type === OrderType.Market
+ ? undefined
+ : removeDecimal(order.price, market.decimalPlaces),
+ size: order.size,
+ type: order.type,
+ side: order.side,
+ timeInForce: order.timeInForce,
+ expiresAt: order.expiration
+ ? // Wallet expects timestamp in nanoseconds, we don't have that level of accuracy so
+ // just append 6 zeroes
+ order.expiration.getTime().toString() + '000000'
+ : undefined,
+ },
+ });
+
+ if (res?.signature) {
+ setId(determineId(res.signature).toUpperCase());
+ }
+ },
+ [market, keypair, send]
+ );
+
+ const reset = useCallback(() => {
+ resetTransaction();
+ setFinalizedOrder(null);
+ setId('');
+ }, [resetTransaction]);
+
+ return {
+ transaction,
+ finalizedOrder,
+ id,
+ submit,
+ reset,
+ };
+};
+
+/**
+ * This function creates an ID in the same way that core does on the backend. This way we
+ * Can match up the newly created order with incoming orders via a subscription
+ */
+export const determineId = (sig: string) => {
+ // Prepend 0x
+ if (sig.slice(0, 2) !== '0x') {
+ sig = '0x' + sig;
+ }
+
+ // Create the ID
+ const hash = new SHA3(256);
+ const bytes = ethers.utils.arrayify(sig);
+ hash.update(Buffer.from(bytes));
+ const id = ethers.utils.hexlify(hash.digest());
+
+ // Remove 0x as core doesn't keep them in the API
+ return id.substring(2);
+};
diff --git a/apps/trading/hooks/use-vega-transaction.spec.tsx b/apps/trading/hooks/use-vega-transaction.spec.tsx
new file mode 100644
index 000000000..5b7be1f4d
--- /dev/null
+++ b/apps/trading/hooks/use-vega-transaction.spec.tsx
@@ -0,0 +1,98 @@
+import { act, renderHook } from '@testing-library/react-hooks';
+import {
+ OrderSubmission,
+ VegaWalletContext,
+ VegaWalletContextShape,
+} from '@vegaprotocol/wallet';
+import { ReactNode } from 'react';
+import { useVegaTransaction, VegaTxStatus } from './use-vega-transaction';
+
+const defaultWalletContext = {
+ keypair: null,
+ keypairs: [],
+ sendTx: jest.fn(),
+ connect: jest.fn(),
+ disconnect: jest.fn(),
+ selectPublicKey: jest.fn(),
+ connector: null,
+};
+
+function setup(context?: Partial) {
+ const wrapper = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+ );
+ return renderHook(() => useVegaTransaction(), { wrapper });
+}
+
+test('Has the correct default state', () => {
+ const { result } = setup();
+ expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
+ expect(result.current.transaction.hash).toEqual(null);
+ expect(result.current.transaction.signature).toEqual(null);
+ expect(result.current.transaction.error).toEqual(null);
+ expect(typeof result.current.reset).toEqual('function');
+ expect(typeof result.current.send).toEqual('function');
+});
+
+test('If provider returns null status should be default', async () => {
+ const mockSendTx = jest.fn().mockReturnValue(Promise.resolve(null));
+ const { result } = setup({ sendTx: mockSendTx });
+ await act(async () => {
+ result.current.send({} as OrderSubmission);
+ });
+ expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
+});
+
+test('Handles a single error', async () => {
+ const errorMessage = 'Oops error!';
+ const mockSendTx = jest
+ .fn()
+ .mockReturnValue(Promise.resolve({ error: errorMessage }));
+ const { result } = setup({ sendTx: mockSendTx });
+ await act(async () => {
+ result.current.send({} as OrderSubmission);
+ });
+ expect(result.current.transaction.status).toEqual(VegaTxStatus.Rejected);
+ expect(result.current.transaction.error).toEqual({ error: errorMessage });
+});
+
+test('Handles multiple errors', async () => {
+ const errorObj = {
+ errors: {
+ something: 'Went wrong!',
+ },
+ };
+ const mockSendTx = jest.fn().mockReturnValue(Promise.resolve(errorObj));
+ const { result } = setup({ sendTx: mockSendTx });
+ await act(async () => {
+ result.current.send({} as OrderSubmission);
+ });
+ expect(result.current.transaction.status).toEqual(VegaTxStatus.Rejected);
+ expect(result.current.transaction.error).toEqual(errorObj);
+});
+
+test('Returns the signature if successful', async () => {
+ const successObj = {
+ tx: {
+ inputData: 'input-data',
+ signature: {
+ algo: 'algo',
+ version: 1,
+ value: 'signature',
+ },
+ },
+ txHash: '0x123',
+ };
+ const mockSendTx = jest.fn().mockReturnValue(Promise.resolve(successObj));
+ const { result } = setup({ sendTx: mockSendTx });
+ await act(async () => {
+ result.current.send({} as OrderSubmission);
+ });
+ expect(result.current.transaction.status).toEqual(VegaTxStatus.Pending);
+ expect(result.current.transaction.hash).toEqual(successObj.txHash);
+ expect(result.current.transaction.signature).toEqual(
+ successObj.tx.signature.value
+ );
+});
diff --git a/apps/trading/hooks/use-vega-transaction.ts b/apps/trading/hooks/use-vega-transaction.ts
new file mode 100644
index 000000000..6765b8ad5
--- /dev/null
+++ b/apps/trading/hooks/use-vega-transaction.ts
@@ -0,0 +1,89 @@
+import { useCallback, useState } from 'react';
+import { useVegaWallet, SendTxError, Transaction } from '@vegaprotocol/wallet';
+
+export enum VegaTxStatus {
+ Default = 'Default',
+ AwaitingConfirmation = 'AwaitingConfirmation',
+ Rejected = 'Rejected',
+ Pending = 'Pending',
+}
+
+export interface TransactionState {
+ status: VegaTxStatus;
+ error: object | null;
+ hash: string | null;
+ signature: string | null;
+}
+
+export const useVegaTransaction = () => {
+ const { sendTx } = useVegaWallet();
+ const [transaction, _setTransaction] = useState({
+ status: VegaTxStatus.Default,
+ error: null,
+ hash: null,
+ signature: null,
+ });
+
+ const setTransaction = useCallback((update: Partial) => {
+ _setTransaction((curr) => ({
+ ...curr,
+ ...update,
+ }));
+ }, []);
+
+ const handleError = useCallback(
+ (error: SendTxError) => {
+ setTransaction({ error, status: VegaTxStatus.Rejected });
+ },
+ [setTransaction]
+ );
+
+ const send = useCallback(
+ async (tx: Transaction) => {
+ setTransaction({
+ error: null,
+ hash: null,
+ signature: null,
+ status: VegaTxStatus.AwaitingConfirmation,
+ });
+
+ const res = await sendTx(tx);
+
+ if (res === null) {
+ setTransaction({ status: VegaTxStatus.Default });
+ return null;
+ }
+
+ if ('error' in res) {
+ handleError(res);
+ return null;
+ } else if ('errors' in res) {
+ handleError(res);
+ return null;
+ } else if (res.tx && res.txHash) {
+ setTransaction({
+ status: VegaTxStatus.Pending,
+ hash: res.txHash,
+ signature: res.tx.signature.value,
+ });
+ return {
+ signature: res.tx.signature?.value,
+ };
+ }
+
+ return null;
+ },
+ [sendTx, handleError, setTransaction]
+ );
+
+ const reset = useCallback(() => {
+ setTransaction({
+ error: null,
+ hash: null,
+ signature: null,
+ status: VegaTxStatus.Default,
+ });
+ }, [setTransaction]);
+
+ return { send, transaction, reset };
+};
diff --git a/apps/trading/pages/index.page.tsx b/apps/trading/pages/index.page.tsx
index 6b5bd7a2b..3377e03af 100644
--- a/apps/trading/pages/index.page.tsx
+++ b/apps/trading/pages/index.page.tsx
@@ -2,6 +2,7 @@ import {
AgGridDynamic as AgGrid,
Button,
Callout,
+ Intent,
} from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
@@ -15,7 +16,7 @@ export function Index() {
{
marketId: Array.isArray(marketId) ? marketId[0] : marketId,
},
skip: !marketId,
+ fetchPolicy: 'network-only',
}}
>
- {({ market }) =>
- w > 1050 ? (
+ {({ market }) => {
+ if (!market) {
+ return Market not found;
+ }
+
+ return w > 960 ? (
) : (
- )
- }
+ );
+ }}
);
};
diff --git a/apps/trading/pages/markets/__generated__/Market.ts b/apps/trading/pages/markets/__generated__/Market.ts
deleted file mode 100644
index 44933dd5d..000000000
--- a/apps/trading/pages/markets/__generated__/Market.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/* tslint:disable */
-/* eslint-disable */
-// @generated
-// This file was automatically generated and should not be edited.
-
-// ====================================================
-// GraphQL query operation: Market
-// ====================================================
-
-export interface Market_market_trades {
- __typename: "Trade";
- /**
- * The hash of the trade data
- */
- id: string;
- /**
- * The price of the trade (probably initially the passive order price, other determination algorithms are possible though) (uint64)
- */
- price: string;
- /**
- * The number of contracts trades, will always be <= the remaining size of both orders immediately before the trade (uint64)
- */
- size: string;
- /**
- * RFC3339Nano time for when the trade occurred
- */
- createdAt: string;
-}
-
-export interface Market_market {
- __typename: "Market";
- /**
- * Market ID
- */
- id: string;
- /**
- * Market full name
- */
- name: string;
- /**
- * Trades on a market
- */
- trades: Market_market_trades[] | null;
-}
-
-export interface Market {
- /**
- * An instrument that is trading on the VEGA network
- */
- market: Market_market | null;
-}
-
-export interface MarketVariables {
- marketId: string;
-}
diff --git a/apps/trading/pages/markets/index.page.tsx b/apps/trading/pages/markets/index.page.tsx
index 70e2c3ba8..4572c810a 100644
--- a/apps/trading/pages/markets/index.page.tsx
+++ b/apps/trading/pages/markets/index.page.tsx
@@ -1,8 +1,8 @@
import { gql } from '@apollo/client';
+import { Markets } from '@vegaprotocol/graphql';
import { PageQueryContainer } from '../../components/page-query-container';
import Link from 'next/link';
import { useRouter } from 'next/router';
-import { Markets } from './__generated__/Markets';
const MARKETS_QUERY = gql`
query Markets {
diff --git a/apps/trading/pages/markets/trade-grid.tsx b/apps/trading/pages/markets/trade-grid.tsx
index 2ae0e2538..b3b9b4c42 100644
--- a/apps/trading/pages/markets/trade-grid.tsx
+++ b/apps/trading/pages/markets/trade-grid.tsx
@@ -1,9 +1,26 @@
+import { Market_market } from '@vegaprotocol/graphql';
import classNames from 'classnames';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useState, ReactNode } from 'react';
-import { TradingView, TradingViews } from '@vegaprotocol/react-helpers';
-import { Market_market } from './__generated__/Market';
import { GridTab, GridTabs } from './grid-tabs';
+import { DealTicketContainer } from '../../components/deal-ticket-container';
+
+const Chart = () => TODO: Chart
;
+const Orderbook = () => TODO: Orderbook
;
+const Orders = () => TODO: Orders
;
+const Positions = () => TODO: Positions
;
+const Collateral = () => TODO: Collateral
;
+
+type TradingView = keyof typeof TradingViews;
+
+const TradingViews = {
+ chart: Chart,
+ ticket: DealTicketContainer,
+ orderbook: Orderbook,
+ orders: Orders,
+ positions: Positions,
+ collateral: Collateral,
+};
interface TradeGridProps {
market: Market_market;
@@ -25,7 +42,7 @@ export const TradeGrid = ({ market }: TradeGridProps) => {
-
+
@@ -88,7 +105,7 @@ export const TradePanels = ({ market }: TradePanelsProps) => {
throw new Error(`No component for view: ${view}`);
}
- return ;
+ return ;
};
return (
diff --git a/apps/trading/project.json b/apps/trading/project.json
index fc87faf48..f96461b2e 100644
--- a/apps/trading/project.json
+++ b/apps/trading/project.json
@@ -54,7 +54,7 @@
"options": {
"commands": [
{
- "command": "npx apollo client:codegen --config=apps/trading/apollo.config.js --target=typescript --globalTypesFile=apps/trading/__generated__/globalTypes.ts"
+ "command": "npx apollo client:codegen libs/graphql/src/lib/ --config=apps/trading/apollo.config.js --target=typescript --globalTypesFile=libs/graphql/src/lib/globalTypes.ts --outputFlat"
}
]
}
diff --git a/libs/deal-ticket/.babelrc b/libs/deal-ticket/.babelrc
new file mode 100644
index 000000000..ccae900be
--- /dev/null
+++ b/libs/deal-ticket/.babelrc
@@ -0,0 +1,12 @@
+{
+ "presets": [
+ [
+ "@nrwl/react/babel",
+ {
+ "runtime": "automatic",
+ "useBuiltIns": "usage"
+ }
+ ]
+ ],
+ "plugins": []
+}
diff --git a/libs/deal-ticket/.eslintrc.json b/libs/deal-ticket/.eslintrc.json
new file mode 100644
index 000000000..734ddacee
--- /dev/null
+++ b/libs/deal-ticket/.eslintrc.json
@@ -0,0 +1,18 @@
+{
+ "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {}
+ },
+ {
+ "files": ["*.js", "*.jsx"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/libs/deal-ticket/README.md b/libs/deal-ticket/README.md
new file mode 100644
index 000000000..8e0882636
--- /dev/null
+++ b/libs/deal-ticket/README.md
@@ -0,0 +1,7 @@
+# deal-ticket
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test deal-ticket` to execute the unit tests via [Jest](https://jestjs.io).
diff --git a/libs/deal-ticket/jest.config.js b/libs/deal-ticket/jest.config.js
new file mode 100644
index 000000000..e194e5637
--- /dev/null
+++ b/libs/deal-ticket/jest.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+ displayName: 'deal-ticket',
+ preset: '../../jest.preset.js',
+ transform: {
+ '^.+\\.[tj]sx?$': 'babel-jest',
+ },
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
+ coverageDirectory: '../../coverage/libs/deal-ticket',
+};
diff --git a/libs/deal-ticket/package.json b/libs/deal-ticket/package.json
new file mode 100644
index 000000000..b5b50d47e
--- /dev/null
+++ b/libs/deal-ticket/package.json
@@ -0,0 +1,4 @@
+{
+ "name": "@vegaprotocol/deal-ticket",
+ "version": "0.0.1"
+}
diff --git a/libs/deal-ticket/project.json b/libs/deal-ticket/project.json
new file mode 100644
index 000000000..941520f1a
--- /dev/null
+++ b/libs/deal-ticket/project.json
@@ -0,0 +1,43 @@
+{
+ "root": "libs/deal-ticket",
+ "sourceRoot": "libs/deal-ticket/src",
+ "projectType": "library",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@nrwl/web:rollup",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/libs/deal-ticket",
+ "tsConfig": "libs/deal-ticket/tsconfig.lib.json",
+ "project": "libs/deal-ticket/package.json",
+ "entryFile": "libs/deal-ticket/src/index.ts",
+ "external": ["react/jsx-runtime"],
+ "rollupConfig": "@nrwl/react/plugins/bundle-rollup",
+ "compiler": "babel",
+ "assets": [
+ {
+ "glob": "libs/deal-ticket/README.md",
+ "input": ".",
+ "output": "."
+ }
+ ]
+ }
+ },
+ "lint": {
+ "executor": "@nrwl/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": ["libs/deal-ticket/**/*.{ts,tsx,js,jsx}"]
+ }
+ },
+ "test": {
+ "executor": "@nrwl/jest:jest",
+ "outputs": ["coverage/libs/deal-ticket"],
+ "options": {
+ "jestConfig": "libs/deal-ticket/jest.config.js",
+ "passWithNoTests": true
+ }
+ }
+ }
+}
diff --git a/libs/deal-ticket/src/button-radio.tsx b/libs/deal-ticket/src/button-radio.tsx
new file mode 100644
index 000000000..a735e8265
--- /dev/null
+++ b/libs/deal-ticket/src/button-radio.tsx
@@ -0,0 +1,38 @@
+import { Button } from '@vegaprotocol/ui-toolkit';
+
+interface ButtonRadioProps {
+ name: string;
+ options: Array<{ value: string; text: string }>;
+ currentOption: string | null;
+ onSelect: (option: string) => void;
+}
+
+export const ButtonRadio = ({
+ name,
+ options,
+ currentOption,
+ onSelect,
+}: ButtonRadioProps) => {
+ return (
+
+ {options.map((option) => {
+ const isSelected = option.value === currentOption;
+ return (
+
+ );
+ })}
+
+ );
+};
diff --git a/libs/deal-ticket/src/deal-ticket-limit.tsx b/libs/deal-ticket/src/deal-ticket-limit.tsx
new file mode 100644
index 000000000..b327ccacd
--- /dev/null
+++ b/libs/deal-ticket/src/deal-ticket-limit.tsx
@@ -0,0 +1,78 @@
+import { FormGroup, Input } from '@vegaprotocol/ui-toolkit';
+import { OrderTimeInForce } from '@vegaprotocol/wallet';
+import { TransactionStatus } from './deal-ticket';
+import { Market_market } from '@vegaprotocol/graphql';
+import { ExpirySelector } from './expiry-selector';
+import { SideSelector } from './side-selector';
+import { SubmitButton } from './submit-button';
+import { TimeInForceSelector } from './time-in-force-selector';
+import { TypeSelector } from './type-selector';
+import { Order } from './use-order-state';
+
+interface DealTicketLimitProps {
+ order: Order;
+ updateOrder: (order: Partial) => void;
+ transactionStatus: TransactionStatus;
+ market: Market_market;
+}
+
+export const DealTicketLimit = ({
+ order,
+ updateOrder,
+ transactionStatus,
+ market,
+}: DealTicketLimitProps) => {
+ return (
+ <>
+ updateOrder({ type })} />
+ updateOrder({ side })} />
+
+
+
+ updateOrder({ size: e.target.value })}
+ className="w-full"
+ type="number"
+ data-testid="order-size"
+ />
+
+
+
@
+
+
+ updateOrder({ price: e.target.value })}
+ className="w-full"
+ type="number"
+ data-testid="order-price"
+ />
+
+
+
+ updateOrder({ timeInForce })}
+ />
+ {order.timeInForce === OrderTimeInForce.GTT && (
+ {
+ if (date) {
+ updateOrder({ expiration: date });
+ }
+ }}
+ />
+ )}
+
+ >
+ );
+};
diff --git a/libs/deal-ticket/src/deal-ticket-market.tsx b/libs/deal-ticket/src/deal-ticket-market.tsx
new file mode 100644
index 000000000..3d46e4770
--- /dev/null
+++ b/libs/deal-ticket/src/deal-ticket-market.tsx
@@ -0,0 +1,63 @@
+import { addDecimal } from '@vegaprotocol/react-helpers';
+import { FormGroup, Input } from '@vegaprotocol/ui-toolkit';
+import { Market_market } from '@vegaprotocol/graphql';
+import { TransactionStatus } from './deal-ticket';
+import { SideSelector } from './side-selector';
+import { SubmitButton } from './submit-button';
+import { TimeInForceSelector } from './time-in-force-selector';
+import { TypeSelector } from './type-selector';
+import { Order } from './use-order-state';
+
+interface DealTicketMarketProps {
+ order: Order;
+ updateOrder: (order: Partial) => void;
+ transactionStatus: TransactionStatus;
+ market: Market_market;
+}
+
+export const DealTicketMarket = ({
+ order,
+ updateOrder,
+ transactionStatus,
+ market,
+}: DealTicketMarketProps) => {
+ return (
+ <>
+ updateOrder({ type })} />
+ updateOrder({ side })} />
+
+
+
+ updateOrder({ size: e.target.value })}
+ className="w-full"
+ type="number"
+ data-testid="order-size"
+ />
+
+
+
@
+
+ {market.depth.lastTrade ? (
+ <>
+ ~{addDecimal(market.depth.lastTrade.price, market.decimalPlaces)}{' '}
+ {market.tradableInstrument.instrument.product.quoteName}
+ >
+ ) : (
+ '-'
+ )}
+
+
+ updateOrder({ timeInForce })}
+ />
+
+ >
+ );
+};
diff --git a/libs/deal-ticket/src/deal-ticket.spec.tsx b/libs/deal-ticket/src/deal-ticket.spec.tsx
new file mode 100644
index 000000000..d259afe5b
--- /dev/null
+++ b/libs/deal-ticket/src/deal-ticket.spec.tsx
@@ -0,0 +1,141 @@
+import '@testing-library/jest-dom';
+import {
+ VegaWalletContext,
+ OrderTimeInForce,
+ OrderType,
+} from '@vegaprotocol/wallet';
+import { addDecimal } from '@vegaprotocol/react-helpers';
+import { fireEvent, render, screen } from '@testing-library/react';
+import { DealTicket, Market } from './deal-ticket';
+import { Order } from './use-order-state';
+
+const order: Order = {
+ type: OrderType.Market,
+ size: '100',
+ timeInForce: OrderTimeInForce.FOK,
+ side: null,
+};
+const market: Market = {
+ id: 'market-id',
+ decimalPlaces: 2,
+ tradingMode: 'Continuous',
+ state: 'Active',
+ tradableInstrument: {
+ instrument: {
+ product: {
+ quoteName: 'quote-name',
+ settlementAsset: {
+ id: 'asset-id',
+ symbol: 'asset-symbol',
+ name: 'asset-name',
+ },
+ },
+ },
+ },
+ depth: {
+ lastTrade: {
+ price: '100',
+ },
+ },
+};
+
+function generateJsx() {
+ return (
+
+
+
+ );
+}
+
+test('Deal ticket defaults', () => {
+ render(generateJsx());
+
+ // Assert defaults are used
+ expect(
+ screen.getByTestId(`order-type-${order.type}-selected`)
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('order-side-SIDE_BUY-selected')
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId('order-side-SIDE_SELL-selected')
+ ).not.toBeInTheDocument();
+ expect(screen.getByTestId('order-size')).toHaveDisplayValue(order.size);
+ expect(screen.getByTestId('order-tif')).toHaveValue(order.timeInForce);
+
+ // Assert last price is shown
+ expect(screen.getByTestId('last-price')).toHaveTextContent(
+ // eslint-disable-next-line
+ `~${addDecimal(market.depth.lastTrade!.price, market.decimalPlaces)} ${
+ market.tradableInstrument.instrument.product.quoteName
+ }`
+ );
+});
+
+test('Can edit deal ticket', () => {
+ render(generateJsx());
+
+ // Asssert changing values
+ fireEvent.click(screen.getByTestId('order-side-SIDE_BUY'));
+ expect(
+ screen.getByTestId('order-side-SIDE_BUY-selected')
+ ).toBeInTheDocument();
+
+ fireEvent.change(screen.getByTestId('order-size'), {
+ target: { value: '200' },
+ });
+ expect(screen.getByTestId('order-size')).toHaveDisplayValue('200');
+
+ fireEvent.change(screen.getByTestId('order-tif'), {
+ target: { value: OrderTimeInForce.IOC },
+ });
+ expect(screen.getByTestId('order-tif')).toHaveValue(OrderTimeInForce.IOC);
+
+ // Switch to limit order
+ fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
+
+ // Assert price input shown with default value
+ expect(screen.getByTestId('order-price')).toHaveDisplayValue('0');
+
+ // Check all TIF options shown
+ expect(screen.getByTestId('order-tif').children).toHaveLength(
+ Object.keys(OrderTimeInForce).length
+ );
+});
+
+test('Handles TIF select box dependent on order type', () => {
+ render(generateJsx());
+
+ // Check only IOC and
+ expect(
+ Array.from(screen.getByTestId('order-tif').children).map(
+ (o) => o.textContent
+ )
+ ).toEqual(['IOC', 'FOK']);
+
+ // Switch to limit order and check all TIF options shown
+ fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
+ expect(screen.getByTestId('order-tif').children).toHaveLength(
+ Object.keys(OrderTimeInForce).length
+ );
+
+ // Change to GTC
+ fireEvent.change(screen.getByTestId('order-tif'), {
+ target: { value: OrderTimeInForce.GTC },
+ });
+ expect(screen.getByTestId('order-tif')).toHaveValue(OrderTimeInForce.GTC);
+
+ // Switch back to market order and TIF should now be IOC
+ fireEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
+ expect(screen.getByTestId('order-tif')).toHaveValue(OrderTimeInForce.IOC);
+
+ // Switch tif to FOK
+ fireEvent.change(screen.getByTestId('order-tif'), {
+ target: { value: OrderTimeInForce.FOK },
+ });
+ expect(screen.getByTestId('order-tif')).toHaveValue(OrderTimeInForce.FOK);
+
+ // Change back to limit and check we are still on FOK
+ fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
+ expect(screen.getByTestId('order-tif')).toHaveValue(OrderTimeInForce.FOK);
+});
diff --git a/libs/deal-ticket/src/deal-ticket.tsx b/libs/deal-ticket/src/deal-ticket.tsx
new file mode 100644
index 000000000..a49dbf958
--- /dev/null
+++ b/libs/deal-ticket/src/deal-ticket.tsx
@@ -0,0 +1,69 @@
+import { OrderSide, OrderTimeInForce, OrderType } from '@vegaprotocol/wallet';
+import { Market_market } from '@vegaprotocol/graphql';
+import { FormEvent } from 'react';
+import { Order, useOrderState } from './use-order-state';
+import { DealTicketMarket } from './deal-ticket-market';
+import { DealTicketLimit } from './deal-ticket-limit';
+
+const DEFAULT_ORDER: Order = {
+ type: OrderType.Market,
+ side: OrderSide.Buy,
+ size: '1',
+ timeInForce: OrderTimeInForce.IOC,
+};
+
+// TODO: Consider using a generated type when we have a better solution for
+// sharing the types from GQL
+
+export type TransactionStatus = 'default' | 'pending';
+
+export interface DealTicketProps {
+ market: Market_market;
+ submit: (order: Order) => void;
+ transactionStatus: TransactionStatus;
+ defaultOrder?: Order;
+}
+
+export const DealTicket = ({
+ market,
+ submit,
+ transactionStatus,
+ defaultOrder = DEFAULT_ORDER,
+}: DealTicketProps) => {
+ const [order, updateOrder] = useOrderState(defaultOrder);
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+ submit(order);
+ };
+
+ let ticket = null;
+
+ if (order.type === OrderType.Market) {
+ ticket = (
+
+ );
+ } else if (order.type === OrderType.Limit) {
+ ticket = (
+
+ );
+ } else {
+ throw new Error('Invalid ticket type');
+ }
+
+ return (
+
+ );
+};
diff --git a/libs/deal-ticket/src/expiry-selector.tsx b/libs/deal-ticket/src/expiry-selector.tsx
new file mode 100644
index 000000000..01f01243d
--- /dev/null
+++ b/libs/deal-ticket/src/expiry-selector.tsx
@@ -0,0 +1,26 @@
+import { FormGroup, Input } from '@vegaprotocol/ui-toolkit';
+import { Order } from './use-order-state';
+import { formatForInput } from '@vegaprotocol/react-helpers';
+
+interface ExpirySelectorProps {
+ order: Order;
+ onSelect: (expiration: Date | null) => void;
+}
+
+export const ExpirySelector = ({ order, onSelect }: ExpirySelectorProps) => {
+ const date = order.expiration ? new Date(order.expiration) : new Date();
+ const dateFormatted = formatForInput(date);
+ const minDate = formatForInput(date);
+ return (
+
+ onSelect(new Date(e.target.value))}
+ min={minDate}
+ />
+
+ );
+};
diff --git a/libs/deal-ticket/src/index.ts b/libs/deal-ticket/src/index.ts
new file mode 100644
index 000000000..22bedf9bd
--- /dev/null
+++ b/libs/deal-ticket/src/index.ts
@@ -0,0 +1,2 @@
+export * from './deal-ticket';
+export * from './use-order-state';
diff --git a/libs/deal-ticket/src/side-selector.tsx b/libs/deal-ticket/src/side-selector.tsx
new file mode 100644
index 000000000..029ea318a
--- /dev/null
+++ b/libs/deal-ticket/src/side-selector.tsx
@@ -0,0 +1,25 @@
+import { FormGroup } from '@vegaprotocol/ui-toolkit';
+import { OrderSide } from '@vegaprotocol/wallet';
+import { ButtonRadio } from './button-radio';
+import { Order } from './use-order-state';
+
+interface SideSelectorProps {
+ order: Order;
+ onSelect: (side: OrderSide) => void;
+}
+
+export const SideSelector = ({ order, onSelect }: SideSelectorProps) => {
+ return (
+
+ ({
+ text,
+ value,
+ }))}
+ currentOption={order.side}
+ onSelect={(value) => onSelect(value as OrderSide)}
+ />
+
+ );
+};
diff --git a/libs/deal-ticket/src/submit-button.tsx b/libs/deal-ticket/src/submit-button.tsx
new file mode 100644
index 000000000..610c78633
--- /dev/null
+++ b/libs/deal-ticket/src/submit-button.tsx
@@ -0,0 +1,78 @@
+import { Button, InputError } from '@vegaprotocol/ui-toolkit';
+import { OrderTimeInForce, OrderType } from '@vegaprotocol/wallet';
+import { Market_market } from '@vegaprotocol/graphql';
+import { useMemo } from 'react';
+import { Order } from './use-order-state';
+import { useVegaWallet } from '@vegaprotocol/wallet';
+import { TransactionStatus } from './deal-ticket';
+
+interface SubmitButtonProps {
+ transactionStatus: TransactionStatus;
+ market: Market_market;
+ order: Order;
+}
+
+export const SubmitButton = ({
+ market,
+ transactionStatus,
+ order,
+}: SubmitButtonProps) => {
+ const { keypair } = useVegaWallet();
+
+ const invalidText = useMemo(() => {
+ if (!keypair) {
+ return 'No public key selected';
+ }
+
+ if (keypair.tainted) {
+ return 'Selected public key has been tainted';
+ }
+
+ // TODO: Change these to use enums from @vegaprotocol/graphql
+ if (market.state !== 'Active') {
+ if (market.state === 'Suspended') {
+ return 'Market is currently suspended';
+ }
+
+ if (market.state === 'Proposed' || market.state === 'Pending') {
+ return 'Market is not active yet';
+ }
+
+ return 'Market is no longer active';
+ }
+
+ if (market.tradingMode !== 'Continuous') {
+ if (order.type === OrderType.Market) {
+ return 'Only limit orders are permitted when market is in auction';
+ }
+
+ if (
+ [
+ OrderTimeInForce.FOK,
+ OrderTimeInForce.IOC,
+ OrderTimeInForce.GFN,
+ ].includes(order.timeInForce)
+ ) {
+ return 'Only GTT, GTC and GFA are permitted when market is in auction';
+ }
+ }
+
+ return '';
+ }, [keypair, market, order]);
+
+ const disabled = transactionStatus === 'pending' || Boolean(invalidText);
+
+ return (
+ <>
+
+ {invalidText && {invalidText}}
+ >
+ );
+};
diff --git a/libs/deal-ticket/src/time-in-force-selector.tsx b/libs/deal-ticket/src/time-in-force-selector.tsx
new file mode 100644
index 000000000..6c793ea46
--- /dev/null
+++ b/libs/deal-ticket/src/time-in-force-selector.tsx
@@ -0,0 +1,40 @@
+import { FormGroup, Select } from '@vegaprotocol/ui-toolkit';
+import { OrderTimeInForce, OrderType } from '@vegaprotocol/wallet';
+import { Order } from './use-order-state';
+
+interface TimeInForceSelectorProps {
+ order: Order;
+ onSelect: (tif: OrderTimeInForce) => void;
+}
+
+export const TimeInForceSelector = ({
+ order,
+ onSelect,
+}: TimeInForceSelectorProps) => {
+ const options =
+ order.type === OrderType.Limit
+ ? Object.entries(OrderTimeInForce)
+ : Object.entries(OrderTimeInForce).filter(
+ ([key, value]) =>
+ value === OrderTimeInForce.FOK || value === OrderTimeInForce.IOC
+ );
+
+ return (
+
+
+
+ );
+};
diff --git a/libs/deal-ticket/src/type-selector.tsx b/libs/deal-ticket/src/type-selector.tsx
new file mode 100644
index 000000000..b3621843a
--- /dev/null
+++ b/libs/deal-ticket/src/type-selector.tsx
@@ -0,0 +1,25 @@
+import { FormGroup } from '@vegaprotocol/ui-toolkit';
+import { OrderType } from '@vegaprotocol/wallet';
+import { ButtonRadio } from './button-radio';
+import { Order } from './use-order-state';
+
+interface TypeSelectorProps {
+ order: Order;
+ onSelect: (type: OrderType) => void;
+}
+
+export const TypeSelector = ({ order, onSelect }: TypeSelectorProps) => {
+ return (
+
+ ({
+ text,
+ value,
+ }))}
+ currentOption={order.type}
+ onSelect={(value) => onSelect(value as OrderType)}
+ />
+
+ );
+};
diff --git a/libs/deal-ticket/src/use-order-state.ts b/libs/deal-ticket/src/use-order-state.ts
new file mode 100644
index 000000000..42545ce01
--- /dev/null
+++ b/libs/deal-ticket/src/use-order-state.ts
@@ -0,0 +1,78 @@
+import { OrderSide, OrderTimeInForce, OrderType } from '@vegaprotocol/wallet';
+import { useState, useCallback } from 'react';
+
+export interface Order {
+ size: string;
+ type: OrderType;
+ timeInForce: OrderTimeInForce;
+ side: OrderSide | null;
+ price?: string;
+ expiration?: Date;
+}
+
+export type UpdateOrder = (order: Partial) => void;
+
+export const useOrderState = (defaultOrder: Order): [Order, UpdateOrder] => {
+ const [order, setOrder] = useState(defaultOrder);
+
+ const updateOrder = useCallback((orderUpdate: Partial) => {
+ setOrder((curr) => {
+ // Type is switching to market so return new market order object with correct defaults
+ if (
+ orderUpdate.type === OrderType.Market &&
+ curr.type !== OrderType.Market
+ ) {
+ // Check if provided TIF or current TIF is valid for a market order and default
+ // to IOC if its not
+
+ const isTifValid = (tif: OrderTimeInForce) => {
+ return tif === OrderTimeInForce.FOK || tif === OrderTimeInForce.IOC;
+ };
+
+ // Default
+ let timeInForce = OrderTimeInForce.IOC;
+
+ if (orderUpdate.timeInForce) {
+ if (isTifValid(orderUpdate.timeInForce)) {
+ timeInForce = orderUpdate.timeInForce;
+ }
+ } else {
+ if (isTifValid(curr.timeInForce)) {
+ timeInForce = curr.timeInForce;
+ }
+ }
+
+ return {
+ type: orderUpdate.type,
+ size: orderUpdate.size || curr.size,
+ side: orderUpdate.side || curr.side,
+ timeInForce,
+ price: undefined,
+ expiration: undefined,
+ };
+ }
+
+ // Type is switching to limit so return new order object with correct defaults
+ if (
+ orderUpdate.type === OrderType.Limit &&
+ curr.type !== OrderType.Limit
+ ) {
+ return {
+ type: orderUpdate.type,
+ size: orderUpdate.size || curr.size,
+ side: orderUpdate.side || curr.side,
+ timeInForce: orderUpdate.timeInForce || curr.timeInForce,
+ price: orderUpdate.price || '0',
+ expiration: orderUpdate.expiration || undefined,
+ };
+ }
+
+ return {
+ ...curr,
+ ...orderUpdate,
+ };
+ });
+ }, []);
+
+ return [order, updateOrder];
+};
diff --git a/libs/deal-ticket/tsconfig.json b/libs/deal-ticket/tsconfig.json
new file mode 100644
index 000000000..4c089585e
--- /dev/null
+++ b/libs/deal-ticket/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "allowJs": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ]
+}
diff --git a/libs/deal-ticket/tsconfig.lib.json b/libs/deal-ticket/tsconfig.lib.json
new file mode 100644
index 000000000..252904bb7
--- /dev/null
+++ b/libs/deal-ticket/tsconfig.lib.json
@@ -0,0 +1,22 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "types": ["node"]
+ },
+ "files": [
+ "../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
+ "../../node_modules/@nrwl/react/typings/image.d.ts"
+ ],
+ "exclude": [
+ "**/*.spec.ts",
+ "**/*.test.ts",
+ "**/*.spec.tsx",
+ "**/*.test.tsx",
+ "**/*.spec.js",
+ "**/*.test.js",
+ "**/*.spec.jsx",
+ "**/*.test.jsx"
+ ],
+ "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
+}
diff --git a/libs/deal-ticket/tsconfig.spec.json b/libs/deal-ticket/tsconfig.spec.json
new file mode 100644
index 000000000..67f149c4c
--- /dev/null
+++ b/libs/deal-ticket/tsconfig.spec.json
@@ -0,0 +1,19 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "module": "commonjs",
+ "types": ["jest", "node"]
+ },
+ "include": [
+ "**/*.test.ts",
+ "**/*.spec.ts",
+ "**/*.test.tsx",
+ "**/*.spec.tsx",
+ "**/*.test.js",
+ "**/*.spec.js",
+ "**/*.test.jsx",
+ "**/*.spec.jsx",
+ "**/*.d.ts"
+ ]
+}
diff --git a/libs/graphql/README.md b/libs/graphql/README.md
new file mode 100644
index 000000000..90e4d61d8
--- /dev/null
+++ b/libs/graphql/README.md
@@ -0,0 +1,7 @@
+# graphql
+
+This library was generated with [Nx](https://nx.dev).
+
+## Building
+
+Run `nx build graphql` to build the library.
diff --git a/libs/graphql/package.json b/libs/graphql/package.json
new file mode 100644
index 000000000..7b0a2b1d3
--- /dev/null
+++ b/libs/graphql/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@vegaprotocol/graphql",
+ "version": "0.0.1",
+ "type": "commonjs"
+}
diff --git a/libs/graphql/project.json b/libs/graphql/project.json
new file mode 100644
index 000000000..90fff2a0b
--- /dev/null
+++ b/libs/graphql/project.json
@@ -0,0 +1,18 @@
+{
+ "root": "libs/graphql",
+ "sourceRoot": "libs/graphql/src",
+ "projectType": "library",
+ "targets": {
+ "build": {
+ "executor": "@nrwl/js:tsc",
+ "outputs": ["{options.outputPath}"],
+ "options": {
+ "outputPath": "dist/libs/graphql",
+ "main": "libs/graphql/src/index.ts",
+ "tsConfig": "libs/graphql/tsconfig.lib.json",
+ "assets": ["libs/graphql/*.md"]
+ }
+ }
+ },
+ "tags": []
+}
diff --git a/libs/graphql/src/index.ts b/libs/graphql/src/index.ts
new file mode 100644
index 000000000..34e1ecfc8
--- /dev/null
+++ b/libs/graphql/src/index.ts
@@ -0,0 +1,4 @@
+export * from './lib/globalTypes';
+export * from './lib/Market';
+export * from './lib/Markets';
+export * from './lib/OrderEvent';
diff --git a/libs/graphql/src/lib/Market.ts b/libs/graphql/src/lib/Market.ts
new file mode 100644
index 000000000..fbba42220
--- /dev/null
+++ b/libs/graphql/src/lib/Market.ts
@@ -0,0 +1,150 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+import { MarketState, MarketTradingMode } from './globalTypes';
+
+// ====================================================
+// GraphQL query operation: Market
+// ====================================================
+
+export interface Market_market_tradableInstrument_instrument_product_settlementAsset {
+ __typename: 'Asset';
+ /**
+ * The id of the asset
+ */
+ id: string;
+ /**
+ * The symbol of the asset (e.g: GBP)
+ */
+ symbol: string;
+ /**
+ * The full name of the asset (e.g: Great British Pound)
+ */
+ name: string;
+}
+
+export interface Market_market_tradableInstrument_instrument_product {
+ __typename: 'Future';
+ /**
+ * String representing the quote (e.g. BTCUSD -> USD is quote)
+ */
+ quoteName: string;
+ /**
+ * The name of the asset (string)
+ */
+ settlementAsset: Market_market_tradableInstrument_instrument_product_settlementAsset;
+}
+
+export interface Market_market_tradableInstrument_instrument {
+ __typename: 'Instrument';
+ /**
+ * A reference to or instance of a fully specified product, including all required product parameters for that product (Product union)
+ */
+ product: Market_market_tradableInstrument_instrument_product;
+}
+
+export interface Market_market_tradableInstrument {
+ __typename: 'TradableInstrument';
+ /**
+ * An instance of or reference to a fully specified instrument.
+ */
+ instrument: Market_market_tradableInstrument_instrument;
+}
+
+export interface Market_market_trades {
+ __typename: 'Trade';
+ /**
+ * The hash of the trade data
+ */
+ id: string;
+ /**
+ * The price of the trade (probably initially the passive order price, other determination algorithms are possible though) (uint64)
+ */
+ price: string;
+ /**
+ * The number of contracts trades, will always be <= the remaining size of both orders immediately before the trade (uint64)
+ */
+ size: string;
+ /**
+ * RFC3339Nano time for when the trade occurred
+ */
+ createdAt: string;
+}
+
+export interface Market_market_depth_lastTrade {
+ __typename: 'Trade';
+ /**
+ * The price of the trade (probably initially the passive order price, other determination algorithms are possible though) (uint64)
+ */
+ price: string;
+}
+
+export interface Market_market_depth {
+ __typename: 'MarketDepth';
+ /**
+ * Last trade for the given market (if available)
+ */
+ lastTrade: Market_market_depth_lastTrade | null;
+}
+
+export interface Market_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;
+ /**
+ * Current state of the market
+ */
+ state: MarketState;
+ /**
+ * Current mode of execution of the market
+ */
+ tradingMode: MarketTradingMode;
+ /**
+ * An instance of or reference to a tradable instrument.
+ */
+ tradableInstrument: Market_market_tradableInstrument;
+ /**
+ * Trades on a market
+ */
+ trades: Market_market_trades[] | null;
+ /**
+ * Current depth on the order book for this market
+ */
+ depth: Market_market_depth;
+}
+
+export interface Market {
+ /**
+ * An instrument that is trading on the VEGA network
+ */
+ market: Market_market | null;
+}
+
+export interface MarketVariables {
+ marketId: string;
+}
diff --git a/apps/trading/pages/markets/__generated__/Markets.ts b/libs/graphql/src/lib/Markets.ts
similarity index 95%
rename from apps/trading/pages/markets/__generated__/Markets.ts
rename to libs/graphql/src/lib/Markets.ts
index 1b9efcdf5..52c4f04fd 100644
--- a/apps/trading/pages/markets/__generated__/Markets.ts
+++ b/libs/graphql/src/lib/Markets.ts
@@ -8,7 +8,7 @@
// ====================================================
export interface Markets_markets {
- __typename: "Market";
+ __typename: 'Market';
/**
* Market ID
*/
diff --git a/libs/graphql/src/lib/OrderEvent.ts b/libs/graphql/src/lib/OrderEvent.ts
new file mode 100644
index 000000000..33cabf251
--- /dev/null
+++ b/libs/graphql/src/lib/OrderEvent.ts
@@ -0,0 +1,122 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+import {
+ BusEventType,
+ OrderType,
+ OrderStatus,
+ OrderRejectionReason,
+} from './globalTypes';
+
+// ====================================================
+// 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;
+}
+
+export interface OrderEvent_busEvents_event_Order {
+ __typename: 'Order';
+ /**
+ * Type the order type (defaults to PARTY)
+ */
+ type: OrderType | null;
+ /**
+ * Hash of the order data
+ */
+ id: string;
+ /**
+ * The status of an order, for example 'Active'
+ */
+ status: OrderStatus;
+ /**
+ * Reason for the order to be rejected
+ */
+ rejectionReason: OrderRejectionReason | null;
+ /**
+ * RFC3339Nano formatted date and time for when the order was created (timestamp)
+ */
+ createdAt: string;
+ /**
+ * Total number of contracts that may be bought or sold (immutable) (uint64)
+ */
+ size: string;
+ /**
+ * The worst price the order will trade at (e.g. buy for price or less, sell for price or more) (uint64)
+ */
+ price: string;
+ /**
+ * The market the order is trading on (probably stored internally as a hash of the market details)
+ */
+ market: OrderEvent_busEvents_event_Order_market | null;
+}
+
+export type OrderEvent_busEvents_event =
+ | OrderEvent_busEvents_event_TimeUpdate
+ | OrderEvent_busEvents_event_Order;
+
+export interface OrderEvent_busEvents {
+ __typename: 'BusEvent';
+ /**
+ * the id for this event
+ */
+ eventId: string;
+ /**
+ * the block hash
+ */
+ block: string;
+ /**
+ * the type of event we're dealing with
+ */
+ type: BusEventType;
+ /**
+ * the payload - the wrapped event
+ */
+ event: OrderEvent_busEvents_event;
+}
+
+export interface OrderEvent {
+ /**
+ * Subscribe to event data from the event bus
+ */
+ busEvents: OrderEvent_busEvents[] | null;
+}
+
+export interface OrderEventVariables {
+ partyId: string;
+}
diff --git a/libs/graphql/src/lib/globalTypes.ts b/libs/graphql/src/lib/globalTypes.ts
new file mode 100644
index 000000000..7848b1dcd
--- /dev/null
+++ b/libs/graphql/src/lib/globalTypes.ts
@@ -0,0 +1,137 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+//==============================================================
+// START Enums and Input Objects
+//==============================================================
+
+export enum BusEventType {
+ Account = 'Account',
+ Asset = 'Asset',
+ Auction = 'Auction',
+ Deposit = 'Deposit',
+ LiquidityProvision = 'LiquidityProvision',
+ LossSocialization = 'LossSocialization',
+ MarginLevels = 'MarginLevels',
+ Market = 'Market',
+ MarketCreated = 'MarketCreated',
+ MarketData = 'MarketData',
+ MarketTick = 'MarketTick',
+ MarketUpdated = 'MarketUpdated',
+ NodeSignature = 'NodeSignature',
+ OracleSpec = 'OracleSpec',
+ Order = 'Order',
+ Party = 'Party',
+ PositionResolution = 'PositionResolution',
+ Proposal = 'Proposal',
+ RiskFactor = 'RiskFactor',
+ SettleDistressed = 'SettleDistressed',
+ SettlePosition = 'SettlePosition',
+ TimeUpdate = 'TimeUpdate',
+ Trade = 'Trade',
+ TransferResponses = 'TransferResponses',
+ Vote = 'Vote',
+ Withdrawal = 'Withdrawal',
+}
+
+/**
+ * The current state of a market
+ */
+export enum MarketState {
+ Active = 'Active',
+ Cancelled = 'Cancelled',
+ Closed = 'Closed',
+ Pending = 'Pending',
+ Proposed = 'Proposed',
+ Rejected = 'Rejected',
+ Settled = 'Settled',
+ Suspended = 'Suspended',
+ TradingTerminated = 'TradingTerminated',
+}
+
+/**
+ * What market trading mode are we in
+ */
+export enum MarketTradingMode {
+ BatchAuction = 'BatchAuction',
+ Continuous = 'Continuous',
+ MonitoringAuction = 'MonitoringAuction',
+ OpeningAuction = 'OpeningAuction',
+}
+
+/**
+ * Reason for the order being rejected by the core node
+ */
+export enum OrderRejectionReason {
+ AmendToGTTWithoutExpiryAt = 'AmendToGTTWithoutExpiryAt',
+ CannotAmendFromGFAOrGFN = 'CannotAmendFromGFAOrGFN',
+ CannotAmendPeggedOrderDetailsOnNonPeggedOrder = 'CannotAmendPeggedOrderDetailsOnNonPeggedOrder',
+ CannotAmendToFOKOrIOC = 'CannotAmendToFOKOrIOC',
+ CannotAmendToGFAOrGFN = 'CannotAmendToGFAOrGFN',
+ EditNotAllowed = 'EditNotAllowed',
+ ExpiryAtBeforeCreatedAt = 'ExpiryAtBeforeCreatedAt',
+ FOKOrderDuringAuction = 'FOKOrderDuringAuction',
+ GFAOrderDuringContinuousTrading = 'GFAOrderDuringContinuousTrading',
+ GFNOrderDuringAuction = 'GFNOrderDuringAuction',
+ GTCWithExpiryAtNotValid = 'GTCWithExpiryAtNotValid',
+ IOCOrderDuringAuction = 'IOCOrderDuringAuction',
+ InsufficientAssetBalance = 'InsufficientAssetBalance',
+ InsufficientFundsToPayFees = 'InsufficientFundsToPayFees',
+ InternalError = 'InternalError',
+ InvalidExpirationTime = 'InvalidExpirationTime',
+ InvalidMarketId = 'InvalidMarketId',
+ InvalidMarketType = 'InvalidMarketType',
+ InvalidOrderId = 'InvalidOrderId',
+ InvalidOrderReference = 'InvalidOrderReference',
+ InvalidPartyId = 'InvalidPartyId',
+ InvalidPersistence = 'InvalidPersistence',
+ InvalidRemainingSize = 'InvalidRemainingSize',
+ InvalidSize = 'InvalidSize',
+ InvalidTimeInForce = 'InvalidTimeInForce',
+ InvalidType = 'InvalidType',
+ MarginCheckFailed = 'MarginCheckFailed',
+ MarketClosed = 'MarketClosed',
+ MissingGeneralAccount = 'MissingGeneralAccount',
+ NonPersistentOrderExceedsPriceBounds = 'NonPersistentOrderExceedsPriceBounds',
+ OrderAmendFailure = 'OrderAmendFailure',
+ OrderNotFound = 'OrderNotFound',
+ OrderOutOfSequence = 'OrderOutOfSequence',
+ OrderRemovalFailure = 'OrderRemovalFailure',
+ PeggedOrderBuyCannotReferenceBestAskPrice = 'PeggedOrderBuyCannotReferenceBestAskPrice',
+ PeggedOrderMustBeGTTOrGTC = 'PeggedOrderMustBeGTTOrGTC',
+ PeggedOrderMustBeLimitOrder = 'PeggedOrderMustBeLimitOrder',
+ PeggedOrderOffsetMustBeGreaterOrEqualToZero = 'PeggedOrderOffsetMustBeGreaterOrEqualToZero',
+ PeggedOrderOffsetMustBeGreaterThanZero = 'PeggedOrderOffsetMustBeGreaterThanZero',
+ PeggedOrderSellCannotReferenceBestBidPrice = 'PeggedOrderSellCannotReferenceBestBidPrice',
+ PeggedOrderWithoutReferencePrice = 'PeggedOrderWithoutReferencePrice',
+ SelfTrading = 'SelfTrading',
+ TimeFailure = 'TimeFailure',
+ UnableToAmendPeggedOrderPrice = 'UnableToAmendPeggedOrderPrice',
+ UnableToRepricePeggedOrder = 'UnableToRepricePeggedOrder',
+}
+
+/**
+ * Valid order statuses, these determine several states for an order that cannot be expressed with other fields in Order.
+ */
+export enum OrderStatus {
+ Active = 'Active',
+ Cancelled = 'Cancelled',
+ Expired = 'Expired',
+ Filled = 'Filled',
+ Parked = 'Parked',
+ PartiallyFilled = 'PartiallyFilled',
+ Rejected = 'Rejected',
+ Stopped = 'Stopped',
+}
+
+export enum OrderType {
+ Limit = 'Limit',
+ Market = 'Market',
+ Network = 'Network',
+}
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================
diff --git a/libs/graphql/tsconfig.json b/libs/graphql/tsconfig.json
new file mode 100644
index 000000000..696b638de
--- /dev/null
+++ b/libs/graphql/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "module": "CommonJS",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ }
+ ]
+}
diff --git a/libs/graphql/tsconfig.lib.json b/libs/graphql/tsconfig.lib.json
new file mode 100644
index 000000000..a8b9431f9
--- /dev/null
+++ b/libs/graphql/tsconfig.lib.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "declaration": true,
+ "types": []
+ },
+ "include": ["**/*.ts"],
+ "exclude": ["**/*.spec.ts"]
+}
diff --git a/libs/react-helpers/src/index.ts b/libs/react-helpers/src/index.ts
index 61504ca1d..3a01e2b38 100644
--- a/libs/react-helpers/src/index.ts
+++ b/libs/react-helpers/src/index.ts
@@ -1,3 +1,4 @@
export * from './lib/context';
export * from './lib/storage';
-export * from './lib/trading';
+export * from './lib/datetime';
+export * from './lib/decimals';
diff --git a/libs/react-helpers/src/lib/datetime/datetime.ts b/libs/react-helpers/src/lib/datetime/datetime.ts
new file mode 100644
index 000000000..e792f8968
--- /dev/null
+++ b/libs/react-helpers/src/lib/datetime/datetime.ts
@@ -0,0 +1,13 @@
+/** Returns date in a format suitable for input[type=date] elements */
+export const formatForInput = (date: Date) => {
+ const padZero = (num: number) => num.toString().padStart(2, '0');
+
+ const year = date.getFullYear();
+ const month = padZero(date.getMonth() + 1);
+ const day = padZero(date.getDate());
+ const hours = padZero(date.getHours());
+ const minutes = padZero(date.getMinutes());
+ const secs = padZero(date.getSeconds());
+
+ return `${year}-${month}-${day}T${hours}:${minutes}:${secs}`;
+};
diff --git a/libs/react-helpers/src/lib/datetime/index.ts b/libs/react-helpers/src/lib/datetime/index.ts
new file mode 100644
index 000000000..bb1c9cca2
--- /dev/null
+++ b/libs/react-helpers/src/lib/datetime/index.ts
@@ -0,0 +1 @@
+export * from './datetime';
diff --git a/libs/react-helpers/src/lib/decimals/index.ts b/libs/react-helpers/src/lib/decimals/index.ts
new file mode 100644
index 000000000..bedea9595
--- /dev/null
+++ b/libs/react-helpers/src/lib/decimals/index.ts
@@ -0,0 +1,12 @@
+import { BigNumber } from 'bignumber.js';
+
+export function addDecimal(value: string, decimals: number): string {
+ if (!decimals) return value;
+ return new BigNumber(value || 0)
+ .dividedBy(Math.pow(10, decimals))
+ .toFixed(decimals);
+}
+export function removeDecimal(value: string, decimals: number): string {
+ if (!decimals) return value;
+ return new BigNumber(value || 0).times(Math.pow(10, decimals)).toFixed(0);
+}
diff --git a/libs/react-helpers/src/lib/trading/index.tsx b/libs/react-helpers/src/lib/trading/index.tsx
deleted file mode 100644
index ad460b543..000000000
--- a/libs/react-helpers/src/lib/trading/index.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-export const Chart = () => TODO: Chart
;
-export const Ticket = () => TODO: Ticket
;
-export const Orderbook = () => TODO: Orderbook
;
-export const Orders = () => TODO: Orders
;
-export const Positions = () => TODO: Positions
;
-export const Collateral = () => TODO: Collateral
;
-
-export type TradingView = keyof typeof TradingViews;
-
-export const TradingViews = {
- chart: Chart,
- ticket: Ticket,
- orderbook: Orderbook,
- orders: Orders,
- positions: Positions,
- collateral: Collateral,
-};
diff --git a/libs/tailwindcss-config/src/theme.js b/libs/tailwindcss-config/src/theme.js
index 4bc3462b1..d093df574 100644
--- a/libs/tailwindcss-config/src/theme.js
+++ b/libs/tailwindcss-config/src/theme.js
@@ -141,11 +141,9 @@ module.exports = {
'ui-small': ['10px', '16px'],
},
- extend: {
- boxShadow: {
- callout: '5px 5px 0 1px rgba(255, 255, 255, 0.05)',
- focus: '0px 0px 0px 1px #FFFFFF, 0px 0px 3px 2px #FFE600',
- 'focus-dark': '0px 0px 0px 1px #000000, 0px 0px 3px 2px #FFE600',
- },
+ boxShadow: {
+ callout: '5px 5px 0 1px rgba(255, 255, 255, 0.05)',
+ focus: '0px 0px 0px 1px #FFFFFF, 0px 0px 3px 2px #FFE600',
+ 'focus-dark': '0px 0px 0px 1px #000000, 0px 0px 3px 2px #FFE600',
},
};
diff --git a/libs/ui-toolkit/.storybook/preview.js b/libs/ui-toolkit/.storybook/preview.js
index 6258bc80b..6d8a1840c 100644
--- a/libs/ui-toolkit/.storybook/preview.js
+++ b/libs/ui-toolkit/.storybook/preview.js
@@ -18,12 +18,22 @@ export const decorators = [
) : (
),
];
+
+const StoryWrapper = ({ children, className }) => (
+
+);
diff --git a/libs/ui-toolkit/src/components/button/button.tsx b/libs/ui-toolkit/src/components/button/button.tsx
index 6402fa382..e53d0fef0 100644
--- a/libs/ui-toolkit/src/components/button/button.tsx
+++ b/libs/ui-toolkit/src/components/button/button.tsx
@@ -128,6 +128,7 @@ export const Button = forwardRef
(
(
{
variant = 'primary',
+ type = 'button',
children,
className,
prependIconName,
@@ -137,7 +138,12 @@ export const Button = forwardRef(
ref
) => {
return (
-