Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

utoipa proof of concept on orderbook endpoint #2338

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ thiserror = "1"
tokio = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
utoipa = {version = "4.2.0", features = ["preserve_order", "yaml"]}
url = "2"
warp = { git = 'https://github.com/cowprotocol/warp.git', rev = "87a91e2", default-features = false }
web3 = { version = "0.19", default-features = false }
6 changes: 6 additions & 0 deletions crates/orderbook/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ doctest = false
name = "orderbook"
path = "src/main.rs"

[[bin]]
name = "openapi"
path = "src/openapi.rs"

[dependencies]
anyhow = { workspace = true }
app-data-hash = { path = "../app-data-hash" }
Expand Down Expand Up @@ -47,10 +51,12 @@ serde_json = { workspace = true }
serde_with = { workspace = true }
shared = { path = "../shared" }
sqlx = { workspace = true }
strum = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal", "sync", "time"] }
tracing = { workspace = true }
url = { workspace = true }
utoipa = { workspace = true }
warp = { workspace = true }
web3 = { workspace = true }

Expand Down
18 changes: 17 additions & 1 deletion crates/orderbook/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
use {
crate::{app_data, database::Postgres, orderbook::Orderbook, quoter::QuoteHandler},
self::model::common::Error,
crate::{
api::model::order::{OrderCreation, OrderKind, OrderUid},
app_data,
database::Postgres,
orderbook::Orderbook,
quoter::QuoteHandler,
},
shared::{
api::{box_filter, error, finalize_router, ApiReply},
price_estimation::native::NativePriceEstimating,
},
std::sync::Arc,
utoipa::OpenApi,
warp::{Filter, Rejection, Reply},
};

Expand All @@ -19,6 +27,7 @@ mod get_solver_competition;
mod get_total_surplus;
mod get_trades;
mod get_user_orders;
mod model;
mod post_order;
mod post_quote;
mod put_app_data;
Expand Down Expand Up @@ -105,3 +114,10 @@ pub fn handle_all_routes(

finalize_router(routes, "orderbook::api::request_summary")
}

#[derive(OpenApi)]
#[openapi(
paths(post_order::post_order),
components(schemas(OrderCreation, OrderKind, OrderUid, Error))
)]
pub struct ApiDoc;
2 changes: 2 additions & 0 deletions crates/orderbook/src/api/model.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod common;
pub mod order;
13 changes: 13 additions & 0 deletions crates/orderbook/src/api/model/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use {
serde::{Deserialize, Serialize},
utoipa::ToSchema,
};

#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct Error {
#[schema(example = "DuplicatedOrder")]
error_type: String,
#[schema(example = "string")]
description: String,
}
167 changes: 167 additions & 0 deletions crates/orderbook/src/api/model/order.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
use {
model::{
app_data::AppDataHash,
bytes_hex,
order::{BuyTokenDestination, OrderCreationAppData, SellTokenSource},
quote::QuoteId,
signature::{Signature, SigningScheme},
},
number::serialization::HexOrDecimalU256,
primitive_types::{H160, U256},
serde::{Deserialize, Serialize, Serializer},
serde_with::serde_as,
std::fmt::{self, Display},
strum::EnumString,
utoipa::{ToResponse, ToSchema},
};

#[derive(
Eq, PartialEq, Clone, Copy, Debug, Default, Deserialize, Serialize, Hash, EnumString, ToSchema,
)]
#[strum(ascii_case_insensitive)]
#[serde(rename_all = "lowercase")]
pub enum OrderKind {
#[default]
Buy,
Sell,
}

// uid as 56 bytes: 32 for orderDigest, 20 for ownerAddress and 4 for validTo
#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord, ToSchema, ToResponse)]
#[schema(example = json!("0xff2e2e54d178997f173266817c1e9ed6fee1a1aae4b43971c53b543cffcc2969845c6f5599fbb25dbdd1b9b013daf85c03f3c63763e4bc4a"))]
pub struct OrderUid(pub [u8; 56]);

impl Display for OrderUid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut bytes = [0u8; 2 + 56 * 2];
bytes[..2].copy_from_slice(b"0x");
// Unwrap because the length is always correct.
hex::encode_to_slice(self.0.as_slice(), &mut bytes[2..]).unwrap();
// Unwrap because the string is always valid utf8.
let str = std::str::from_utf8(&bytes).unwrap();
f.write_str(str)
}
}

impl Serialize for OrderUid {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_str())
}
}

#[serde_as]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct OrderCreation {
/// Address of token sold.
#[schema(value_type = String, example = "0x6810e776880c02933d47db1b9fc05908e5386b96")]
pub sell_token: H160,
/// Address of token bought.
#[schema(value_type = String, example = "0x6810e776880c02933d47db1b9fc05908e5386b96")]
pub buy_token: H160,
/// An optional address to receive the proceeds of the trade instead of the
/// `owner` (i.e. the order signer).
#[serde(default)]
#[schema(value_type = Option<String>, example = "0x6810e776880c02933d47db1b9fc05908e5386b96")]
pub receiver: Option<H160>,
/// Amount of `sellToken` to be sold in atoms.
#[serde_as(as = "HexOrDecimalU256")]
#[schema(value_type = String, example = "1234567890")]
pub sell_amount: U256,
/// Amount of `buyToken` to be sold in atoms.
#[serde_as(as = "HexOrDecimalU256")]
#[schema(value_type = String, example = "1234567890")]
pub buy_amount: U256,
/// Unix timestamp (`uint32`) until which the order is valid.
#[schema(example = 0)]
pub valid_to: u32,
/// feeRatio * sellAmount + minimal_fee in atoms.
#[serde_as(as = "HexOrDecimalU256")]
#[schema(value_type = String, example = "1234567890")]
pub fee_amount: U256,
/// Buy or sell?
#[schema(example = OrderKind::Buy)]
pub kind: OrderKind,
/// Is the order fill-or-kill or partially fillable?
#[schema(example = true)]
pub partially_fillable: bool,
#[serde(default)]
#[schema(value_type = String, example = "erc20")]
pub sell_token_balance: SellTokenSource,
#[serde(default)]
#[schema(value_type = String, example = "erc20")]
pub buy_token_balance: BuyTokenDestination,
/// If set, the backend enforces that this address matches what is decoded
/// as the *signer* of the signature. This helps catch errors with
/// invalid signature encodings as the backend might otherwise silently
/// work with an unexpected address that for example does not have
/// any balance.
#[schema(value_type = Option<String>, example = "0x6810e776880c02933d47db1b9fc05908e5386b96")]
pub from: Option<H160>,
/// How was the order signed?
#[schema(value_type = String, example = "eip712")]
signing_scheme: SigningScheme,
/// A signature.
#[serde(with = "bytes_hex")]
#[schema(value_type = String, example = "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")]
signature: Vec<u8>,
/// Orders can optionally include a quote ID. This way the order can be
/// linked to a quote and enable providing more metadata when analysing
/// order slippage.
#[schema(value_type = i64, example = 0)]
pub quote_id: Option<QuoteId>,
/// The string encoding of a JSON object representing some `appData`. The
/// format of the JSON expected in the `appData` field is defined
/// [here](https://github.com/cowprotocol/app-data).
#[schema(value_type = String, example = "{\"version\":\"0.9.0\",\"metadata\":{}}")]
pub app_data: String,
#[schema(value_type = String, example = "0x0000000000000000000000000000000000000000000000000000000000000000")]
pub app_data_hash: Option<String>,
}

impl TryFrom<OrderCreation> for model::order::OrderCreation {
type Error = anyhow::Error;

fn try_from(value: OrderCreation) -> Result<Self, Self::Error> {
let signature = Signature::from_bytes(value.signing_scheme, &value.signature)?;

let kind = match value.kind {
OrderKind::Buy => model::order::OrderKind::Buy,
OrderKind::Sell => model::order::OrderKind::Sell,
};

let app_data = match value.app_data_hash {
Some(hash) => OrderCreationAppData::Both {
full: value.app_data,
expected: serde_json::from_str(&hash).unwrap(),
},
None => match serde_json::from_str::<AppDataHash>(&value.app_data) {
Ok(deser) => OrderCreationAppData::Hash { hash: deser },
Err(_) => OrderCreationAppData::Full {
full: value.app_data,
},
},
};

Ok(model::order::OrderCreation {
sell_token: value.sell_token,
buy_token: value.buy_token,
receiver: value.receiver,
sell_amount: value.sell_amount,
buy_amount: value.buy_amount,
valid_to: value.valid_to,
fee_amount: value.fee_amount,
kind,
partially_fillable: value.partially_fillable,
sell_token_balance: value.sell_token_balance,
buy_token_balance: value.buy_token_balance,
from: value.from,
signature,
quote_id: value.quote_id,
app_data,
})
}
}
Loading
Loading