diff --git a/Cargo.lock b/Cargo.lock index 5e00e109..2b4eae47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1615,12 +1615,14 @@ version = "0.3.0" source = "git+https://github.com/fluidex/paperclip.git#374e30b0bbef538f536f896999313c03f995a69a" dependencies = [ "actix-web", + "chrono", "mime", "once_cell", "paperclip-macros", "parking_lot", "pin-project", "regex", + "rust_decimal", "serde 1.0.124", "serde_json", "serde_yaml", diff --git a/Cargo.toml b/Cargo.toml index 020508aa..e16d9b92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ log = "0.4.14" nix = "0.20.0" num_enum = "0.5.1" orchestra = { git = "https://github.com/fluidex/orchestra.git", branch = "master", features = [ "exchange" ] } -paperclip = { git = "https://github.com/fluidex/paperclip.git", features = ["actix"] } +paperclip = { git = "https://github.com/fluidex/paperclip.git", features = [ "actix", "chrono", "rust_decimal" ] } qstring = "0.7.2" rand = "0.8.3" serde = { version = "1.0.124", features = [ "derive" ] } diff --git a/src/bin/openapi.rs b/src/bin/openapi.rs index e0da6aa3..988d2f6a 100644 --- a/src/bin/openapi.rs +++ b/src/bin/openapi.rs @@ -1,4 +1,5 @@ use actix_web::{App, HttpServer}; +use dingir_exchange::openapi::public_history::{order_trades, recent_trades}; use dingir_exchange::openapi::user::get_user; use dingir_exchange::restapi::state::{AppCache, AppState}; use fluidex_common::non_blocking_tracing; @@ -48,7 +49,9 @@ async fn main() -> std::io::Result<()> { .service( web::scope("/openapi") .route("/ping", web::get().to(ping)) - .route("/user/{l1addr_or_l2pubkey}", web::get().to(get_user)), + .route("/user/{l1addr_or_l2pubkey}", web::get().to(get_user)) + .route("/recenttrades/{market}", web::get().to(recent_trades)) + .route("/ordertrades/{market}/{order_id}", web::get().to(order_trades)), ) .with_json_spec_at("/api/spec") .build() diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs index 22d12a38..29752920 100644 --- a/src/openapi/mod.rs +++ b/src/openapi/mod.rs @@ -1 +1,2 @@ +pub mod public_history; pub mod user; diff --git a/src/openapi/public_history.rs b/src/openapi/public_history.rs new file mode 100644 index 00000000..4fcabefc --- /dev/null +++ b/src/openapi/public_history.rs @@ -0,0 +1,114 @@ +use crate::models::tablenames::{MARKETTRADE, USERTRADE}; +use crate::models::{self, DecimalDbType, TimestampDbType}; +use crate::restapi::errors::RpcError; +use crate::restapi::state::AppState; +use crate::restapi::types; +use chrono::{DateTime, SecondsFormat, Utc}; +use core::cmp::min; +use paperclip::actix::api_v2_operation; +use paperclip::actix::web::{self, HttpRequest, Json}; + +fn check_market_exists(_market: &str) -> bool { + // TODO + true +} + +#[api_v2_operation] +pub async fn recent_trades(req: HttpRequest, data: web::Data) -> Result>, actix_web::Error> { + let market = req.match_info().get("market").unwrap(); + let qstring = qstring::QString::from(req.query_string()); + let limit = min(100, qstring.get("limit").unwrap_or_default().parse::().unwrap_or(20)); + log::debug!("recent_trades market {} limit {}", market, limit); + if !check_market_exists(market) { + return Err(RpcError::bad_request("invalid market").into()); + } + + // TODO: this API result should be cached, either in-memory or using redis + + // Here we use the kline trade table, which is more market-centric + // and more suitable for fetching latest trades on a market. + // models::UserTrade is designed for a user to fetch his trades. + + let sql_query = format!("select * from {} where market = $1 order by time desc limit {}", MARKETTRADE, limit); + + let trades: Vec = match sqlx::query_as(&sql_query).bind(market).fetch_all(&data.db).await { + Ok(trades) => trades, + Err(error) => { + let error: RpcError = error.into(); + return Err(error.into()); + } + }; + + log::debug!("query {} recent_trades records", trades.len()); + Ok(Json(trades)) +} + +#[derive(sqlx::FromRow, Debug, Clone)] +struct QueriedUserTrade { + pub time: TimestampDbType, + pub user_id: i32, + pub trade_id: i64, + pub order_id: i64, + pub price: DecimalDbType, + pub amount: DecimalDbType, + pub quote_amount: DecimalDbType, + pub fee: DecimalDbType, +} + +#[cfg(sqlxverf)] +fn sqlverf_ticker() -> impl std::any::Any { + sqlx::query_as!( + QueriedUserTrade, + "select time, user_id, trade_id, order_id, + price, amount, quote_amount, fee + from user_trade where market = $1 and order_id = $2 + order by trade_id, time asc", + "USDT_ETH", + 10000, + ) +} + +#[api_v2_operation] +pub async fn order_trades( + app_state: web::Data, + path: web::Path<(String, i64)>, +) -> Result, actix_web::Error> { + let (market_name, order_id): (String, i64) = path.into_inner(); + log::debug!("order_trades market {} order_id {}", market_name, order_id); + + let sql_query = format!( + " + select time, user_id, trade_id, order_id, + price, amount, quote_amount, fee + from {} where market = $1 and order_id = $2 + order by trade_id, time asc", + USERTRADE + ); + + let trades: Vec = match sqlx::query_as(&sql_query) + .bind(market_name) + .bind(order_id) + .fetch_all(&app_state.db) + .await + { + Ok(trades) => trades, + Err(error) => { + let error: RpcError = error.into(); + return Err(error.into()); + } + }; + + Ok(Json(types::OrderTradeResult { + trades: trades + .into_iter() + .map(|v| types::MarketTrade { + trade_id: v.trade_id, + time: DateTime::::from_utc(v.time, Utc).to_rfc3339_opts(SecondsFormat::Secs, true), + amount: v.amount.to_string(), + quote_amount: v.quote_amount.to_string(), + price: v.price.to_string(), + fee: v.fee.to_string(), + }) + .collect(), + })) +} diff --git a/src/restapi/types.rs b/src/restapi/types.rs index e1560d20..f5ab958f 100644 --- a/src/restapi/types.rs +++ b/src/restapi/types.rs @@ -1,4 +1,5 @@ use crate::config::{Asset, Market}; +use paperclip::actix::Apiv2Schema; use serde::{Deserialize, Serialize}; #[derive(Deserialize, Debug)] @@ -42,7 +43,7 @@ pub struct TickerResult { pub to: u64, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Apiv2Schema)] pub struct MarketTrade { pub time: String, pub trade_id: i64, @@ -52,7 +53,7 @@ pub struct MarketTrade { pub fee: String, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Apiv2Schema)] pub struct OrderTradeResult { pub trades: Vec, } diff --git a/src/storage/models.rs b/src/storage/models.rs index ab1841b2..05ebe954 100644 --- a/src/storage/models.rs +++ b/src/storage/models.rs @@ -188,7 +188,7 @@ pub struct SliceHistory { pub end_trade_id: i64, } -#[derive(sqlx::FromRow, Debug, Clone, Serialize)] +#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Apiv2Schema)] pub struct MarketTrade { pub time: TimestampDbType, pub market: String, diff --git a/src/types/mod.rs b/src/types/mod.rs index 2d17c752..e16d8b22 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,4 +1,5 @@ use num_enum::TryFromPrimitive; +use paperclip::actix::Apiv2Schema; use serde::{Deserialize, Serialize}; pub type SimpleResult = anyhow::Result<()>; @@ -14,7 +15,7 @@ pub enum MarketRole { // It seems we don't need varchar(n), text is enough? // https://github.com/launchbadge/sqlx/issues/237#issuecomment-610696905 must use 'varchar'!!! // text is more readable than #[repr(i16)] and TryFromPrimitive -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy, sqlx::Type)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy, sqlx::Type, Apiv2Schema)] #[sqlx(type_name = "varchar")] #[sqlx(rename_all = "lowercase")] pub enum OrderSide {