From def91b73b43cecb108e129fcc3ef1e4ddc7f4351 Mon Sep 17 00:00:00 2001 From: holygits Date: Fri, 15 Dec 2023 16:44:45 +0800 Subject: [PATCH] make tx response in json format Add cancelAndPlace endpoint --- README.md | 31 ++++++++++++++++++++++++++ src/controller.rs | 57 +++++++++++++++++++++++++++++++++++++++-------- src/main.rs | 40 +++++++++++++++++++++++++++++++-- src/types.rs | 17 ++++++++++++++ 4 files changed, 134 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d35d372..83e7c82 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ Options: ## API Examples +Please refer to https://drift-labs.github.io/v2-teacher/ for further examples and reference documentation on various types, fields, and operations available on drift. + ### Get Market Info ```bash $ curl localhost:8080/v2/markets @@ -110,6 +112,7 @@ $ curl localhost:8080/v2/orders -X POST -H 'content-type: application/json' -d ' }] }' ``` +Returns solana tx signature on success ### Modify Orders like place orders but caller must specify either `orderId` or `userOrderId` to indicate which order to modify. @@ -130,6 +133,7 @@ $ curl localhost:8080/v2/orders -X PATCH -H 'content-type: application/json' -d }] }' ``` +Returns solana tx signature on success ### Cancel Orders ```bash @@ -142,6 +146,33 @@ $ curl localhost:8080/v2/orders -X DELETE -H 'content-type: application/json' -d # cancel all orders $ curl localhost:8080/v2/orders -X DELETE ``` +Returns solana tx signature on success + +### Cancel and Place Orders + +Atomically cancel then place orders without possible downtime. +Request format is an embedded cancel and place request + +```bash +$ curl localhost:8080/v2/orders/cancelAndPlace -X POST -H 'content-type: application/json' -d '{ + "cancel": { + "marketIndex": 0, + "marketType": "perp" + }, + "place": { + "orders": [ + { + "marketIndex": 0, + "marketType": "perp", + "amount": -1.23, + "price": 80.0, + "postOnly": true, + "orderType": "limit", + "immediateOrCancel": false, + "reduce_only": false + }] + } +}' ### Errors error responses have the following JSON structure: diff --git a/src/controller.rs b/src/controller.rs index 6879730..fdd6a99 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -10,9 +10,9 @@ use log::error; use thiserror::Error; use crate::types::{ - AllMarketsResponse, CancelOrdersRequest, GetOrderbookRequest, GetOrdersRequest, - GetOrdersResponse, GetPositionsRequest, GetPositionsResponse, ModifyOrdersRequest, Order, - PlaceOrdersRequest, SpotPosition, + AllMarketsResponse, CancelAndPlaceRequest, CancelOrdersRequest, GetOrderbookRequest, + GetOrdersRequest, GetOrdersResponse, GetPositionsRequest, GetPositionsResponse, + ModifyOrdersRequest, Order, PlaceOrdersRequest, SpotPosition, TxResponse, }; pub type GatewayResult = Result; @@ -82,7 +82,7 @@ impl AppState { /// 2) "user ids" are set, cancel all orders by user assigned id /// 3) ids are given, cancel all orders by id (global, exchange assigned id) /// 4) catch all. cancel all orders - pub async fn cancel_orders(&self, req: CancelOrdersRequest) -> GatewayResult { + pub async fn cancel_orders(&self, req: CancelOrdersRequest) -> GatewayResult { let user_data = self.client.get_user_account(self.user()).await?; let builder = TransactionBuilder::new(&self.wallet, &user_data); @@ -106,7 +106,7 @@ impl AppState { self.client .sign_and_send(&self.wallet, tx) .await - .map(|s| s.to_string()) + .map(|s| TxResponse::new(s.to_string())) .map_err(handle_tx_err) } @@ -187,7 +187,46 @@ impl AppState { } } - pub async fn place_orders(&self, req: PlaceOrdersRequest) -> GatewayResult { + pub async fn cancel_and_place_orders( + &self, + req: CancelAndPlaceRequest, + ) -> GatewayResult { + let orders = req + .place + .orders + .into_iter() + .map(|o| o.to_order_params(self.context())) + .collect(); + + let user_data = self.client.get_user_account(self.user()).await?; + let builder = TransactionBuilder::new(&self.wallet, &user_data); + + let tx = if let Some(market) = req.cancel.market { + builder.cancel_orders((market.market_index, market.market_type), None) + } else if !req.cancel.user_ids.is_empty() { + let order_ids = user_data + .orders + .iter() + .filter(|o| o.slot > 0 && req.cancel.user_ids.contains(&o.user_order_id)) + .map(|o| o.order_id) + .collect(); + builder.cancel_orders_by_id(order_ids) + } else if !req.cancel.ids.is_empty() { + builder.cancel_orders_by_id(req.cancel.ids) + } else { + builder.cancel_all_orders() + } + .place_orders(orders) + .build(); + + self.client + .sign_and_send(&self.wallet, tx) + .await + .map(|s| TxResponse::new(s.to_string())) + .map_err(handle_tx_err) + } + + pub async fn place_orders(&self, req: PlaceOrdersRequest) -> GatewayResult { let orders = req .orders .into_iter() @@ -203,11 +242,11 @@ impl AppState { self.client .sign_and_send(&self.wallet, tx) .await - .map(|s| s.to_string()) + .map(|s| TxResponse::new(s.to_string())) .map_err(handle_tx_err) } - pub async fn modify_orders(&self, req: ModifyOrdersRequest) -> GatewayResult { + pub async fn modify_orders(&self, req: ModifyOrdersRequest) -> GatewayResult { let user_data = &self.client.get_user_account(self.user()).await?; // NB: its possible to let the drift program sort the modifications by userOrderId // sorting it client side for simplicity @@ -257,7 +296,7 @@ impl AppState { self.client .sign_and_send(&self.wallet, tx) .await - .map(|s| s.to_string()) + .map(|s| TxResponse::new(s.to_string())) .map_err(handle_tx_err) } diff --git a/src/main.rs b/src/main.rs index 2f2dfbf..ff12bfa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,10 @@ use log::{error, info}; use controller::{AppState, ControllerError}; use serde_json::json; -use types::{CancelOrdersRequest, GetOrderbookRequest, ModifyOrdersRequest, PlaceOrdersRequest}; +use types::{ + CancelAndPlaceRequest, CancelOrdersRequest, GetOrderbookRequest, ModifyOrdersRequest, + PlaceOrdersRequest, +}; mod controller; mod types; @@ -61,6 +64,17 @@ async fn cancel_orders(controller: web::Data, body: web::Bytes) -> imp handle_result(controller.cancel_orders(req).await) } +#[post("/orders/cancelAndPlace")] +async fn cancel_and_place_orders( + controller: web::Data, + body: web::Bytes, +) -> impl Responder { + match serde_json::from_slice::<'_, CancelAndPlaceRequest>(body.as_ref()) { + Ok(req) => handle_result(controller.cancel_and_place_orders(req).await), + Err(err) => handle_deser_error(err), + } +} + #[get("/positions")] async fn get_positions(controller: web::Data, body: web::Bytes) -> impl Responder { let mut req = None; @@ -111,7 +125,8 @@ async fn main() -> std::io::Result<()> { .service(create_orders) .service(cancel_orders) .service(modify_orders) - .service(get_orderbook), + .service(get_orderbook) + .service(cancel_and_place_orders), ) }) .bind((config.host, config.port))? @@ -175,3 +190,24 @@ fn handle_deser_error(err: serde_json::Error) -> Either } ))) } + +#[cfg(test)] +mod tests { + use actix_web::{http::header::ContentType, test, App}; + + use super::*; + + #[actix_web::test] + async fn get_orders_works() { + let controller = AppState::new("test", "example.com", true).await; + let app = test::init_service( + App::new() + .app_data(web::Data::new(controller)) + .service(get_orders), + ) + .await; + let req = test::TestRequest::default().to_request(); + let resp = test::call_service(&app, req).await; + assert!(resp.status().is_success()); + } +} diff --git a/src/types.rs b/src/types.rs index 8bcd818..4a73204 100644 --- a/src/types.rs +++ b/src/types.rs @@ -408,6 +408,23 @@ pub struct GetOrderbookRequest { pub market: Market, } +#[derive(Serialize, Deserialize)] +pub struct TxResponse { + tx: String, +} + +impl TxResponse { + pub fn new(tx_signature: String) -> Self { + Self { tx: tx_signature } + } +} + +#[derive(Serialize, Deserialize)] +pub struct CancelAndPlaceRequest { + pub cancel: CancelOrdersRequest, + pub place: PlaceOrdersRequest, +} + #[cfg(test)] mod tests { use drift_sdk::types::{Context, MarketType, PositionDirection};