From 6c1a3325af4880be6429ab1ad592eb26d25fc632 Mon Sep 17 00:00:00 2001 From: holygits Date: Mon, 18 Dec 2023 11:06:53 +0800 Subject: [PATCH] Add some integration tests Fix orderbook amount/price to use human readble numbers Add responses to docs --- Cargo.lock | 13 ++--- README.md | 108 ++++++++++++++++++++++++++++++++++++- src/controller.rs | 10 ++-- src/main.rs | 81 ++++++++++++++++++++++++++-- src/types.rs | 134 ++++++++++++++++++++++++++++++++++++++-------- 5 files changed, 309 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86e7265..a76caea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1362,7 +1362,7 @@ dependencies = [ [[package]] name = "drift" version = "2.48.0" -source = "git+https://github.com/circuit-research/protocol-v2?branch=cargo-add-sdk#36545c2302af4ce16684f390ab78f4fa4402e137" +source = "git+https://github.com/circuit-research/protocol-v2?branch=cargo-add-sdk#33926376876c89a70c4822ad79e84c560fd2b642" dependencies = [ "anchor-lang", "anchor-spl", @@ -1402,7 +1402,7 @@ dependencies = [ [[package]] name = "drift-sdk" version = "0.1.0" -source = "git+https://github.com/circuit-research/protocol-v2?branch=cargo-add-sdk#36545c2302af4ce16684f390ab78f4fa4402e137" +source = "git+https://github.com/circuit-research/protocol-v2?branch=cargo-add-sdk#33926376876c89a70c4822ad79e84c560fd2b642" dependencies = [ "anchor-lang", "drift", @@ -3236,12 +3236,13 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.42" +version = "0.7.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" +checksum = "527a97cdfef66f65998b5f3b637c26f5a5ec09cc52a3f9932313ac645f4190f5" dependencies = [ "bitvec", "bytecheck", + "bytes", "hashbrown 0.12.3", "ptr_meta", "rend", @@ -3253,9 +3254,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.42" +version = "0.7.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" +checksum = "b5c462a1328c8e67e4d6dbad1eb0355dd43e8ab432c6e227a43657f16ade5033" dependencies = [ "proc-macro2 1.0.70", "quote 1.0.33", diff --git a/README.md b/README.md index 32ed1c0..4ee9101 100644 --- a/README.md +++ b/README.md @@ -46,16 +46,70 @@ Options: 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 +gets info on all available spot & perp markets ```bash $ curl localhost:8080/v2/markets ``` +**Response** +```json +{ + "spot": [ + { + "marketIndex": 0, + "symbol": "USDC", + "precision": 6 + }, + // ... + ], + "perp": [ + { + "marketIndex": 0, + "symbol": "SOL-PERP", + "precision": 6 + }, + ] + // ... +} +``` + ### Get Orderbook -gets a full snapshot of the current orderbook +gets a full snapshot of the current orderbook for a given market ```bash $ curl localhost:8080/v2/orderbook -X GET -H 'content-type: application/json' -d '{"marketIndex":0,"marketType":"perp"}' ``` +**Response** +```json +{ + "slot": 266118166, + "bids": [ + { + "price": "53.616300", + "amount": "7.110000000" + }, + { + "price": "47.014300", + "amount": "2.000000000" + }, + { + "price": "20.879800", + "amount": "12.160000000" + } + ], + "asks": [ + { + "price": "80.000000", + "amount": "1.230000000" + }, + { + "price": "120.015569", + "amount": "1.000000000" + } + ] +} +``` + to stream orderbooks via websocket public DLOB servers are available at: - devnet: `wss://master.dlob.drift.trade/ws` - mainnet: `wss://dlob.drift.trade/ws` @@ -71,6 +125,40 @@ get orders by market $ curl localhost:8080/v2/orders -X GET -H 'content-type: application/json' -d '{"marketIndex":1,"marketType":"spot"}' ``` +**Response** +```json +{ + "orders": [ + { + "order_type": "limit", + "market_id": 1, + "market_type": "spot", + "amount": "-1.100000000", + "filled": "0.000000000", + "price": "80.500000", + "post_only": true, + "reduce_only": false, + "user_order_id": 101, + "order_id": 35, + "immediate_or_cancel": false + }, + { + "order_type": "limit", + "market_id": 0, + "market_type": "perp", + "amount": "-1.230000000", + "filled": "0.000000000", + "price": "80.000000", + "post_only": true, + "reduce_only": false, + "user_order_id": 0, + "order_id": 37, + "immediate_or_cancel": false + } + ] +} +``` + ### Get Positions get all positions ```bash @@ -81,6 +169,24 @@ get positions by market $ curl localhost:8080/v2/positions -X GET -H 'content-type: application/json' -d '{"marketIndex":0,"marketType":"perp"}' ``` +```json +{ + "spot": [ + { + "amount": "0.400429", + "type": "deposit", + "market_id": 0 + }, + { + "amount": "9.971961702", + "type": "deposit", + "market_id": 1 + } + ], + "perp": [] +} +``` + ### Place Orders - use sub-zero `amount` to indicate sell/offer order diff --git a/src/controller.rs b/src/controller.rs index fdd6a99..3a47d60 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use drift_sdk::{ - dlob::{DLOBClient, L2Orderbook}, + dlob::DLOBClient, types::{Context, MarketType, ModifyOrderParams, SdkError, SdkResult}, DriftClient, Pubkey, TransactionBuilder, Wallet, WsAccountProvider, }; @@ -12,7 +12,7 @@ use thiserror::Error; use crate::types::{ AllMarketsResponse, CancelAndPlaceRequest, CancelOrdersRequest, GetOrderbookRequest, GetOrdersRequest, GetOrdersResponse, GetPositionsRequest, GetPositionsResponse, - ModifyOrdersRequest, Order, PlaceOrdersRequest, SpotPosition, TxResponse, + ModifyOrdersRequest, Order, OrderbookL2, PlaceOrdersRequest, SpotPosition, TxResponse, }; pub type GatewayResult = Result; @@ -300,9 +300,9 @@ impl AppState { .map_err(handle_tx_err) } - pub async fn get_orderbook(&self, req: GetOrderbookRequest) -> GatewayResult { - let book = self.dlob_client.get_l2(req.market.as_market_id()).await; - Ok(book?) + pub async fn get_orderbook(&self, req: GetOrderbookRequest) -> GatewayResult { + let book = self.dlob_client.get_l2(req.market.as_market_id()).await?; + Ok(OrderbookL2::new(book, req.market, self.context())) } } diff --git a/src/main.rs b/src/main.rs index ff12bfa..2884801 100644 --- a/src/main.rs +++ b/src/main.rs @@ -193,20 +193,95 @@ fn handle_deser_error(err: serde_json::Error) -> Either #[cfg(test)] mod tests { - use actix_web::{http::header::ContentType, test, App}; + use actix_web::{http::Method, test, App}; + + use crate::types::Market; use super::*; + const TEST_ENDPOINT: &str = "https://api.devnet.solana.com"; + + fn get_seed() -> String { + std::env::var("DRIFT_GATEWAY_KEY") + .expect("DRIFT_GATEWAY_KEY is set") + .to_string() + } + + async fn setup_controller() -> AppState { + AppState::new(&get_seed(), TEST_ENDPOINT, true).await + } + #[actix_web::test] async fn get_orders_works() { - let controller = AppState::new("test", "example.com", true).await; + let controller = setup_controller().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 req = test::TestRequest::default() + .method(Method::GET) + .uri("/orders") + .to_request(); + + let resp = test::call_service(&app, req).await; + assert!(resp.status().is_success()); + } + + #[actix_web::test] + async fn get_positions_works() { + let controller = setup_controller().await; + let app = test::init_service( + App::new() + .app_data(web::Data::new(controller)) + .service(get_positions), + ) + .await; + let req = test::TestRequest::default() + .method(Method::GET) + .uri("/positions") + .to_request(); + + let resp = test::call_service(&app, req).await; + assert!(resp.status().is_success()); + } + + #[actix_web::test] + async fn get_orderbook_works() { + let controller = setup_controller().await; + let app = test::init_service( + App::new() + .app_data(web::Data::new(controller)) + .service(get_orderbook), + ) + .await; + let req = test::TestRequest::default() + .method(Method::GET) + .uri("/orderbook") + .set_json(GetOrderbookRequest { + market: Market::perp(0), // sol-perp + }) + .to_request(); + + let resp = test::call_service(&app, req).await; + assert!(resp.status().is_success()); + } + + #[actix_web::test] + async fn get_markets_works() { + let controller = setup_controller().await; + let app = test::init_service( + App::new() + .app_data(web::Data::new(controller)) + .service(get_markets), + ) + .await; + let req = test::TestRequest::default() + .method(Method::GET) + .uri("/markets") + .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 4a73204..8144295 100644 --- a/src/types.rs +++ b/src/types.rs @@ -7,13 +7,14 @@ use drift_sdk::{ spot_market_config_by_index, PerpMarketConfig, SpotMarketConfig, BASE_PRECISION, PRICE_PRECISION, }, + dlob::{self, L2Level, L2Orderbook}, types::{ self as sdk_types, Context, MarketType, ModifyOrderParams, OrderParams, PositionDirection, PostOnlyParam, }, }; use rust_decimal::Decimal; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; #[derive(Serialize, Deserialize, Debug)] pub struct Order { @@ -37,13 +38,7 @@ pub struct Order { impl Order { pub fn from_sdk_order(value: sdk_types::Order, context: Context) -> Self { - let precision = if let MarketType::Perp = value.market_type { - BASE_PRECISION.ilog10() - } else { - let config = - spot_market_config_by_index(context, value.market_index).expect("market exists"); - config.precision_exp as u32 - }; + let decimals = get_market_decimals(context, value.market_index, value.market_type); // 0 = long // 1 = short @@ -53,8 +48,8 @@ impl Order { market_id: value.market_index, market_type: value.market_type, price: Decimal::new(value.price as i64, PRICE_PRECISION.ilog10()), - amount: Decimal::new(value.base_asset_amount as i64 * to_sign, precision), - filled: Decimal::new(value.base_asset_amount_filled as i64, precision), + amount: Decimal::new(value.base_asset_amount as i64 * to_sign, decimals), + filled: Decimal::new(value.base_asset_amount_filled as i64, decimals), immediate_or_cancel: value.immediate_or_cancel, reduce_only: value.reduce_only, order_type: value.order_type, @@ -165,12 +160,7 @@ impl ModifyOrder { market_type: MarketType, context: Context, ) -> ModifyOrderParams { - let target_scale = if let MarketType::Perp = market_type { - BASE_PRECISION as u32 - } else { - let config = spot_market_config_by_index(context, market_index).expect("market exists"); - config.precision as u32 - }; + let target_scale = get_market_precision(context, market_index, market_type); let (amount, direction) = if let Some(base_amount) = self.amount { let direction = if base_amount.is_sign_negative() { @@ -186,11 +176,9 @@ impl ModifyOrder { (None, None) }; - let price = if let Some(price) = self.price { - Some(scale_decimal_to_u64(price, PRICE_PRECISION as u32)) - } else { - None - }; + let price = self + .price + .map(|p| scale_decimal_to_u64(p, PRICE_PRECISION as u32)); ModifyOrderParams { base_asset_amount: amount, @@ -298,7 +286,7 @@ impl PlaceOrder { } #[cfg_attr(test, derive(Default))] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Copy, Clone)] #[serde(rename_all = "camelCase")] pub struct Market { /// The market index @@ -356,6 +344,7 @@ pub struct GetPositionsResponse { #[derive(Serialize)] pub struct MarketInfo { + #[serde(rename = "marketIndex")] market_id: u16, symbol: &'static str, precision: u8, @@ -425,6 +414,107 @@ pub struct CancelAndPlaceRequest { pub place: PlaceOrdersRequest, } +/// Serialize DLOB with human readable numeric values +pub struct OrderbookL2 { + inner: L2Orderbook, + context: Context, + market: Market, +} + +impl OrderbookL2 { + pub fn new(inner: L2Orderbook, market: Market, context: Context) -> Self { + Self { + inner, + market, + context, + } + } +} + +impl Serialize for OrderbookL2 { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(3))?; + map.serialize_entry("slot", &self.inner.slot)?; + map.serialize_entry( + "bids", + &PriceLevelSerializer { + inner: self.inner.bids.as_slice(), + market: self.market, + context: self.context, + }, + )?; + map.serialize_entry( + "asks", + &PriceLevelSerializer { + inner: self.inner.asks.as_slice(), + market: self.market, + context: self.context, + }, + )?; + map.end() + } +} + +struct PriceLevelSerializer<'a> { + inner: &'a [L2Level], + market: Market, + context: Context, +} + +impl<'a> Serialize for PriceLevelSerializer<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_seq( + self.inner + .iter() + .map(|l| PriceLevel::new(l, self.market, self.context)), + ) + } +} + +#[derive(Serialize, Deserialize)] +pub struct PriceLevel { + price: Decimal, + amount: Decimal, +} + +impl PriceLevel { + pub fn new(level: &dlob::L2Level, market: Market, context: Context) -> Self { + let decimals = get_market_decimals(context, market.market_index, market.market_type); + Self { + price: Decimal::new(level.price as i64, PRICE_PRECISION.ilog10()), + amount: Decimal::new(level.size as i64, decimals), + } + } +} + +/// Return the number of units in a whole token for this market +#[inline] +fn get_market_precision(context: Context, market_index: u16, market_type: MarketType) -> u32 { + if let MarketType::Perp = market_type { + BASE_PRECISION as u32 + } else { + let config = spot_market_config_by_index(context, market_index).expect("market exists"); + config.precision as u32 + } +} + +/// Return the number of decimal places for the market +#[inline] +fn get_market_decimals(context: Context, market_index: u16, market_type: MarketType) -> u32 { + if let MarketType::Perp = market_type { + BASE_PRECISION.ilog10() + } else { + let config = spot_market_config_by_index(context, market_index).expect("market exists"); + config.precision_exp as u32 + } +} + #[cfg(test)] mod tests { use drift_sdk::types::{Context, MarketType, PositionDirection};