From 420bb25d07f0d3c6d77817fffc61c78bb3e47ecd Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Mon, 27 Sep 2021 07:53:44 +0200 Subject: [PATCH] implement post_qoute with fee as exists in current fee route --- orderbook/src/api.rs | 4 +- orderbook/src/api/get_fee_and_quote.rs | 102 +++++++------- orderbook/src/api/post_quote.rs | 177 +++++++++++++++++++++++-- 3 files changed, 222 insertions(+), 61 deletions(-) diff --git a/orderbook/src/api.rs b/orderbook/src/api.rs index 0919011b1..eca9b954e 100644 --- a/orderbook/src/api.rs +++ b/orderbook/src/api.rs @@ -47,9 +47,9 @@ pub fn handle_all_routes( let get_fee_and_quote_sell = get_fee_and_quote::get_fee_and_quote_sell(fee_calculator.clone(), price_estimator.clone()); let get_fee_and_quote_buy = - get_fee_and_quote::get_fee_and_quote_buy(fee_calculator, price_estimator.clone()); + get_fee_and_quote::get_fee_and_quote_buy(fee_calculator.clone(), price_estimator.clone()); let get_user_orders = get_user_orders::get_user_orders(orderbook); - let post_quote = post_quote::post_quote(); + let post_quote = post_quote::post_quote(fee_calculator, price_estimator.clone()); let cors = warp::cors() .allow_any_origin() .allow_methods(vec!["GET", "POST", "DELETE", "OPTIONS", "PUT", "PATCH"]) diff --git a/orderbook/src/api/get_fee_and_quote.rs b/orderbook/src/api/get_fee_and_quote.rs index 54977f6fb..0590a56eb 100644 --- a/orderbook/src/api/get_fee_and_quote.rs +++ b/orderbook/src/api/get_fee_and_quote.rs @@ -8,63 +8,64 @@ use serde::{Deserialize, Serialize}; use shared::price_estimation::{self, PriceEstimating, PriceEstimationError}; use std::convert::Infallible; use std::sync::Arc; +use warp::reply::Json; use warp::{hyper::StatusCode, reply, Filter, Rejection, Reply}; #[derive(Serialize)] #[serde(rename_all = "camelCase")] -struct Fee { +pub struct Fee { #[serde(with = "u256_decimal")] - amount: U256, - expiration_date: DateTime, + pub amount: U256, + pub expiration_date: DateTime, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -struct SellQuery { +pub struct SellQuery { #[serde(with = "h160_hexadecimal")] - sell_token: H160, + pub sell_token: H160, #[serde(with = "h160_hexadecimal")] - buy_token: H160, + pub buy_token: H160, // The total amount to be sold from which the fee will be deducted. #[serde(with = "u256_decimal")] - sell_amount_before_fee: U256, + pub sell_amount_before_fee: U256, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] -struct SellResponse { +pub struct SellResponse { // The fee that is deducted from sell_amount_before_fee. The sell amount that is traded is // sell_amount_before_fee - fee_in_sell_token. - fee: Fee, + pub fee: Fee, // The expected buy amount for the traded sell amount. #[serde(with = "u256_decimal")] - buy_amount_after_fee: U256, + pub buy_amount_after_fee: U256, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -struct BuyQuery { +pub struct BuyQuery { #[serde(with = "h160_hexadecimal")] - sell_token: H160, + pub sell_token: H160, #[serde(with = "h160_hexadecimal")] - buy_token: H160, + pub buy_token: H160, // The total amount to be bought. #[serde(with = "u256_decimal")] - buy_amount_after_fee: U256, + pub buy_amount_after_fee: U256, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] -struct BuyResponse { +pub struct BuyResponse { // The fee that is deducted from sell_amount_before_fee. The sell amount that is traded is // sell_amount_before_fee - fee_in_sell_token. - fee: Fee, + pub fee: Fee, #[serde(with = "u256_decimal")] - sell_amount_before_fee: U256, + pub sell_amount_before_fee: U256, } #[derive(Debug)] -enum Error { +pub enum Error { NoLiquidity, UnsupportedToken(H160), AmountIsZero, @@ -72,6 +73,39 @@ enum Error { Other(anyhow::Error), } +impl Error { + pub fn convert_to_reply(self) -> (Json, StatusCode) { + match self { + Error::NoLiquidity => ( + super::error("NoLiquidity", "not enough liquidity"), + StatusCode::NOT_FOUND, + ), + Error::UnsupportedToken(token) => ( + super::error("UnsupportedToken", format!("Token address {:?}", token)), + StatusCode::BAD_REQUEST, + ), + Error::AmountIsZero => ( + super::error( + "AmountIsZero", + "The input amount must be greater than zero.".to_string(), + ), + StatusCode::BAD_REQUEST, + ), + Error::SellAmountDoesNotCoverFee => ( + super::error( + "SellAmountDoesNotCoverFee", + "The sell amount for the sell order is lower than the fee.".to_string(), + ), + StatusCode::BAD_REQUEST, + ), + Error::Other(err) => { + tracing::error!(?err, "get_fee_and_price error"); + (super::internal_error(), StatusCode::INTERNAL_SERVER_ERROR) + } + } + } +} + impl From for Error { fn from(other: PriceEstimationError) -> Self { match other { @@ -82,7 +116,7 @@ impl From for Error { } } -async fn calculate_sell( +pub async fn calculate_sell( fee_calculator: Arc, price_estimator: Arc, query: SellQuery, @@ -124,7 +158,7 @@ async fn calculate_sell( }) } -async fn calculate_buy( +pub async fn calculate_buy( fee_calculator: Arc, price_estimator: Arc, query: BuyQuery, @@ -179,31 +213,9 @@ fn buy_request() -> impl Filter + Clon fn response(result: Result) -> impl Reply { match result { Ok(response) => reply::with_status(reply::json(&response), StatusCode::OK), - Err(Error::NoLiquidity) => reply::with_status( - super::error("NoLiquidity", "not enough liquidity"), - StatusCode::NOT_FOUND, - ), - Err(Error::UnsupportedToken(token)) => reply::with_status( - super::error("UnsupportedToken", format!("Token address {:?}", token)), - StatusCode::BAD_REQUEST, - ), - Err(Error::AmountIsZero) => reply::with_status( - super::error( - "AmountIsZero", - "The input amount must be greater than zero.".to_string(), - ), - StatusCode::BAD_REQUEST, - ), - Err(Error::SellAmountDoesNotCoverFee) => reply::with_status( - super::error( - "SellAmountDoesNotCoverFee", - "The sell amount for the sell order is lower than the fee.".to_string(), - ), - StatusCode::BAD_REQUEST, - ), - Err(Error::Other(err)) => { - tracing::error!(?err, "get_fee_and_price error"); - reply::with_status(super::internal_error(), StatusCode::INTERNAL_SERVER_ERROR) + Err(error) => { + let (reply, status) = error.convert_to_reply(); + reply::with_status(reply, status) } } } diff --git a/orderbook/src/api/post_quote.rs b/orderbook/src/api/post_quote.rs index 8b8841def..8c32b7804 100644 --- a/orderbook/src/api/post_quote.rs +++ b/orderbook/src/api/post_quote.rs @@ -1,4 +1,9 @@ use crate::api; +use crate::api::get_fee_and_quote::{ + calculate_buy, calculate_sell, BuyQuery, BuyResponse, Error as FeeAndQuoteError, SellQuery, + SellResponse, +}; +use crate::fee::MinFeeCalculating; use anyhow::{anyhow, Result}; use ethcontract::{H160, U256}; use model::{ @@ -7,11 +12,13 @@ use model::{ u256_decimal, }; use serde::{Deserialize, Serialize}; +use shared::price_estimation::PriceEstimating; use std::convert::Infallible; +use std::sync::Arc; use warp::{hyper::StatusCode, reply, Filter, Rejection, Reply}; /// The order parameters to quote a price and fee for. -#[derive(Debug, Deserialize, PartialEq)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] struct OrderQuoteRequest { from: H160, @@ -30,7 +37,7 @@ struct OrderQuoteRequest { buy_token_balance: BuyTokenDestination, } -#[derive(Debug, Deserialize, PartialEq)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] #[serde(tag = "kind", rename_all = "lowercase")] enum OrderQuoteSide { #[serde(rename_all = "camelCase")] @@ -45,7 +52,7 @@ enum OrderQuoteSide { }, } -#[derive(Debug, Deserialize, PartialEq)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] #[serde(untagged)] enum SellAmount { BeforeFee { @@ -61,7 +68,7 @@ enum SellAmount { /// The quoted order by the service. #[derive(Serialize)] #[serde(rename_all = "camelCase")] -struct OrderQuote { +pub struct OrderQuote { from: H160, sell_token: H160, buy_token: H160, @@ -81,26 +88,168 @@ struct OrderQuote { buy_token_balance: BuyTokenDestination, } +impl OrderQuoteRequest { + fn build_quote_from_sell_response( + &self, + fee_response: SellResponse, + ) -> Result { + match self.side { + OrderQuoteSide::Sell { sell_amount } => { + // TODO - make sure the sell_amount field in OrderCreation is correct. + let sell_amount = match sell_amount { + SellAmount::BeforeFee { + value: sell_amount_before_fee, + } => sell_amount_before_fee, + SellAmount::AfterFee { + value: sell_amount_after_fee, + } => sell_amount_after_fee + .checked_sub(fee_response.fee.amount) + .ok_or(FeeAndQuoteError::SellAmountDoesNotCoverFee)? + .max(U256::one()), + }; + Ok(OrderQuote { + from: self.from, + sell_token: self.sell_token, + buy_token: self.buy_token, + receiver: self.receiver, + sell_amount, // Still need to figure out if this is before or after fee here. + buy_amount: fee_response.buy_amount_after_fee, + valid_to: self.valid_to, + app_data: self.app_data, + fee_amount: fee_response.fee.amount, + kind: match self.side { + OrderQuoteSide::Sell { .. } => OrderKind::Sell, + OrderQuoteSide::Buy { .. } => OrderKind::Buy, + }, + partially_fillable: self.partially_fillable, + sell_token_balance: self.sell_token_balance, + buy_token_balance: self.buy_token_balance, + }) + } + OrderQuoteSide::Buy { .. } => Err(FeeAndQuoteError::Other(anyhow!( + "Can't build Quote for Buy Request with SellResponse" + ))), + } + } + + fn build_quote_from_buy_response( + &self, + fee_response: BuyResponse, + ) -> Result { + match self.side { + OrderQuoteSide::Sell { .. } => Err(FeeAndQuoteError::Other(anyhow!( + "Can't build Quote for Sell Request with BuyResponse" + ))), + OrderQuoteSide::Buy { + buy_amount_after_fee, + } => { + Ok(OrderQuote { + from: self.from, + sell_token: self.sell_token, + buy_token: self.buy_token, + receiver: self.receiver, + sell_amount: fee_response.sell_amount_before_fee, // Still need to figure out if this is before or after fee here. + buy_amount: buy_amount_after_fee, + valid_to: self.valid_to, + app_data: self.app_data, + fee_amount: fee_response.fee.amount, + kind: match self.side { + OrderQuoteSide::Sell { .. } => OrderKind::Sell, + OrderQuoteSide::Buy { .. } => OrderKind::Buy, + }, + partially_fillable: self.partially_fillable, + sell_token_balance: self.sell_token_balance, + buy_token_balance: self.buy_token_balance, + }) + } + } + } +} + +impl OrderQuoteRequest { + pub async fn try_into_order_quote_result( + &self, + fee_calculator: Arc, + price_estimator: Arc, + ) -> Result { + tracing::debug!("Received quote request {:?}", self); + // TODO - use additional info to construct a more appropriate fee. + // For now, we simply write mimic the existing fee route + let sell_token = self.sell_token; + let buy_token = self.buy_token; + match self.side { + OrderQuoteSide::Sell { sell_amount } => match sell_amount { + SellAmount::BeforeFee { + value: sell_amount_before_fee, + } => { + let fee_response = calculate_sell( + fee_calculator, + price_estimator, + SellQuery { + sell_token, + buy_token, + sell_amount_before_fee, + }, + ) + .await?; + self.build_quote_from_sell_response(fee_response) + } + SellAmount::AfterFee { .. } => { + tracing::warn!("Unimplemented Route as of yet"); + Err(FeeAndQuoteError::Other(anyhow!("Not yet implemented"))) + } + }, + OrderQuoteSide::Buy { + buy_amount_after_fee, + } => { + let fee_response = calculate_buy( + fee_calculator, + price_estimator, + BuyQuery { + sell_token, + buy_token, + buy_amount_after_fee, + }, + ) + .await?; + self.build_quote_from_buy_response(fee_response) + } + } + } +} + fn post_quote_request() -> impl Filter + Clone { warp::path!("quote") .and(warp::post()) .and(api::extract_payload()) } -fn post_order_response(result: Result) -> impl Reply { +fn post_order_response(result: Result) -> impl Reply { match result { - Ok(response) => reply::with_status(reply::json(&response), StatusCode::OK), - Err(err) => reply::with_status( - super::error("InternalServerError", err.to_string()), - StatusCode::INTERNAL_SERVER_ERROR, - ), + Ok(quote) => reply::with_status(reply::json("e), StatusCode::OK), + Err(err) => { + let (reply, status) = err.convert_to_reply(); + reply::with_status(reply, status) + } } } -pub fn post_quote() -> impl Filter + Clone { - post_quote_request().and_then(move |request| async move { - tracing::warn!("unimplemented request {:#?}", request); - Result::<_, Infallible>::Ok(post_order_response(Err(anyhow!("not yet implemented")))) +pub fn post_quote( + fee_calculator: Arc, + price_estimator: Arc, +) -> impl Filter + Clone { + post_quote_request().and_then(move |request: OrderQuoteRequest| { + let fee_calculator = fee_calculator.clone(); + let price_estimator = price_estimator.clone(); + async move { + let result = request + .try_into_order_quote_result(fee_calculator, price_estimator) + .await; + if let Err(err) = &result { + tracing::error!(?err, ?request, "post_quote error"); + } + Result::<_, Infallible>::Ok(post_order_response(result)) + } }) }