Browse Source

Merge #472 #474

472: Initialise the `RELEASE_BRANCH` envvar to ensure drafting release PR works r=da-kami a=da-kami

Thanks to `@scratchscratchscratchy` for pointing this out!

474: Provide UI feedback for failed actions in the taker r=klochowicz a=klochowicz

Also:
- improve the handling on the maker side.
- populate errors created within CfdTable

Co-authored-by: Daniel Karzel <daniel@comit.network>
Co-authored-by: Mariusz Klochowicz <mariusz@klochowicz.com>
fix/sql-oddness
bors[bot] 3 years ago
committed by GitHub
parent
commit
83e09f6287
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .github/workflows/draft-new-release.yml
  2. 13
      daemon/src/routes_maker.rs
  3. 53
      daemon/src/routes_taker.rs
  4. 12
      daemon/src/taker_cfd.rs
  5. 28
      frontend/src/TakerApp.tsx
  6. 18
      frontend/src/components/cfdtables/CfdTable.tsx

4
.github/workflows/draft-new-release.yml

@ -11,13 +11,15 @@ jobs:
draft-new-release: draft-new-release:
name: "Draft a new release" name: "Draft a new release"
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
RELEASE_BRANCH: release/${{ github.event.inputs.version }}
steps: steps:
- uses: actions/checkout@v2.4.0 - uses: actions/checkout@v2.4.0
with: with:
token: ${{ secrets.BOTTY_GITHUB_TOKEN }} token: ${{ secrets.BOTTY_GITHUB_TOKEN }}
- name: Create release branch - name: Create release branch
run: git checkout -b release/${{ github.event.inputs.version }} run: git checkout -b ${{ env.RELEASE_BRANCH }}
- name: Initialize mandatory git config - name: Initialize mandatory git config
run: | run: |

13
daemon/src/routes_maker.rs

@ -122,11 +122,11 @@ pub async fn post_sell_order(
max_quantity: order.max_quantity, max_quantity: order.max_quantity,
}) })
.await .await
.unwrap_or_else(|_| anyhow::bail!("actor disconnected")) // TODO: is there a better way? .unwrap_or_else(|e| anyhow::bail!(e))
.map_err(|_| { .map_err(|e| {
HttpApiProblem::new(StatusCode::INTERNAL_SERVER_ERROR) HttpApiProblem::new(StatusCode::INTERNAL_SERVER_ERROR)
.title("Action failed") .title("Posting offer failed")
.detail("failed to post a sell order") .detail(e.to_string())
})?; })?;
Ok(status::Accepted(None)) Ok(status::Accepted(None))
@ -179,10 +179,11 @@ pub async fn post_cfd_action(
result result
.await .await
.unwrap_or_else(|_| anyhow::bail!("actor disconnected")) // TODO: is there a better way? .unwrap_or_else(|e| anyhow::bail!(e))
.map_err(|_| { .map_err(|e| {
HttpApiProblem::new(StatusCode::INTERNAL_SERVER_ERROR) HttpApiProblem::new(StatusCode::INTERNAL_SERVER_ERROR)
.title(action.to_string() + " failed") .title(action.to_string() + " failed")
.detail(e.to_string())
})?; })?;
Ok(status::Accepted(None)) Ok(status::Accepted(None))

53
daemon/src/routes_taker.rs

@ -4,6 +4,7 @@ use daemon::model::{Leverage, Price, Usd, WalletInfo};
use daemon::routes::EmbeddedFileExt; use daemon::routes::EmbeddedFileExt;
use daemon::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent}; use daemon::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent};
use daemon::{bitmex_price_feed, taker_cfd}; use daemon::{bitmex_price_feed, taker_cfd};
use http_api_problem::{HttpApiProblem, StatusCode};
use rocket::http::{ContentType, Status}; use rocket::http::{ContentType, Status};
use rocket::response::stream::EventStream; use rocket::response::stream::EventStream;
use rocket::response::{status, Responder}; use rocket::response::{status, Responder};
@ -15,7 +16,7 @@ use std::borrow::Cow;
use std::path::PathBuf; use std::path::PathBuf;
use tokio::select; use tokio::select;
use tokio::sync::watch; use tokio::sync::watch;
use xtra::prelude::MessageChannel; use xtra::prelude::*;
#[rocket::get("/feed")] #[rocket::get("/feed")]
pub async fn feed( pub async fn feed(
@ -105,13 +106,21 @@ pub struct CfdOrderRequest {
pub async fn post_order_request( pub async fn post_order_request(
cfd_order_request: Json<CfdOrderRequest>, cfd_order_request: Json<CfdOrderRequest>,
take_offer_channel: &State<Box<dyn MessageChannel<taker_cfd::TakeOffer>>>, take_offer_channel: &State<Box<dyn MessageChannel<taker_cfd::TakeOffer>>>,
) { ) -> Result<status::Accepted<()>, HttpApiProblem> {
take_offer_channel take_offer_channel
.do_send(taker_cfd::TakeOffer { .send(taker_cfd::TakeOffer {
order_id: cfd_order_request.order_id, order_id: cfd_order_request.order_id,
quantity: cfd_order_request.quantity, quantity: cfd_order_request.quantity,
}) })
.expect("actor to always be available"); .await
.unwrap_or_else(|e| anyhow::bail!(e.to_string()))
.map_err(|e| {
HttpApiProblem::new(StatusCode::INTERNAL_SERVER_ERROR)
.title("Order request failed")
.detail(e.to_string())
})?;
Ok(status::Accepted(None))
} }
#[rocket::post("/cfd/<id>/<action>")] #[rocket::post("/cfd/<id>/<action>")]
@ -120,37 +129,37 @@ pub async fn post_cfd_action(
action: CfdAction, action: CfdAction,
cfd_action_channel: &State<Box<dyn MessageChannel<taker_cfd::CfdAction>>>, cfd_action_channel: &State<Box<dyn MessageChannel<taker_cfd::CfdAction>>>,
quote_updates: &State<watch::Receiver<bitmex_price_feed::Quote>>, quote_updates: &State<watch::Receiver<bitmex_price_feed::Quote>>,
) -> Result<status::Accepted<()>, status::BadRequest<String>> { ) -> Result<status::Accepted<()>, HttpApiProblem> {
use taker_cfd::CfdAction::*; use taker_cfd::CfdAction::*;
match action { let result = match action {
CfdAction::AcceptOrder CfdAction::AcceptOrder
| CfdAction::RejectOrder | CfdAction::RejectOrder
| CfdAction::AcceptSettlement | CfdAction::AcceptSettlement
| CfdAction::RejectSettlement | CfdAction::RejectSettlement
| CfdAction::AcceptRollOver | CfdAction::AcceptRollOver
| CfdAction::RejectRollOver => { | CfdAction::RejectRollOver => {
return Err(status::BadRequest(None)); return Err(HttpApiProblem::new(StatusCode::BAD_REQUEST)
} .detail(format!("taker cannot invoke action {}", action)));
CfdAction::Commit => {
cfd_action_channel
.do_send(Commit { order_id: id })
.map_err(|e| status::BadRequest(Some(e.to_string())))?;
} }
CfdAction::Commit => cfd_action_channel.send(Commit { order_id: id }),
CfdAction::Settle => { CfdAction::Settle => {
let current_price = quote_updates.borrow().for_taker(); let current_price = quote_updates.borrow().for_taker();
cfd_action_channel cfd_action_channel.send(ProposeSettlement {
.do_send(ProposeSettlement {
order_id: id, order_id: id,
current_price, current_price,
}) })
.expect("actor to always be available");
}
CfdAction::RollOver => {
cfd_action_channel
.do_send(ProposeRollOver { order_id: id })
.expect("actor to always be available");
}
} }
CfdAction::RollOver => cfd_action_channel.send(ProposeRollOver { order_id: id }),
};
result
.await
.unwrap_or_else(|e| anyhow::bail!(e.to_string()))
.map_err(|e| {
HttpApiProblem::new(StatusCode::INTERNAL_SERVER_ERROR)
.title(action.to_string() + " failed")
.detail(e.to_string())
})?;
Ok(status::Accepted(None)) Ok(status::Accepted(None))
} }
@ -177,7 +186,7 @@ pub struct MarginResponse {
#[rocket::post("/calculate/margin", data = "<margin_request>")] #[rocket::post("/calculate/margin", data = "<margin_request>")]
pub fn margin_calc( pub fn margin_calc(
margin_request: Json<MarginRequest>, margin_request: Json<MarginRequest>,
) -> Result<status::Accepted<Json<MarginResponse>>, status::BadRequest<String>> { ) -> Result<status::Accepted<Json<MarginResponse>>, HttpApiProblem> {
let margin = calculate_long_margin( let margin = calculate_long_margin(
margin_request.price, margin_request.price,
margin_request.quantity, margin_request.quantity,

12
daemon/src/taker_cfd.rs

@ -652,8 +652,8 @@ where
#[async_trait] #[async_trait]
impl<O: 'static, M: 'static, W: 'static> Handler<TakeOffer> for Actor<O, M, W> { impl<O: 'static, M: 'static, W: 'static> Handler<TakeOffer> for Actor<O, M, W> {
async fn handle(&mut self, msg: TakeOffer, _ctx: &mut Context<Self>) { async fn handle(&mut self, msg: TakeOffer, _ctx: &mut Context<Self>) -> Result<()> {
log_error!(self.handle_take_offer(msg.order_id, msg.quantity)); self.handle_take_offer(msg.order_id, msg.quantity).await
} }
} }
@ -664,7 +664,7 @@ where
+ xtra::Handler<wallet::Sign> + xtra::Handler<wallet::Sign>
+ xtra::Handler<wallet::BuildPartyParams>, + xtra::Handler<wallet::BuildPartyParams>,
{ {
async fn handle(&mut self, msg: CfdAction, _ctx: &mut Context<Self>) { async fn handle(&mut self, msg: CfdAction, _ctx: &mut Context<Self>) -> Result<()> {
use CfdAction::*; use CfdAction::*;
if let Err(e) = match msg { if let Err(e) = match msg {
@ -679,7 +679,9 @@ where
ProposeRollOver { order_id } => self.handle_propose_roll_over(order_id).await, ProposeRollOver { order_id } => self.handle_propose_roll_over(order_id).await,
} { } {
tracing::error!("Message handler failed: {:#}", e); tracing::error!("Message handler failed: {:#}", e);
anyhow::bail!(e)
} }
Ok(())
} }
} }
@ -789,11 +791,11 @@ where
} }
impl Message for TakeOffer { impl Message for TakeOffer {
type Result = (); type Result = Result<()>;
} }
impl Message for CfdAction { impl Message for CfdAction {
type Result = (); type Result = Result<()>;
} }
// this signature is a bit different because we use `Address::attach_stream` // this signature is a bit different because we use `Address::attach_stream`

28
frontend/src/TakerApp.tsx

@ -20,7 +20,9 @@ import { useAsync } from "react-async";
import { useEventSource } from "react-sse-hooks"; import { useEventSource } from "react-sse-hooks";
import { CfdTable } from "./components/cfdtables/CfdTable"; import { CfdTable } from "./components/cfdtables/CfdTable";
import CurrencyInputField from "./components/CurrencyInputField"; import CurrencyInputField from "./components/CurrencyInputField";
import createErrorToast from "./components/ErrorToast";
import useLatestEvent from "./components/Hooks"; import useLatestEvent from "./components/Hooks";
import { HttpError } from "./components/HttpError";
import { Cfd, intoCfd, intoOrder, Order, StateGroupKey, WalletInfo } from "./components/Types"; import { Cfd, intoCfd, intoOrder, Order, StateGroupKey, WalletInfo } from "./components/Types";
import Wallet from "./components/Wallet"; import Wallet from "./components/Wallet";
@ -43,7 +45,8 @@ async function postCfdOrderRequest(payload: CfdOrderRequestPayload) {
let res = await fetch(`/api/cfd/order`, { method: "POST", body: JSON.stringify(payload) }); let res = await fetch(`/api/cfd/order`, { method: "POST", body: JSON.stringify(payload) });
if (!res.status.toString().startsWith("2")) { if (!res.status.toString().startsWith("2")) {
throw new Error("failed to create new CFD order request: " + res.status + ", " + res.statusText); const resp = await res.json();
throw new HttpError(resp);
} }
} }
@ -51,7 +54,8 @@ async function getMargin(payload: MarginRequestPayload): Promise<MarginResponse>
let res = await fetch(`/api/calculate/margin`, { method: "POST", body: JSON.stringify(payload) }); let res = await fetch(`/api/calculate/margin`, { method: "POST", body: JSON.stringify(payload) });
if (!res.status.toString().startsWith("2")) { if (!res.status.toString().startsWith("2")) {
throw new Error("failed to create new CFD order request: " + res.status + ", " + res.statusText); const resp = await res.json();
throw new HttpError(resp);
} }
return res.json(); return res.json();
@ -81,15 +85,7 @@ export default function App() {
let res = await getMargin(payload as MarginRequestPayload); let res = await getMargin(payload as MarginRequestPayload);
setMargin(res.margin.toString()); setMargin(res.margin.toString());
} catch (e) { } catch (e) {
const description = typeof e === "string" ? e : JSON.stringify(e); createErrorToast(toast, e);
toast({
title: "Error",
description,
status: "error",
duration: 9000,
isClosable: true,
});
} }
}, },
}); });
@ -119,15 +115,7 @@ export default function App() {
try { try {
await postCfdOrderRequest(payload as CfdOrderRequestPayload); await postCfdOrderRequest(payload as CfdOrderRequestPayload);
} catch (e) { } catch (e) {
const description = typeof e === "string" ? e : JSON.stringify(e); createErrorToast(toast, e);
toast({
title: "Error",
description,
status: "error",
duration: 9000,
isClosable: true,
});
} }
}, },
}); });

18
frontend/src/components/cfdtables/CfdTable.tsx

@ -31,6 +31,8 @@ import {
import React from "react"; import React from "react";
import { useAsync } from "react-async"; import { useAsync } from "react-async";
import { Column, Row, useExpanded, useSortBy, useTable } from "react-table"; import { Column, Row, useExpanded, useSortBy, useTable } from "react-table";
import createErrorToast from "../ErrorToast";
import { HttpError } from "../HttpError";
import Timestamp from "../Timestamp"; import Timestamp from "../Timestamp";
import { Action, Cfd } from "../Types"; import { Action, Cfd } from "../Types";
@ -48,15 +50,7 @@ export function CfdTable(
try { try {
await doPostAction(orderId, action); await doPostAction(orderId, action);
} catch (e) { } catch (e) {
const description = typeof e === "string" ? e : JSON.stringify(e); createErrorToast(toast, e);
toast({
title: "Error",
description,
status: "error",
duration: 9000,
isClosable: true,
});
} }
}, },
}); });
@ -400,8 +394,12 @@ export function Table({ columns, tableData, hiddenColumns, renderDetails }: Tabl
} }
async function doPostAction(id: string, action: string) { async function doPostAction(id: string, action: string) {
await fetch( let res = await fetch(
`/api/cfd/${id}/${action}`, `/api/cfd/${id}/${action}`,
{ method: "POST", credentials: "include" }, { method: "POST", credentials: "include" },
); );
if (!res.status.toString().startsWith("2")) {
const resp = await res.json();
throw new HttpError(resp);
}
} }

Loading…
Cancel
Save