use serde::{Deserialize, Serialize};
use crate::wallet::Address;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Subscription {
L2Book {
coin: String,
},
Trades {
coin: String,
},
Bbo {
coin: String,
},
ActiveAssetCtx {
coin: String,
},
Candles {
coin: String,
interval: String,
},
AllMids,
Fills {
user: Address,
},
UserEvents {
user: Address,
},
OrderUpdates {
user: Address,
},
Notifications {
user: Address,
},
LedgerUpdates {
user: Address,
},
UserFundings {
user: Address,
},
UserTwapSliceFills {
user: Address,
},
UserTwapHistory {
user: Address,
},
AccountState {
user: Address,
},
SpotState {
user: Address,
},
ActiveAssetData {
coin: String,
user: Address,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "channel", content = "data", rename_all = "snake_case")]
pub enum WsMessage {
#[serde(rename = "subscriptionResponse")]
SubscriptionResponse {
method: String,
subscription: Subscription,
},
Error {
error: String,
},
L2Book(serde_json::Value),
Trades(serde_json::Value),
Bbo(serde_json::Value),
ActiveAssetCtx(serde_json::Value),
Candles(serde_json::Value),
AllMids(serde_json::Value),
Fills(serde_json::Value),
UserEvents(serde_json::Value),
OrderUpdates(serde_json::Value),
Notifications(serde_json::Value),
LedgerUpdates(serde_json::Value),
UserFundings(serde_json::Value),
UserTwapSliceFills(serde_json::Value),
UserTwapHistory(serde_json::Value),
AccountState(serde_json::Value),
SpotState(serde_json::Value),
ActiveAssetData(serde_json::Value),
Pong,
#[serde(other)]
Unknown,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subscription_l2_book_uses_coin_string() {
let s = Subscription::L2Book { coin: "1".into() };
let j = serde_json::to_value(&s).unwrap();
assert_eq!(j["type"], "l2_book");
assert_eq!(j["coin"], "1");
assert!(j["coin"].is_string());
assert!(j.get("market_id").is_none());
}
#[test]
fn subscription_account_channel_uses_user_address() {
let s = Subscription::Fills {
user: Address::ZERO,
};
let j = serde_json::to_value(&s).unwrap();
assert_eq!(j["type"], "fills");
assert!(j["user"].is_string());
assert!(j["user"].as_str().unwrap().starts_with("0x"));
}
#[test]
fn subscription_candles_carries_coin_and_interval() {
let s = Subscription::Candles {
coin: "7".into(),
interval: "5m".into(),
};
let j = serde_json::to_value(&s).unwrap();
assert_eq!(j["type"], "candles");
assert_eq!(j["coin"], "7");
assert_eq!(j["interval"], "5m");
}
#[test]
fn subscription_all_mids_is_bare_type() {
let j = serde_json::to_value(Subscription::AllMids).unwrap();
assert_eq!(j["type"], "all_mids");
assert!(j.get("coin").is_none() && j.get("user").is_none());
}
#[test]
fn subscription_active_asset_data_carries_coin_and_user() {
let s = Subscription::ActiveAssetData {
coin: "2".into(),
user: Address::ZERO,
};
let j = serde_json::to_value(&s).unwrap();
assert_eq!(j["type"], "active_asset_data");
assert_eq!(j["coin"], "2");
assert!(j["user"].is_string());
}
#[test]
fn ws_message_decodes_subscription_response_camel_channel() {
let raw = serde_json::json!({
"channel": "subscriptionResponse",
"data": {
"method": "subscribe",
"subscription": { "type": "l2_book", "coin": "1" }
}
});
let m: WsMessage = serde_json::from_value(raw).unwrap();
match m {
WsMessage::SubscriptionResponse {
method,
subscription,
} => {
assert_eq!(method, "subscribe");
assert!(matches!(subscription, Subscription::L2Book { .. }));
}
other => panic!("expected SubscriptionResponse, got {other:?}"),
}
}
#[test]
fn ws_message_decodes_error_with_error_field() {
let raw = serde_json::json!({ "channel": "error", "data": { "error": "bad channel" } });
let m: WsMessage = serde_json::from_value(raw).unwrap();
match m {
WsMessage::Error { error } => assert_eq!(error, "bad channel"),
other => panic!("expected Error, got {other:?}"),
}
}
#[test]
fn ws_message_decodes_bare_pong() {
let raw = serde_json::json!({ "channel": "pong" });
let m: WsMessage = serde_json::from_value(raw).unwrap();
assert!(matches!(m, WsMessage::Pong));
}
#[test]
fn ws_message_decodes_data_channels() {
for chan in [
"l2_book",
"trades",
"bbo",
"active_asset_ctx",
"all_mids",
"fills",
"order_updates",
"account_state",
] {
let raw = serde_json::json!({ "channel": chan, "data": { "x": 1 } });
let m: WsMessage = serde_json::from_value(raw)
.unwrap_or_else(|e| panic!("channel {chan} should decode: {e}"));
assert!(
!matches!(m, WsMessage::Unknown),
"channel {chan} fell through to Unknown"
);
}
}
#[test]
fn ws_message_unknown_channel_without_data_is_unknown() {
let raw = serde_json::json!({ "channel": "definitely_not_real" });
let m: WsMessage = serde_json::from_value(raw).unwrap();
assert!(matches!(m, WsMessage::Unknown));
}
#[test]
fn ws_message_unknown_channel_with_data_fails_decode() {
let raw = serde_json::json!({ "channel": "definitely_not_real", "data": { "x": 1 } });
assert!(serde_json::from_value::<WsMessage>(raw).is_err());
}
}