From dee9571f2f41e3b13d5ad95dac855c8833a94801 Mon Sep 17 00:00:00 2001 From: Doug Chimento Date: Sun, 27 Jul 2025 09:07:47 +0000 Subject: [PATCH] feat: Add drift-gateway-types crate for gateway controller resquest/response types --- Cargo.lock | 40 ++- Cargo.toml | 35 +- crates/drift-gateway-types/Cargo.toml | 19 + .../drift-gateway-types/src/account_event.rs | 294 +++++++++++++++ crates/drift-gateway-types/src/lib.rs | 57 +++ .../drift-gateway-types/src}/types.rs | 135 +++---- src/controller.rs | 27 +- src/main.rs | 4 +- src/websocket.rs | 338 +----------------- 9 files changed, 515 insertions(+), 434 deletions(-) create mode 100644 crates/drift-gateway-types/Cargo.toml create mode 100644 crates/drift-gateway-types/src/account_event.rs create mode 100644 crates/drift-gateway-types/src/lib.rs rename {src => crates/drift-gateway-types/src}/types.rs (91%) diff --git a/Cargo.lock b/Cargo.lock index 38e84e1..9631c2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1170,9 +1170,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.30" +version = "1.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" dependencies = [ "jobserver", "libc", @@ -1616,6 +1616,7 @@ dependencies = [ "actix-web", "argh", "base64 0.22.1", + "drift-gateway-types", "drift-rs", "env_logger 0.11.8", "faster-hex", @@ -1636,6 +1637,18 @@ dependencies = [ "tokio-tungstenite 0.27.0", ] +[[package]] +name = "drift-gateway-types" +version = "0.1.0" +dependencies = [ + "drift-rs", + "faster-hex", + "nanoid", + "rust_decimal", + "serde", + "serde_json", +] + [[package]] name = "drift-idl-gen" version = "0.2.0" @@ -3530,9 +3543,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7251471db004e509f4e75a62cca9435365b5ec7bcdff530d612ac7c87c44a792" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] @@ -3754,9 +3767,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.30" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069a8df149a16b1a12dcc31497c3396a173844be3cac4bd40c9e7671fef96671" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "log", "once_cell", @@ -3924,9 +3937,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "itoa", "memchr", @@ -6416,9 +6429,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.0" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", @@ -7089,7 +7102,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] [[package]] @@ -7110,10 +7123,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", diff --git a/Cargo.toml b/Cargo.toml index 60b6f11..9443a9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,18 @@ +[workspace] +members = [".", "crates/drift-gateway-types"] +resolver = "2" + +[workspace.dependencies] +drift-rs = { git = "https://github.com/drift-labs/drift-rs", rev = "c6a3647" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +rust_decimal = "1" +tokio = { version = "1", features = ["full"] } +reqwest = { version = "0.12", features = ["json"] } +thiserror = "2" +nanoid = "0.4" +faster-hex = "0.10" + [package] name = "drift-gateway" version = "1.5.3" @@ -6,25 +21,27 @@ edition = "2021" [dependencies] actix-web = "*" argh = "*" -drift-rs = { git = "https://github.com/drift-labs/drift-rs", rev = "c6a3647" } +drift-rs = { workspace = true } base64 = "0.22.1" env_logger = "*" -faster-hex = "0.10.0" +faster-hex = { workspace = true } futures-util = "*" log = "*" -nanoid = "0.4.0" -reqwest = { version = "*", features = ["json"] } -rust_decimal = "*" -serde = { version = "*", features = ["derive"] } -serde_json = "*" +nanoid = { workspace = true } +reqwest = { workspace = true } +rust_decimal = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } sha256 = "1.6.0" solana-account-decoder-client-types = "2" solana-rpc-client-api = "2" solana-sdk = "2" solana-transaction-status = "2" -thiserror = "*" -tokio = {version ="*", features = ["full"]} +thiserror = { workspace = true } +tokio = { workspace = true } tokio-tungstenite = "*" +# Add dependency on our types crate +drift-gateway-types = { path = "crates/drift-gateway-types" } [profile.release] panic = 'abort' diff --git a/crates/drift-gateway-types/Cargo.toml b/crates/drift-gateway-types/Cargo.toml new file mode 100644 index 0000000..e69f1a9 --- /dev/null +++ b/crates/drift-gateway-types/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "drift-gateway-types" +version = "0.1.0" +edition = "2021" +description = "Shared types for Drift Gateway API" +license = "Apache-2.0" +readme = "README.md" +repository = "https://github.com/drift-labs/gateway" +homepage = "https://drift.trade" +categories = ["cryptography::cryptocurrencies", "api-bindings"] +keywords = ["solana", "dex", "drift", "sdk"] + +[dependencies] +drift-rs = { workspace = true } +faster-hex = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +rust_decimal = { workspace = true } +nanoid = { workspace = true } diff --git a/crates/drift-gateway-types/src/account_event.rs b/crates/drift-gateway-types/src/account_event.rs new file mode 100644 index 0000000..ca8d1cb --- /dev/null +++ b/crates/drift-gateway-types/src/account_event.rs @@ -0,0 +1,294 @@ +use drift_rs::types::{MarketType, Order, OrderType, PositionDirection}; +use rust_decimal::Decimal; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::PRICE_DECIMALS; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub enum AccountEvent { + Fill(FillEvent), + Trigger { order_id: u32, oracle_price: u64 }, + OrderCreate(OrderCreateEvent), + OrderCancel(OrderCancelEvent), + OrderCancelMissing(OrderCancelMissingEvent), + OrderExpire(OrderExpireEvent), + FundingPayment(FundingPaymentEvent), + Swap(SwapEvent), +} + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct FillEvent { + pub side: Side, + pub fee: Decimal, + pub amount: Decimal, + pub price: Decimal, + pub oracle_price: Decimal, + pub order_id: u32, + pub market_index: u16, + #[serde( + serialize_with = "crate::types::ser_market_type", + deserialize_with = "crate::types::de_market_type" + )] + pub market_type: MarketType, + pub ts: u64, + pub tx_idx: usize, + pub signature: String, + pub maker: Option, + pub maker_order_id: Option, + pub maker_fee: Option, + pub taker: Option, + pub taker_order_id: Option, + pub taker_fee: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OrderCreateEvent { + pub order: OrderWithDecimals, + pub ts: u64, + pub signature: String, + pub tx_idx: usize, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OrderCancelEvent { + pub order_id: u32, + pub ts: u64, + pub signature: String, + pub tx_idx: usize, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OrderCancelMissingEvent { + pub user_order_id: u8, + pub order_id: u32, + pub signature: String, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OrderExpireEvent { + pub order_id: u32, + pub fee: Decimal, + pub ts: u64, + pub signature: String, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct FundingPaymentEvent { + pub amount: Decimal, + pub market_index: u16, + pub ts: u64, + pub signature: String, + pub tx_idx: usize, +} + +impl AccountEvent { + pub fn fill( + side: PositionDirection, + fee: i64, + base_amount: u64, + quote_amount: u64, + oracle_price: i64, + order_id: u32, + ts: u64, + decimals: u32, + signature: &String, + tx_idx: usize, + market_index: u16, + market_type: MarketType, + maker: Option, + maker_order_id: Option, + maker_fee: Option, + taker: Option, + taker_order_id: Option, + taker_fee: Option, + ) -> Self { + let base_amount = Decimal::new(base_amount as i64, decimals); + let price = Decimal::new(quote_amount as i64, PRICE_DECIMALS) / base_amount; + let f = FillEvent { + side: if let PositionDirection::Long = side { + Side::Buy + } else { + Side::Sell + }, + price: price.normalize(), + oracle_price: Decimal::new(oracle_price, PRICE_DECIMALS).normalize(), + fee: Decimal::new(fee, PRICE_DECIMALS).normalize(), + order_id, + amount: base_amount.normalize(), + ts, + signature: signature.to_string(), + market_index, + market_type, + tx_idx, + maker, + maker_order_id, + maker_fee: maker_fee.map(|x| Decimal::new(x, PRICE_DECIMALS)), + taker, + taker_order_id, + taker_fee: taker_fee.map(|x| Decimal::new(x, PRICE_DECIMALS)), + }; + Self::Fill(f) + } +} + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub enum Side { + #[default] + Buy, + Sell, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OrderWithDecimals { + /// The slot the order was placed + pub slot: u64, + /// The limit price for the order (can be 0 for market orders) + /// For orders with an auction, this price isn't used until the auction is complete + pub price: Decimal, + /// The size of the order + pub amount: Decimal, + /// The amount of the order filled + pub filled: Decimal, + /// At what price the order will be triggered. Only relevant for trigger orders + pub trigger_price: Decimal, + /// The start price for the auction. Only relevant for market/oracle orders + pub auction_start_price: Decimal, + /// The end price for the auction. Only relevant for market/oracle orders + pub auction_end_price: Decimal, + /// The time when the order will expire + pub max_ts: i64, + /// If set, the order limit price is the oracle price + this offset + pub oracle_price_offset: Decimal, + /// The id for the order. Each users has their own order id space + pub order_id: u32, + /// The perp/spot market index + pub market_index: u16, + /// The type of order + #[serde(serialize_with = "ser_order_type", deserialize_with = "de_order_type")] + pub order_type: OrderType, + /// Whether market is spot or perp + #[serde( + serialize_with = "crate::types::ser_market_type", + deserialize_with = "crate::types::de_market_type" + )] + pub market_type: MarketType, + /// User generated order id. Can make it easier to place/cancel orders + pub user_order_id: u8, + #[serde( + serialize_with = "ser_position_direction", + deserialize_with = "de_position_direction" + )] + pub direction: PositionDirection, + /// Whether the order is allowed to only reduce position size + pub reduce_only: bool, + /// Whether the order must be a maker + pub post_only: bool, + /// Whether the order must be canceled the same slot it is placed + pub immediate_or_cancel: bool, + /// How many slots the auction lasts + pub auction_duration: u8, +} + +impl OrderWithDecimals { + pub fn from_order(value: Order, decimals: u32) -> Self { + Self { + slot: value.slot, + price: Decimal::new(value.price as i64, PRICE_DECIMALS).normalize(), + amount: Decimal::new(value.base_asset_amount as i64, decimals).normalize(), + filled: Decimal::new(value.base_asset_amount_filled as i64, decimals).normalize(), + trigger_price: Decimal::new(value.trigger_price as i64, PRICE_DECIMALS).normalize(), + auction_start_price: Decimal::new(value.auction_start_price, PRICE_DECIMALS) + .normalize(), + auction_end_price: Decimal::new(value.auction_end_price, PRICE_DECIMALS).normalize(), + oracle_price_offset: Decimal::new(value.oracle_price_offset as i64, PRICE_DECIMALS) + .normalize(), + max_ts: value.max_ts, + order_id: value.order_id, + market_index: value.market_index, + order_type: value.order_type, + market_type: value.market_type, + user_order_id: value.user_order_id, + direction: value.direction, + reduce_only: value.reduce_only, + post_only: value.post_only, + immediate_or_cancel: value.immediate_or_cancel, + auction_duration: value.auction_duration, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct SwapEvent { + pub amount_in: Decimal, + pub amount_out: Decimal, + pub market_in: u16, + pub market_out: u16, + pub ts: u64, + pub tx_idx: usize, + pub signature: String, +} + +fn ser_order_type(x: &OrderType, s: S) -> Result +where + S: Serializer, +{ + s.serialize_str(match x { + OrderType::Limit => "limit", + OrderType::Market => "market", + OrderType::Oracle => "oracle", + OrderType::TriggerLimit => "triggerLimit", + OrderType::TriggerMarket => "triggerMarket", + }) +} + +fn ser_position_direction(x: &PositionDirection, s: S) -> Result +where + S: Serializer, +{ + s.serialize_str(match x { + PositionDirection::Long => "buy", + PositionDirection::Short => "sell", + }) +} + +fn de_position_direction<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + match s.as_str() { + "buy" => Ok(PositionDirection::Long), + "sell" => Ok(PositionDirection::Short), + _ => Err(serde::de::Error::custom(format!( + "unknown position direction: {}", + s + ))), + } +} + +fn de_order_type<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + match s.as_str() { + "limit" => Ok(OrderType::Limit), + "market" => Ok(OrderType::Market), + "oracle" => Ok(OrderType::Oracle), + "triggerLimit" => Ok(OrderType::TriggerLimit), + "triggerMarket" => Ok(OrderType::TriggerMarket), + _ => Err(serde::de::Error::custom(format!( + "unknown order type: {}", + s + ))), + } +} diff --git a/crates/drift-gateway-types/src/lib.rs b/crates/drift-gateway-types/src/lib.rs new file mode 100644 index 0000000..2e5b5df --- /dev/null +++ b/crates/drift-gateway-types/src/lib.rs @@ -0,0 +1,57 @@ +mod account_event; +mod types; +pub use account_event::*; +pub use drift_rs; +use drift_rs::{ + constants::ProgramData, + math::constants::{BASE_PRECISION, PRICE_PRECISION, QUOTE_PRECISION}, + types::MarketType, +}; +pub use drift_rs::{constants::PROGRAM_ID, Context}; +use serde::{Deserialize, Serialize}; +pub use types::*; +pub const PRICE_DECIMALS: u32 = PRICE_PRECISION.ilog10(); +pub const QUOTE_DECIMALS: u32 = QUOTE_PRECISION.ilog10(); + +/// Return the number of decimal places for the market +#[inline] +pub fn get_market_decimals(program_data: &ProgramData, market: Market) -> u32 { + if let MarketType::Perp = market.market_type { + BASE_PRECISION.ilog10() + } else { + let spot_market = program_data + .spot_market_config_by_index(market.market_index) + .expect("market exists"); + spot_market.decimals + } +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "lowercase")] +pub enum Method { + Subscribe, + Unsubscribe, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "lowercase")] +pub enum Channel { + Fills, + Orders, + Funding, + Swap, +} +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct WsRequest { + pub method: Method, + pub sub_account_id: u8, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct WsEvent { + pub data: T, + pub channel: Channel, + pub sub_account_id: u8, +} diff --git a/src/types.rs b/crates/drift-gateway-types/src/types.rs similarity index 91% rename from src/types.rs rename to crates/drift-gateway-types/src/types.rs index 31907a6..c770263 100644 --- a/src/types.rs +++ b/crates/drift-gateway-types/src/types.rs @@ -5,9 +5,8 @@ use std::convert::TryInto; use drift_rs::{ - constants::ProgramData, math::{ - constants::{BASE_PRECISION, PRICE_PRECISION, QUOTE_PRECISION}, + constants::{BASE_PRECISION, PRICE_PRECISION}, liquidation::{CollateralInfo, MarginRequirementInfo}, }, swift_order_subscriber::SignedOrderType, @@ -22,33 +21,29 @@ use nanoid::nanoid; use rust_decimal::Decimal; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::websocket::AccountEvent; - -/// decimal places in price values -pub const PRICE_DECIMALS: u32 = PRICE_PRECISION.ilog10(); -pub const QUOTE_DECIMALS: u32 = QUOTE_PRECISION.ilog10(); +use crate::{account_event::AccountEvent, PRICE_DECIMALS, QUOTE_DECIMALS}; #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Order { #[serde(serialize_with = "order_type_ser", deserialize_with = "order_type_de")] - order_type: sdk_types::OrderType, - market_index: u16, + pub order_type: sdk_types::OrderType, + pub market_index: u16, #[serde( serialize_with = "ser_market_type", deserialize_with = "de_market_type" )] - market_type: MarketType, - amount: Decimal, - filled: Decimal, - price: Decimal, - post_only: bool, - reduce_only: bool, - user_order_id: u8, - order_id: u32, - immediate_or_cancel: bool, + pub market_type: MarketType, + pub amount: Decimal, + pub filled: Decimal, + pub price: Decimal, + pub post_only: bool, + pub reduce_only: bool, + pub user_order_id: u8, + pub order_id: u32, + pub immediate_or_cancel: bool, #[serde(skip_serializing_if = "Option::is_none")] - oracle_price_offset: Option, + pub oracle_price_offset: Option, } impl Order { @@ -113,10 +108,10 @@ where #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct SpotPosition { - amount: Decimal, + pub amount: Decimal, #[serde(rename = "type")] - balance_type: String, // deposit or borrow - market_index: u16, + pub balance_type: String, // deposit or borrow + pub market_index: u16, } impl SpotPosition { @@ -139,11 +134,11 @@ impl SpotPosition { #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct PerpPosition { - amount: Decimal, - average_entry: Decimal, - market_index: u16, + pub amount: Decimal, + pub average_entry: Decimal, + pub market_index: u16, #[serde(flatten, skip_serializing_if = "Option::is_none")] - extended: Option, + pub extended: Option, } impl PerpPosition { @@ -200,13 +195,13 @@ pub struct ModifyOrdersRequest { pub struct ModifyOrder { #[serde(flatten)] pub market: Market, - amount: Option, - price: Option, + pub amount: Option, + pub price: Option, pub user_order_id: Option, pub order_id: Option, - reduce_only: Option, - oracle_price_offset: Option, - max_ts: Option, + pub reduce_only: Option, + pub oracle_price_offset: Option, + pub max_ts: Option, } impl ModifyOrder { @@ -261,13 +256,13 @@ pub struct PlaceOrdersRequest { pub struct PlaceOrder { #[serde(flatten)] pub market: Market, - amount: Decimal, + pub amount: Decimal, #[serde(default)] - price: Decimal, + pub price: Decimal, #[serde(default)] - trigger_price: Option, + pub trigger_price: Option, #[serde(default)] - trigger_condition: Option, + pub trigger_condition: Option, /// 0 indicates it is not set (according to program) #[serde(default)] pub user_order_id: u8, @@ -276,20 +271,20 @@ pub struct PlaceOrder { deserialize_with = "order_type_de", default )] - order_type: sdk_types::OrderType, + pub order_type: sdk_types::OrderType, #[serde(default)] - post_only: bool, + pub post_only: bool, #[serde(default)] - reduce_only: bool, + pub reduce_only: bool, #[serde(default)] - oracle_price_offset: Option, - max_ts: Option, + pub oracle_price_offset: Option, + pub max_ts: Option, #[serde(default)] - auction_duration: Option, + pub auction_duration: Option, #[serde(default)] - auction_start_price: Option, + pub auction_start_price: Option, #[serde(default)] - auction_end_price: Option, + pub auction_end_price: Option, } #[derive(Serialize, Debug)] @@ -495,19 +490,19 @@ pub struct GetPositionsResponse { pub perp: Vec, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct MarketInfo { #[serde(rename = "marketIndex")] - market_id: u16, - symbol: String, - price_step: Decimal, - amount_step: Decimal, - min_order_size: Decimal, + pub market_id: u16, + pub symbol: String, + pub price_step: Decimal, + pub amount_step: Decimal, + pub min_order_size: Decimal, #[serde(skip_serializing_if = "Option::is_none")] - initial_margin_ratio: Option, + pub initial_margin_ratio: Option, #[serde(skip_serializing_if = "Option::is_none")] - maintenance_margin_ratio: Option, + pub maintenance_margin_ratio: Option, } impl From for MarketInfo { @@ -550,14 +545,15 @@ impl From for MarketInfo { } } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MarketInfoResponse { pub open_interest: u64, pub max_open_interest: u64, } -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct AllMarketsResponse { pub spot: Vec, pub perp: Vec, @@ -579,7 +575,7 @@ pub struct CancelOrdersRequest { #[derive(Serialize, Deserialize, Debug)] pub struct TxResponse { - tx: String, + pub tx: String, } impl TxResponse { @@ -590,10 +586,10 @@ impl TxResponse { #[derive(Serialize, Deserialize, Debug, Default)] pub struct TxEventsResponse { - events: Vec, - success: bool, + pub events: Vec, + pub success: bool, #[serde(skip_serializing_if = "Option::is_none")] - error: Option, + pub error: Option, } impl TxEventsResponse { @@ -632,31 +628,18 @@ pub struct CancelAndPlaceRequest { pub place: PlaceOrdersRequest, } -/// Return the number of decimal places for the market -#[inline] -pub(crate) fn get_market_decimals(program_data: &ProgramData, market: Market) -> u32 { - if let MarketType::Perp = market.market_type { - BASE_PRECISION.ilog10() - } else { - let spot_market = program_data - .spot_market_config_by_index(market.market_index) - .expect("market exists"); - spot_market.decimals - } -} - -#[derive(Serialize, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct SolBalanceResponse { pub balance: Decimal, pub pubkey: String, } -#[derive(Serialize, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct AuthorityResponse { pub pubkey: String, } -#[derive(Serialize, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct UserMarginResponse { pub initial: Decimal, pub maintenance: Decimal, @@ -673,7 +656,7 @@ impl From for UserMarginResponse { } } -#[derive(Serialize, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct UserLeverageResponse { pub leverage: Decimal, } @@ -686,7 +669,7 @@ impl From for UserLeverageResponse { } } -#[derive(Serialize, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct UserCollateralResponse { pub total: Decimal, pub free: Decimal, @@ -701,7 +684,7 @@ impl From for UserCollateralResponse { } } -#[derive(serde::Serialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct IncomingSignedMessage { pub taker_authority: String, pub signature: String, diff --git a/src/controller.rs b/src/controller.rs index 26f4ee4..661d5cd 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -1,12 +1,8 @@ -use std::{ - borrow::Cow, - collections::HashSet, - str::FromStr, - sync::Arc, - time::{Duration, SystemTime}, -}; +use std::{borrow::Cow, collections::HashSet, str::FromStr, sync::Arc, time::Duration}; use base64::Engine as _; +#[cfg(not(test))] +use drift_rs::types::{ProgramError, RpcSendTransactionConfig}; use drift_rs::{ constants::ProgramData, drift_idl::{self, types::MarginRequirementType}, @@ -24,15 +20,12 @@ use drift_rs::{ slot_subscriber::SlotSubscriber, types::{ self, accounts::SpotMarket, MarketId, MarketType, ModifyOrderParams, OrderParams, - OrderStatus, ProgramError, RpcSendTransactionConfig, SdkError, SdkResult, VersionedMessage, + OrderStatus, SdkError, SdkResult, VersionedMessage, }, utils::get_http_url, DriftClient, Pubkey, TransactionBuilder, Wallet, }; -use futures_util::{ - stream::{FuturesOrdered, FuturesUnordered}, - FutureExt, StreamExt, -}; +use futures_util::{stream::FuturesOrdered, FutureExt, StreamExt}; use log::{debug, info, trace, warn}; use rust_decimal::Decimal; use sha256::digest; @@ -63,6 +56,7 @@ use crate::{ /// Default TTL in seconds of gateway tx retry /// after which gateway will no longer resubmit or monitor the tx // ~15 slots +#[cfg(not(test))] const DEFAULT_TX_TTL: u16 = 6; pub type GatewayResult = Result; @@ -88,10 +82,12 @@ pub struct AppState { /// sub_account_ids to subscribe to sub_account_ids: Vec, /// skip tx preflight on send or not (default: false) + #[allow(dead_code)] skip_tx_preflight: bool, priority_fee_subscriber: Arc, slot_subscriber: Arc, /// list of additional RPC endpoints for tx broadcast + #[allow(dead_code)] extra_rpcs: Vec>, /// swift node url swift_node: String, @@ -1011,14 +1007,14 @@ impl AppState { let tx_signature = sig; let extra_rpcs = self.extra_rpcs.clone(); tokio::spawn(async move { - let start = SystemTime::now(); + let start = std::time::SystemTime::now(); let ttl = Duration::from_secs(ttl.unwrap_or(DEFAULT_TX_TTL) as u64); let mut confirmed = false; - while SystemTime::now() + while std::time::SystemTime::now() .duration_since(start) .is_ok_and(|x| x < ttl) { - let mut futs = FuturesUnordered::new(); + let mut futs = futures_util::stream::FuturesUnordered::new(); for rpc in extra_rpcs.iter() { futs.push(rpc.send_transaction_with_config(&tx, tx_config)); } @@ -1052,6 +1048,7 @@ impl AppState { } } +#[cfg(not(test))] fn handle_tx_err(err: SdkError) -> ControllerError { if let Some(program_err) = err.to_anchor_error_code() { match program_err { diff --git a/src/main.rs b/src/main.rs index 4614ca5..3e15af2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,13 +8,14 @@ use actix_web::{ App, Either, HttpResponse, HttpServer, Responder, }; use argh::FromArgs; +use drift_gateway_types as types; +use drift_gateway_types::{SetLeverageRequest, SwapRequest}; use drift_rs::{ types::{CommitmentConfig, MarginRequirementType, MarketId}, GrpcSubscribeOpts, Pubkey, }; use log::{debug, info, warn}; use serde_json::json; -use types::{SetLeverageRequest, SwapRequest}; use crate::{ controller::{create_wallet, AppState, ControllerError}, @@ -24,7 +25,6 @@ use crate::{ }; mod controller; -mod types; mod websocket; pub const LOG_TARGET: &str = "gateway"; diff --git a/src/websocket.rs b/src/websocket.rs index 4cf9281..f6f5975 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -2,16 +2,18 @@ use std::{collections::HashMap, ops::Neg, sync::Arc}; +use drift_gateway_types::{ + AccountEvent, FundingPaymentEvent, OrderCancelEvent, OrderCancelMissingEvent, OrderCreateEvent, + OrderExpireEvent, OrderWithDecimals, SwapEvent, +}; use drift_rs::{ constants::ProgramData, event_subscriber::{DriftEvent, EventSubscriber, PubsubClient}, - types::{MarketType, Order, OrderType, PositionDirection}, Pubkey, Wallet, }; use futures_util::{SinkExt, StreamExt}; use log::{debug, info, warn}; use rust_decimal::Decimal; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::json; use tokio::{ net::{TcpListener, TcpStream}, @@ -21,7 +23,7 @@ use tokio::{ use tokio_tungstenite::{accept_async, tungstenite::Message}; use crate::{ - types::{get_market_decimals, Market, PRICE_DECIMALS}, + types::{get_market_decimals, Channel, Market, Method, WsEvent, WsRequest, PRICE_DECIMALS}, LOG_TARGET, }; @@ -199,308 +201,6 @@ async fn accept_connection( info!(target: LOG_TARGET, "closing Ws connection: {}", addr); } -#[derive(Deserialize, Debug)] -#[serde(rename_all = "lowercase")] -enum Method { - Subscribe, - Unsubscribe, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "lowercase")] -pub(crate) enum Channel { - Fills, - Orders, - Funding, - Swap, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct WsRequest { - method: Method, - sub_account_id: u8, -} - -#[derive(Serialize, Debug)] -#[serde(rename_all = "camelCase")] -struct WsEvent { - data: T, - channel: Channel, - sub_account_id: u8, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub(crate) enum AccountEvent { - #[serde(rename_all = "camelCase")] - Fill { - side: Side, - fee: Decimal, - amount: Decimal, - price: Decimal, - oracle_price: Decimal, - order_id: u32, - market_index: u16, - #[serde( - serialize_with = "crate::types::ser_market_type", - deserialize_with = "crate::types::de_market_type" - )] - market_type: MarketType, - ts: u64, - - /// The index of the event in the transaction - tx_idx: usize, - signature: String, - - maker: Option, - maker_order_id: Option, - maker_fee: Option, - taker: Option, - taker_order_id: Option, - taker_fee: Option, - }, - #[serde(rename_all = "camelCase")] - OrderCreate { - order: OrderWithDecimals, - ts: u64, - signature: String, - tx_idx: usize, - }, - #[serde(rename_all = "camelCase")] - OrderCancel { - order_id: u32, - ts: u64, - signature: String, - tx_idx: usize, - }, - #[serde(rename_all = "camelCase")] - OrderCancelMissing { - user_order_id: u8, - order_id: u32, - signature: String, - }, - #[serde(rename_all = "camelCase")] - OrderExpire { - order_id: u32, - fee: Decimal, - ts: u64, - signature: String, - }, - #[serde(rename_all = "camelCase")] - FundingPayment { - amount: Decimal, - market_index: u16, - ts: u64, - signature: String, - tx_idx: usize, - }, - #[serde(rename_all = "camelCase")] - Swap { - amount_in: Decimal, - amount_out: Decimal, - market_in: u16, - market_out: u16, - ts: u64, - tx_idx: usize, - signature: String, - }, - #[serde(rename_all = "camelCase")] - Trigger { order_id: u32, oracle_price: u64 }, -} - -impl AccountEvent { - fn fill( - side: PositionDirection, - fee: i64, - base_amount: u64, - quote_amount: u64, - oracle_price: i64, - order_id: u32, - ts: u64, - decimals: u32, - signature: &String, - tx_idx: usize, - market_index: u16, - market_type: MarketType, - maker: Option, - maker_order_id: Option, - maker_fee: Option, - taker: Option, - taker_order_id: Option, - taker_fee: Option, - ) -> Self { - let base_amount = Decimal::new(base_amount as i64, decimals); - let price = Decimal::new(quote_amount as i64, PRICE_DECIMALS) / base_amount; - AccountEvent::Fill { - side: if let PositionDirection::Long = side { - Side::Buy - } else { - Side::Sell - }, - price: price.normalize(), - oracle_price: Decimal::new(oracle_price, PRICE_DECIMALS).normalize(), - fee: Decimal::new(fee, PRICE_DECIMALS).normalize(), - order_id, - amount: base_amount.normalize(), - ts, - signature: signature.to_string(), - market_index, - market_type, - tx_idx, - maker, - maker_order_id, - maker_fee: maker_fee.map(|x| Decimal::new(x, PRICE_DECIMALS)), - taker, - taker_order_id, - taker_fee: taker_fee.map(|x| Decimal::new(x, PRICE_DECIMALS)), - } - } -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub(crate) enum Side { - Buy, - Sell, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub(crate) struct OrderWithDecimals { - /// The slot the order was placed - pub slot: u64, - /// The limit price for the order (can be 0 for market orders) - /// For orders with an auction, this price isn't used until the auction is complete - pub price: Decimal, - /// The size of the order - pub amount: Decimal, - /// The amount of the order filled - pub filled: Decimal, - /// At what price the order will be triggered. Only relevant for trigger orders - pub trigger_price: Decimal, - /// The start price for the auction. Only relevant for market/oracle orders - pub auction_start_price: Decimal, - /// The end price for the auction. Only relevant for market/oracle orders - pub auction_end_price: Decimal, - /// The time when the order will expire - pub max_ts: i64, - /// If set, the order limit price is the oracle price + this offset - pub oracle_price_offset: Decimal, - /// The id for the order. Each users has their own order id space - pub order_id: u32, - /// The perp/spot market index - pub market_index: u16, - /// The type of order - #[serde(serialize_with = "ser_order_type", deserialize_with = "de_order_type")] - pub order_type: OrderType, - /// Whether market is spot or perp - #[serde( - serialize_with = "crate::types::ser_market_type", - deserialize_with = "crate::types::de_market_type" - )] - pub market_type: MarketType, - /// User generated order id. Can make it easier to place/cancel orders - pub user_order_id: u8, - #[serde( - serialize_with = "ser_position_direction", - deserialize_with = "de_position_direction" - )] - pub direction: PositionDirection, - /// Whether the order is allowed to only reduce position size - pub reduce_only: bool, - /// Whether the order must be a maker - pub post_only: bool, - /// Whether the order must be canceled the same slot it is placed - pub immediate_or_cancel: bool, - /// How many slots the auction lasts - pub auction_duration: u8, -} - -impl OrderWithDecimals { - fn from_order(value: Order, decimals: u32) -> Self { - Self { - slot: value.slot, - price: Decimal::new(value.price as i64, PRICE_DECIMALS).normalize(), - amount: Decimal::new(value.base_asset_amount as i64, decimals).normalize(), - filled: Decimal::new(value.base_asset_amount_filled as i64, decimals).normalize(), - trigger_price: Decimal::new(value.trigger_price as i64, PRICE_DECIMALS).normalize(), - auction_start_price: Decimal::new(value.auction_start_price, PRICE_DECIMALS) - .normalize(), - auction_end_price: Decimal::new(value.auction_end_price, PRICE_DECIMALS).normalize(), - oracle_price_offset: Decimal::new(value.oracle_price_offset as i64, PRICE_DECIMALS) - .normalize(), - max_ts: value.max_ts, - order_id: value.order_id, - market_index: value.market_index, - order_type: value.order_type, - market_type: value.market_type, - user_order_id: value.user_order_id, - direction: value.direction, - reduce_only: value.reduce_only, - post_only: value.post_only, - immediate_or_cancel: value.immediate_or_cancel, - auction_duration: value.auction_duration, - } - } -} - -fn ser_order_type(x: &OrderType, s: S) -> Result -where - S: Serializer, -{ - s.serialize_str(match x { - OrderType::Limit => "limit", - OrderType::Market => "market", - OrderType::Oracle => "oracle", - OrderType::TriggerLimit => "triggerLimit", - OrderType::TriggerMarket => "triggerMarket", - }) -} - -fn de_order_type<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let s = String::deserialize(deserializer)?; - match s.as_str() { - "limit" => Ok(OrderType::Limit), - "market" => Ok(OrderType::Market), - "oracle" => Ok(OrderType::Oracle), - "triggerLimit" => Ok(OrderType::TriggerLimit), - "triggerMarket" => Ok(OrderType::TriggerMarket), - _ => Err(serde::de::Error::custom(format!( - "unknown order type: {}", - s - ))), - } -} - -fn ser_position_direction(x: &PositionDirection, s: S) -> Result -where - S: Serializer, -{ - s.serialize_str(match x { - PositionDirection::Long => "buy", - PositionDirection::Short => "sell", - }) -} - -fn de_position_direction<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let s = String::deserialize(deserializer)?; - match s.as_str() { - "buy" => Ok(PositionDirection::Long), - "sell" => Ok(PositionDirection::Short), - _ => Err(serde::de::Error::custom(format!( - "unknown position direction: {}", - s - ))), - } -} - /// Map drift-program events into gateway friendly types for events to the specific UserAccount pub(crate) fn map_drift_event_for_account( program_data: &ProgramData, @@ -605,12 +305,12 @@ pub(crate) fn map_drift_event_for_account( }; ( Channel::Orders, - Some(AccountEvent::OrderCancel { + Some(AccountEvent::OrderCancel(OrderCancelEvent { order_id: *order_id, ts: *ts, signature: signature.clone(), tx_idx: *tx_idx, - }), + })), ) } DriftEvent::OrderCancelMissing { @@ -619,11 +319,11 @@ pub(crate) fn map_drift_event_for_account( signature, } => ( Channel::Orders, - Some(AccountEvent::OrderCancelMissing { + Some(AccountEvent::OrderCancelMissing(OrderCancelMissingEvent { user_order_id: *user_order_id, order_id: *order_id, signature: signature.clone(), - }), + })), ), DriftEvent::OrderExpire { order_id, @@ -633,12 +333,12 @@ pub(crate) fn map_drift_event_for_account( .. } => ( Channel::Orders, - Some(AccountEvent::OrderExpire { + Some(AccountEvent::OrderExpire(OrderExpireEvent { order_id: *order_id, fee: Decimal::new((*fee as i64).neg(), PRICE_DECIMALS), ts: *ts, signature: signature.to_string(), - }), + })), ), DriftEvent::OrderCreate { order, @@ -653,12 +353,12 @@ pub(crate) fn map_drift_event_for_account( ); ( Channel::Orders, - Some(AccountEvent::OrderCreate { + Some(AccountEvent::OrderCreate(OrderCreateEvent { order: OrderWithDecimals::from_order(*order, decimals), ts: *ts, signature: signature.clone(), tx_idx: *tx_idx, - }), + })), ) } DriftEvent::FundingPayment { @@ -670,21 +370,21 @@ pub(crate) fn map_drift_event_for_account( .. } => ( Channel::Funding, - Some(AccountEvent::FundingPayment { + Some(AccountEvent::FundingPayment(FundingPaymentEvent { amount: Decimal::new(*amount, PRICE_DECIMALS).normalize(), market_index: *market_index, ts: *ts, signature: signature.clone(), tx_idx: *tx_idx, - }), + })), ), DriftEvent::Swap { - user, + user: _, amount_in, amount_out, market_in, market_out, - fee, + fee: _, ts, signature, tx_idx, @@ -693,7 +393,7 @@ pub(crate) fn map_drift_event_for_account( let decimals_out = get_market_decimals(program_data, Market::spot(*market_out)); ( Channel::Swap, - Some(AccountEvent::Swap { + Some(AccountEvent::Swap(SwapEvent { amount_in: Decimal::new(*amount_in as i64, decimals_in), amount_out: Decimal::new(*amount_out as i64, decimals_out), market_in: *market_in, @@ -701,7 +401,7 @@ pub(crate) fn map_drift_event_for_account( ts: *ts, tx_idx: *tx_idx, signature: signature.clone(), - }), + })), ) } }