From a2fbfedc0263c5071ab5084886b41fb7091454e7 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Thu, 30 Sep 2021 13:31:32 +1000 Subject: [PATCH] UI state mapping & dynamic actions All display related decisions are taken in the UI, but on top of the UI's model. For this purpose we introduce classes for `CfdState` and `Position` so we can add the relevant mapping functions to these classes. To achieve the mapping from daemon sse even to the `Cfd` / `Order` interface (that now contain classes instead of just primitives) we extend the sse hook to accept a mapping function. We define this mapping function for `Cfd` and `Order`, because those contain classes, for all others we just use the default mapping. Actions are dynamically rendered based on the state. The daemon decides on the action name. A single post endpoint handles all actions. The UI maps the actions to icons. Co-authored-by: Thomas Eizinger --- Cargo.lock | 10 + daemon/Cargo.toml | 1 + daemon/src/maker.rs | 3 +- daemon/src/model/cfd.rs | 12 +- daemon/src/routes_maker.rs | 51 ++-- daemon/src/to_sse_event.rs | 99 +++++++- frontend/src/MakerApp.tsx | 46 ++-- frontend/src/MakerClient.tsx | 18 -- frontend/src/TakerApp.tsx | 27 +- frontend/src/components/Hooks.tsx | 13 +- frontend/src/components/Types.tsx | 164 +++++++++++- .../src/components/cfdtables/CfdTable.tsx | 104 ++++++-- .../components/cfdtables/CfdTableMaker.tsx | 236 ------------------ 13 files changed, 434 insertions(+), 350 deletions(-) delete mode 100644 frontend/src/components/cfdtables/CfdTableMaker.tsx diff --git a/Cargo.lock b/Cargo.lock index 7568679..bca6401 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,6 +507,7 @@ dependencies = [ "rust_decimal_macros", "serde", "serde_json", + "serde_plain", "serde_with", "sha2", "sqlx", @@ -2154,6 +2155,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95455e7e29fada2052e72170af226fbe368a4ca33dee847875325d9fdb133858" +dependencies = [ + "serde", +] + [[package]] name = "serde_with" version = "1.10.0" diff --git a/daemon/Cargo.toml b/daemon/Cargo.toml index 82cdbfa..1a76d4b 100644 --- a/daemon/Cargo.toml +++ b/daemon/Cargo.toml @@ -23,6 +23,7 @@ rust_decimal = { version = "1.16", features = ["serde-float", "serde-arbitrary-p rust_decimal_macros = "1.16" serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_plain = "1" serde_with = { version = "1", features = ["macros"] } sha2 = "0.9" sqlx = { version = "0.5", features = ["offline"] } diff --git a/daemon/src/maker.rs b/daemon/src/maker.rs index 9eb5592..6f02804 100644 --- a/daemon/src/maker.rs +++ b/daemon/src/maker.rs @@ -209,8 +209,7 @@ async fn main() -> Result<()> { rocket::routes![ routes_maker::maker_feed, routes_maker::post_sell_order, - routes_maker::post_accept_order, - routes_maker::post_reject_order, + routes_maker::post_cfd_action, routes_maker::get_health_check ], ) diff --git a/daemon/src/model/cfd.rs b/daemon/src/model/cfd.rs index 147d50e..71264f5 100644 --- a/daemon/src/model/cfd.rs +++ b/daemon/src/model/cfd.rs @@ -6,6 +6,7 @@ use bdk::bitcoin::{Address, Amount, PublicKey, Transaction}; use bdk::descriptor::Descriptor; use cfd_protocol::secp256k1_zkp::{EcdsaAdaptorSignature, SECP256K1}; use cfd_protocol::{finalize_spend_transaction, spending_tx_sighash}; +use rocket::request::FromParam; use rust_decimal::Decimal; use rust_decimal_macros::dec; use serde::{Deserialize, Serialize}; @@ -29,6 +30,15 @@ impl Display for OrderId { } } +impl<'v> FromParam<'v> for OrderId { + type Error = uuid::Error; + + fn from_param(param: &'v str) -> Result { + let uuid = param.parse::()?; + Ok(OrderId(uuid)) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum Origin { Ours, @@ -293,7 +303,7 @@ impl Display for CfdState { write!(f, "Refunded") } CfdState::SetupFailed { .. } => { - write!(f, "Safely Aborted") + write!(f, "Setup Failed") } } } diff --git a/daemon/src/routes_maker.rs b/daemon/src/routes_maker.rs index 2fa2e26..246d865 100644 --- a/daemon/src/routes_maker.rs +++ b/daemon/src/routes_maker.rs @@ -2,7 +2,7 @@ use crate::auth::Authenticated; use crate::model::cfd::{Cfd, Order, OrderId, Origin}; use crate::model::{Usd, WalletInfo}; use crate::routes::EmbeddedFileExt; -use crate::to_sse_event::{CfdsWithCurrentPrice, ToSseEvent}; +use crate::to_sse_event::{CfdAction, CfdsWithCurrentPrice, ToSseEvent}; use crate::{bitmex_price_feed, maker_cfd}; use anyhow::Result; use rocket::http::{ContentType, Header, Status}; @@ -127,40 +127,33 @@ pub struct PromptAuthentication { /// The maker POSTs this to accept an order #[derive(Debug, Clone, Deserialize)] -pub struct AcceptOrRejectOrderRequest { +pub struct AcceptOrRejectTakeRequest { pub order_id: OrderId, } -#[rocket::post("/order/accept", data = "")] -pub async fn post_accept_order( - cfd_accept_order_request: Json, +#[rocket::post("/cfd//")] +pub async fn post_cfd_action( + id: OrderId, + action: CfdAction, cfd_actor_address: &State>, _auth: Authenticated, -) -> status::Accepted<()> { - cfd_actor_address - .do_send_async(maker_cfd::AcceptOrder { - order_id: cfd_accept_order_request.order_id, - }) - .await - .expect("actor to always be available"); - - status::Accepted(None) -} - -#[rocket::post("/order/reject", data = "")] -pub async fn post_reject_order( - cfd_reject_order_request: Json, - cfd_actor_address: &State>, - _auth: Authenticated, -) -> status::Accepted<()> { - cfd_actor_address - .do_send_async(maker_cfd::RejectOrder { - order_id: cfd_reject_order_request.order_id, - }) - .await - .expect("actor to always be available"); +) -> Result, status::BadRequest> { + match action { + CfdAction::Accept => { + cfd_actor_address + .do_send_async(maker_cfd::AcceptOrder { order_id: id }) + .await + .expect("actor to always be available"); + } + CfdAction::Reject => { + cfd_actor_address + .do_send_async(maker_cfd::RejectOrder { order_id: id }) + .await + .expect("actor to always be available"); + } + } - status::Accepted(None) + Ok(status::Accepted(None)) } #[rocket::get("/alive")] diff --git a/daemon/src/to_sse_event.rs b/daemon/src/to_sse_event.rs index efd6a58..a3da356 100644 --- a/daemon/src/to_sse_event.rs +++ b/daemon/src/to_sse_event.rs @@ -2,8 +2,9 @@ use crate::model::cfd::OrderId; use crate::model::{Leverage, Position, TradingPair, Usd}; use crate::{bitmex_price_feed, model}; use bdk::bitcoin::Amount; +use rocket::request::FromParam; use rocket::response::stream::Event; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::time::{SystemTime, UNIX_EPOCH}; #[derive(Debug, Clone, Serialize)] @@ -25,10 +26,42 @@ pub struct Cfd { pub profit_btc: Amount, pub profit_usd: Usd, - pub state: String, + pub state: CfdState, + pub actions: Vec, pub state_transition_timestamp: u64, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum CfdAction { + Accept, + Reject, +} + +impl<'v> FromParam<'v> for CfdAction { + type Error = serde_plain::Error; + + fn from_param(param: &'v str) -> Result { + let action = serde_plain::from_str(param)?; + Ok(action) + } +} + +#[derive(Debug, Clone, Serialize)] +pub enum CfdState { + OutgoingOrderRequest, + IncomingOrderRequest, + Accepted, + Rejected, + ContractSetup, + PendingOpen, + Open, + OpenCommitted, + MustRefund, + Refunded, + SetupFailed, +} + #[derive(Debug, Clone, Serialize)] pub struct CfdOrder { pub id: OrderId, @@ -79,7 +112,8 @@ impl ToSseEvent for CfdsWithCurrentPrice { quantity_usd: cfd.quantity_usd, profit_btc, profit_usd, - state: cfd.state.to_string(), + state: cfd.state.clone().into(), + actions: actions_for_state(cfd.state.clone()), state_transition_timestamp: cfd .state .get_transition_timestamp() @@ -141,6 +175,24 @@ impl ToSseEvent for model::WalletInfo { } } +impl From for CfdState { + fn from(cfd_state: model::cfd::CfdState) -> Self { + match cfd_state { + model::cfd::CfdState::OutgoingOrderRequest { .. } => CfdState::OutgoingOrderRequest, + model::cfd::CfdState::IncomingOrderRequest { .. } => CfdState::IncomingOrderRequest, + model::cfd::CfdState::Accepted { .. } => CfdState::Accepted, + model::cfd::CfdState::Rejected { .. } => CfdState::Rejected, + model::cfd::CfdState::ContractSetup { .. } => CfdState::ContractSetup, + model::cfd::CfdState::PendingOpen { .. } => CfdState::PendingOpen, + model::cfd::CfdState::Open { .. } => CfdState::Open, + model::cfd::CfdState::OpenCommitted { .. } => CfdState::OpenCommitted, + model::cfd::CfdState::MustRefund { .. } => CfdState::MustRefund, + model::cfd::CfdState::Refunded { .. } => CfdState::Refunded, + model::cfd::CfdState::SetupFailed { .. } => CfdState::SetupFailed, + } + } +} + #[derive(Debug, Clone, Serialize)] pub struct Quote { bid: Usd, @@ -165,3 +217,44 @@ fn into_unix_secs(time: SystemTime) -> u64 { .expect("timestamp to be convertible to duration since epoch") .as_secs() } + +fn actions_for_state(state: model::cfd::CfdState) -> Vec { + if let model::cfd::CfdState::IncomingOrderRequest { .. } = state { + vec![CfdAction::Accept, CfdAction::Reject] + } else { + vec![] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn state_snapshot_test() { + // Make sure to update the UI after changing this test! + + let json = serde_json::to_string(&CfdState::OutgoingOrderRequest).unwrap(); + assert_eq!(json, "\"OutgoingOrderRequest\""); + let json = serde_json::to_string(&CfdState::IncomingOrderRequest).unwrap(); + assert_eq!(json, "\"IncomingOrderRequest\""); + let json = serde_json::to_string(&CfdState::Accepted).unwrap(); + assert_eq!(json, "\"Accepted\""); + let json = serde_json::to_string(&CfdState::Rejected).unwrap(); + assert_eq!(json, "\"Rejected\""); + let json = serde_json::to_string(&CfdState::ContractSetup).unwrap(); + assert_eq!(json, "\"ContractSetup\""); + let json = serde_json::to_string(&CfdState::PendingOpen).unwrap(); + assert_eq!(json, "\"PendingOpen\""); + let json = serde_json::to_string(&CfdState::Open).unwrap(); + assert_eq!(json, "\"Open\""); + let json = serde_json::to_string(&CfdState::OpenCommitted).unwrap(); + assert_eq!(json, "\"OpenCommitted\""); + let json = serde_json::to_string(&CfdState::MustRefund).unwrap(); + assert_eq!(json, "\"MustRefund\""); + let json = serde_json::to_string(&CfdState::Refunded).unwrap(); + assert_eq!(json, "\"Refunded\""); + let json = serde_json::to_string(&CfdState::SetupFailed).unwrap(); + assert_eq!(json, "\"SetupFailed\""); + } +} diff --git a/frontend/src/MakerApp.tsx b/frontend/src/MakerApp.tsx index d9c253a..86186a9 100644 --- a/frontend/src/MakerApp.tsx +++ b/frontend/src/MakerApp.tsx @@ -19,24 +19,30 @@ import React, { useState } from "react"; import { useAsync } from "react-async"; import { useEventSource } from "react-sse-hooks"; import { CfdTable } from "./components/cfdtables/CfdTable"; -import { CfdTableMaker } from "./components/cfdtables/CfdTableMaker"; import CurrencyInputField from "./components/CurrencyInputField"; import CurrentPrice from "./components/CurrentPrice"; import useLatestEvent from "./components/Hooks"; import OrderTile from "./components/OrderTile"; -import { Cfd, Order, PriceInfo, WalletInfo } from "./components/Types"; +import { + Cfd, + intoCfd, + intoOrder, + Order, + Position, + PriceInfo, + State, + StateGroupKey, + WalletInfo, +} from "./components/Types"; import Wallet from "./components/Wallet"; import { CfdSellOrderPayload, postCfdSellOrderRequest } from "./MakerClient"; export default function App() { let source = useEventSource({ source: "/api/feed", options: { withCredentials: true } }); - const cfdsOrUndefined = useLatestEvent(source, "cfds"); + const cfdsOrUndefined = useLatestEvent(source, "cfds", intoCfd); let cfds = cfdsOrUndefined ? cfdsOrUndefined! : []; - const order = useLatestEvent(source, "order"); - - console.log(cfds); - + const order = useLatestEvent(source, "order", intoOrder); const walletInfo = useLatestEvent(source, "wallet"); const priceInfo = useLatestEvent(source, "quote"); @@ -66,16 +72,10 @@ export default function App() { }, }); - const runningStates = ["Accepted", "Contract Setup", "Pending Open"]; - const running = cfds.filter((value) => runningStates.includes(value.state)); - const openStates = ["Requested"]; - const open = cfds.filter((value) => openStates.includes(value.state)); - const closedStates = ["Rejected", "Closed"]; - const closed = cfds.filter((value) => closedStates.includes(value.state)); - // TODO: remove this. It just helps to detect immediately if we missed a state. - const unsorted = cfds.filter((value) => - !runningStates.includes(value.state) && !closedStates.includes(value.state) && !openStates.includes(value.state) - ); + const acceptOrReject = cfds.filter((value) => value.state.getGroup() === StateGroupKey.ACCEPT_OR_REJECT); + const opening = cfds.filter((value) => value.state.getGroup() === StateGroupKey.OPENING); + const open = cfds.filter((value) => value.state.getGroup() === StateGroupKey.OPEN); + const closed = cfds.filter((value) => value.state.getGroup() === StateGroupKey.CLOSED); return ( @@ -149,24 +149,24 @@ export default function App() { - Running [{running.length}] Open [{open.length}] + Accept / Reject [{acceptOrReject.length}] + Opening [{opening.length}] Closed [{closed.length}] - Unsorted [{unsorted.length}] (should be empty) - + - + - + - + diff --git a/frontend/src/MakerClient.tsx b/frontend/src/MakerClient.tsx index ff2e3ff..d98b6da 100644 --- a/frontend/src/MakerClient.tsx +++ b/frontend/src/MakerClient.tsx @@ -4,10 +4,6 @@ export interface CfdSellOrderPayload { max_quantity: number; } -export interface AcceptOrderRequestPayload { - order_id: string; -} - export async function postCfdSellOrderRequest(payload: CfdSellOrderPayload) { let res = await fetch(`/api/order/sell`, { method: "POST", @@ -23,17 +19,3 @@ export async function postCfdSellOrderRequest(payload: CfdSellOrderPayload) { throw new Error("failed to publish new order"); } } - -export async function postAcceptOrder(payload: AcceptOrderRequestPayload) { - let res = await fetch( - `/api/order/accept`, - { method: "POST", body: JSON.stringify(payload), credentials: "include" }, - ); -} - -export async function postRejectOrder(payload: AcceptOrderRequestPayload) { - let res = await fetch( - `/api/order/reject`, - { method: "POST", body: JSON.stringify(payload), credentials: "include" }, - ); -} diff --git a/frontend/src/TakerApp.tsx b/frontend/src/TakerApp.tsx index 21e90b3..63aee17 100644 --- a/frontend/src/TakerApp.tsx +++ b/frontend/src/TakerApp.tsx @@ -22,7 +22,7 @@ import { CfdTable } from "./components/cfdtables/CfdTable"; import CurrencyInputField from "./components/CurrencyInputField"; import CurrentPrice from "./components/CurrentPrice"; import useLatestEvent from "./components/Hooks"; -import { Cfd, Order, PriceInfo, WalletInfo } from "./components/Types"; +import { Cfd, intoCfd, intoOrder, Order, PriceInfo, StateGroupKey, WalletInfo } from "./components/Types"; import Wallet from "./components/Wallet"; interface CfdOrderRequestPayload { @@ -61,9 +61,9 @@ async function getMargin(payload: MarginRequestPayload): Promise export default function App() { let source = useEventSource({ source: "/api/feed" }); - const cfdsOrUndefined = useLatestEvent(source, "cfds"); + const cfdsOrUndefined = useLatestEvent(source, "cfds", intoCfd); let cfds = cfdsOrUndefined ? cfdsOrUndefined! : []; - const order = useLatestEvent(source, "order"); + const order = useLatestEvent(source, "order", intoOrder); const walletInfo = useLatestEvent(source, "wallet"); const priceInfo = useLatestEvent(source, "quote"); @@ -111,14 +111,9 @@ export default function App() { }, }); - const runningStates = ["Request sent", "Requested", "Contract Setup", "Pending Open"]; - const running = cfds.filter((value) => runningStates.includes(value.state)); - const closedStates = ["Rejected", "Closed"]; - const closed = cfds.filter((value) => closedStates.includes(value.state)); - // TODO: remove this. It just helps to detect immediately if we missed a state. - const unsorted = cfds.filter((value) => - !runningStates.includes(value.state) && !closedStates.includes(value.state) - ); + const opening = cfds.filter((value) => value.state.getGroup() === StateGroupKey.OPENING); + const open = cfds.filter((value) => value.state.getGroup() === StateGroupKey.OPEN); + const closed = cfds.filter((value) => value.state.getGroup() === StateGroupKey.CLOSED); return ( @@ -193,20 +188,20 @@ export default function App() { - Running [{running.length}] + Open [{open.length}] + Opening [{opening.length}] Closed [{closed.length}] - Unsorted [{unsorted.length}] (should be empty) - + - + - + diff --git a/frontend/src/components/Hooks.tsx b/frontend/src/components/Hooks.tsx index c4c3ae7..13ca6b3 100644 --- a/frontend/src/components/Hooks.tsx +++ b/frontend/src/components/Hooks.tsx @@ -1,7 +1,11 @@ import React, { useState } from "react"; -import { useEventSource, useEventSourceListener } from "react-sse-hooks"; +import { useEventSourceListener } from "react-sse-hooks"; -export default function useLatestEvent(source: EventSource, event_name: string): T | null { +export default function useLatestEvent( + source: EventSource, + event_name: string, + mapping: (key: string, value: any) => any = (key, value) => value, +): T | null { const [state, setState] = useState(null); useEventSourceListener( @@ -10,7 +14,10 @@ export default function useLatestEvent(source: EventSource, event_name: strin startOnInit: true, event: { name: event_name, - listener: ({ data }) => setState(data), + listener: ({ event }) => { + // @ts-ignore - yes, there is a data field on event + setState(JSON.parse(event.data, mapping)); + }, }, }, [source], diff --git a/frontend/src/components/Types.tsx b/frontend/src/components/Types.tsx index 59b2399..02b067a 100644 --- a/frontend/src/components/Types.tsx +++ b/frontend/src/components/Types.tsx @@ -1,7 +1,7 @@ export interface Order { id: string; trading_pair: string; - position: string; + position: Position; price: number; min_quantity: number; max_quantity: number; @@ -11,13 +11,31 @@ export interface Order { term_in_secs: number; } +export class Position { + constructor(public key: PositionKey) {} + + public getColorScheme(): string { + switch (this.key) { + case PositionKey.BUY: + return "green"; + case PositionKey.SELL: + return "red"; + } + } +} + +enum PositionKey { + BUY = "Buy", + SELL = "Sell", +} + export interface Cfd { order_id: string; initial_price: number; leverage: number; trading_pair: string; - position: string; + position: Position; liquidation_price: number; quantity_usd: number; @@ -27,10 +45,130 @@ export interface Cfd { profit_btc: number; profit_usd: number; - state: string; + state: State; + actions: Action[]; state_transition_timestamp: number; } +export class State { + constructor(public key: StateKey) {} + + public getLabel(): string { + switch (this.key) { + case StateKey.INCOMING_ORDER_REQUEST: + return "Take Requested"; + case StateKey.OUTGOING_ORDER_REQUEST: + return "Take Requested"; + case StateKey.ACCEPTED: + return "Accepted"; + case StateKey.REJECTED: + return "Rejected"; + case StateKey.CONTRACT_SETUP: + return "Contract Setup"; + case StateKey.PENDING_OPEN: + return "Pending Open"; + case StateKey.OPEN: + return "Open"; + case StateKey.OPEN_COMMITTED: + return "Open (commit-tx published)"; + case StateKey.MUST_REFUND: + return "Refunding"; + case StateKey.REFUNDED: + return "Refunded"; + case StateKey.SETUP_FAILED: + return "Setup Failed"; + } + } + + public getColorScheme(): string { + const default_color = "gray"; + const green = "green"; + const red = "red"; + const orange = "orange"; + + switch (this.key) { + case StateKey.OUTGOING_ORDER_REQUEST: + return default_color; + case StateKey.INCOMING_ORDER_REQUEST: + return default_color; + case StateKey.ACCEPTED: + return green; + case StateKey.REJECTED: + return red; + case StateKey.CONTRACT_SETUP: + return default_color; + case StateKey.PENDING_OPEN: + return default_color; + case StateKey.OPEN: + return green; + case StateKey.OPEN_COMMITTED: + return orange; + case StateKey.MUST_REFUND: + return orange; + case StateKey.REFUNDED: + return default_color; + case StateKey.SETUP_FAILED: + return default_color; + } + } + + public getGroup(): StateGroupKey { + switch (this.key) { + case StateKey.OUTGOING_ORDER_REQUEST: + return StateGroupKey.OPENING; + case StateKey.INCOMING_ORDER_REQUEST: + return StateGroupKey.ACCEPT_OR_REJECT; + case StateKey.ACCEPTED: + return StateGroupKey.OPENING; + case StateKey.REJECTED: + return StateGroupKey.CLOSED; + case StateKey.CONTRACT_SETUP: + return StateGroupKey.OPENING; + case StateKey.PENDING_OPEN: + return StateGroupKey.OPEN; + case StateKey.OPEN: + return StateGroupKey.OPEN; + case StateKey.OPEN_COMMITTED: + return StateGroupKey.OPEN; + case StateKey.MUST_REFUND: + return StateGroupKey.OPEN; + case StateKey.REFUNDED: + return StateGroupKey.CLOSED; + case StateKey.SETUP_FAILED: + return StateGroupKey.CLOSED; + } + } +} + +export enum Action { + ACCEPT = "accept", + REJECT = "reject", +} + +const enum StateKey { + OUTGOING_ORDER_REQUEST = "OutgoingOrderRequest", + INCOMING_ORDER_REQUEST = "IncomingOrderRequest", + ACCEPTED = "Accepted", + REJECTED = "Rejected", + CONTRACT_SETUP = "ContractSetup", + PENDING_OPEN = "PendingOpen", + OPEN = "Open", + OPEN_COMMITTED = "OpenCommitted", + MUST_REFUND = "MustRefund", + REFUNDED = "Refunded", + SETUP_FAILED = "SetupFailed", +} + +export enum StateGroupKey { + /// A CFD which is still being set up (not on chain yet) + OPENING = "Opening", + ACCEPT_OR_REJECT = "Accept / Reject", + /// A CFD that is an ongoing open position (on chain) + OPEN = "Open", + /// A CFD that has been successfully or not-successfully terminated + CLOSED = "Closed", +} + export interface WalletInfo { balance: number; address: string; @@ -46,3 +184,23 @@ export interface PriceInfo { export function unixTimestampToDate(unixTimestamp: number): Date { return new Date(unixTimestamp * 1000); } + +export function intoCfd(key: string, value: any): any { + switch (key) { + case "position": + return new Position(value); + case "state": + return new State(value); + default: + return value; + } +} + +export function intoOrder(key: string, value: any): any { + switch (key) { + case "position": + return new Position(value); + default: + return value; + } +} diff --git a/frontend/src/components/cfdtables/CfdTable.tsx b/frontend/src/components/cfdtables/CfdTable.tsx index 1a6c330..0d620a6 100644 --- a/frontend/src/components/cfdtables/CfdTable.tsx +++ b/frontend/src/components/cfdtables/CfdTable.tsx @@ -1,8 +1,29 @@ -import { ChevronRightIcon, ChevronUpIcon, TriangleDownIcon, TriangleUpIcon } from "@chakra-ui/icons"; -import { Badge, Box, chakra, HStack, IconButton, Table as CUITable, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react"; +import { + CheckIcon, + ChevronRightIcon, + ChevronUpIcon, + CloseIcon, + TriangleDownIcon, + TriangleUpIcon, +} from "@chakra-ui/icons"; +import { + Badge, + Box, + chakra, + HStack, + IconButton, + Table as CUITable, + Tbody, + Td, + Th, + Thead, + Tr, + useToast, +} from "@chakra-ui/react"; import React from "react"; +import { useAsync } from "react-async"; import { Column, Row, useExpanded, useSortBy, useTable } from "react-table"; -import { Cfd } from "../Types"; +import { Action, Cfd } from "../Types"; interface CfdTableProps { data: Cfd[]; @@ -11,6 +32,26 @@ interface CfdTableProps { export function CfdTable( { data }: CfdTableProps, ) { + const toast = useToast(); + + let { run: postAction, isLoading: isActioning } = useAsync({ + deferFn: async ([orderId, action]: any[]) => { + try { + await doPostAction(orderId, action); + } catch (e) { + const description = typeof e === "string" ? e : JSON.stringify(e); + + toast({ + title: "Error", + description, + status: "error", + duration: 9000, + isClosable: true, + }); + } + }, + }); + const tableData = React.useMemo( () => data, [data], @@ -48,12 +89,8 @@ export function CfdTable( { Header: "Position", accessor: ({ position }) => { - let colorScheme = "green"; - if (position.toLocaleLowerCase() === "buy") { - colorScheme = "purple"; - } return ( - {position} + {position.key} ); }, isNumeric: true, @@ -104,18 +141,28 @@ export function CfdTable( { Header: "State", accessor: ({ state }) => { - let colorScheme = "gray"; - if (state.toLowerCase() === "rejected") { - colorScheme = "red"; - } - if (state.toLowerCase() === "contract setup") { - colorScheme = "green"; - } return ( - {state} + {state.getLabel()} ); }, }, + { + Header: "Action", + accessor: ({ actions, order_id }) => { + const actionIcons = actions.map((action) => { + return ( postAction(order_id, action)} + isLoading={isActioning} + />); + }); + + return {actionIcons}; + }, + }, ], [], ); @@ -133,6 +180,24 @@ export function CfdTable( ); } +function iconForAction(action: Action): any { + switch (action) { + case Action.ACCEPT: + return ; + case Action.REJECT: + return ; + } +} + +function colorSchemaForAction(action: Action): string { + switch (action) { + case Action.ACCEPT: + return "green"; + case Action.REJECT: + return "red"; + } +} + function renderRowSubComponent(row: Row) { // TODO: I would show additional information here such as txids, timestamps, actions let cells = row.allCells @@ -270,3 +335,10 @@ export function Table({ columns, tableData, hiddenColumns, renderDetails }: Tabl ); } + +async function doPostAction(id: string, action: string) { + let res = await fetch( + `/api/cfd/${id}/${action}`, + { method: "POST", credentials: "include" }, + ); +} diff --git a/frontend/src/components/cfdtables/CfdTableMaker.tsx b/frontend/src/components/cfdtables/CfdTableMaker.tsx deleted file mode 100644 index a7951f0..0000000 --- a/frontend/src/components/cfdtables/CfdTableMaker.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { CheckIcon, ChevronRightIcon, ChevronUpIcon, CloseIcon } from "@chakra-ui/icons"; -import { Badge, Box, HStack, IconButton, useToast } from "@chakra-ui/react"; -import React from "react"; -import { useAsync } from "react-async"; -import { Column, Row } from "react-table"; -import { postAcceptOrder, postRejectOrder } from "../../MakerClient"; -import { Cfd } from "../Types"; -import { Table } from "./CfdTable"; - -interface CfdMableTakerProps { - data: Cfd[]; -} - -export function CfdTableMaker( - { data }: CfdMableTakerProps, -) { - const tableData = React.useMemo( - () => data, - [data], - ); - - const toast = useToast(); - - let { run: acceptOrder, isLoading: isAccepting } = useAsync({ - deferFn: async ([orderId]: any[]) => { - try { - let payload = { - order_id: orderId, - }; - await postAcceptOrder(payload); - } catch (e) { - const description = typeof e === "string" ? e : JSON.stringify(e); - - toast({ - title: "Error", - description, - status: "error", - duration: 9000, - isClosable: true, - }); - } - }, - }); - - let { run: rejectOrder, isLoading: isRejecting } = useAsync({ - deferFn: async ([orderId]: any[]) => { - try { - let payload = { - order_id: orderId, - }; - await postRejectOrder(payload); - } catch (e) { - const description = typeof e === "string" ? e : JSON.stringify(e); - - toast({ - title: "Error", - description, - status: "error", - duration: 9000, - isClosable: true, - }); - } - }, - }); - - const columns: Array> = React.useMemo( - () => [ - { - id: "expander", - Header: () => null, - Cell: ({ row }: any) => ( - - {row.isExpanded - ? } - onClick={() => { - row.toggleRowExpanded(); - }} - /> - : } - onClick={() => { - row.toggleRowExpanded(); - }} - />} - - ), - }, - { - Header: "OrderId", - accessor: "order_id", - }, - { - Header: "Position", - accessor: ({ position }) => { - let colorScheme = "green"; - if (position.toLocaleLowerCase() === "buy") { - colorScheme = "purple"; - } - return ( - {position} - ); - }, - isNumeric: true, - }, - - { - Header: "Quantity", - accessor: ({ quantity_usd }) => { - return (); - }, - isNumeric: true, - }, - { - Header: "Leverage", - accessor: "leverage", - isNumeric: true, - }, - { - Header: "Margin", - accessor: "margin", - isNumeric: true, - }, - { - Header: "Initial Price", - accessor: ({ initial_price }) => { - return (); - }, - isNumeric: true, - }, - { - Header: "Liquidation Price", - isNumeric: true, - accessor: ({ liquidation_price }) => { - return (); - }, - }, - { - Header: "Unrealized P/L", - accessor: ({ profit_usd }) => { - return (); - }, - isNumeric: true, - }, - { - Header: "Timestamp", - accessor: "state_transition_timestamp", - }, - { - Header: "Action", - accessor: ({ state, order_id }) => { - if (state.toLowerCase() === "requested") { - return ( - } - onClick={async () => acceptOrder(order_id)} - isLoading={isAccepting} - /> - } - onClick={async () => rejectOrder(order_id)} - isLoading={isRejecting} - /> - ); - } - - let colorScheme = "gray"; - if (state.toLowerCase() === "rejected") { - colorScheme = "red"; - } - if (state.toLowerCase() === "contract setup") { - colorScheme = "green"; - } - return ( - {state} - ); - }, - }, - ], - [], - ); - - // if we mark certain columns only as hidden, they are still around and we can render them in the sub-row - const hiddenColumns = ["order_id", "leverage", "Unrealized P/L", "state_transition_timestamp"]; - - return ( - - ); -} - -function renderRowSubComponent(row: Row) { - // TODO: I would show additional information here such as txids, timestamps, actions - let cells = row.allCells - .filter((cell) => { - return ["state_transition_timestamp"].includes(cell.column.id); - }) - .map((cell) => { - return cell; - }); - - return ( - <> - Showing some more information here... - - {cells.map(cell => ( - - {cell.column.id} = {cell.render("Cell")} - - ))} - - - ); -} - -interface DollarsProps { - amount: number; -} -function Dollars({ amount }: DollarsProps) { - const price = Math.floor(amount * 100.0) / 100.0; - return ( - <> - $ {price} - - ); -}