use std::{borrow::Cow, fmt};
use crate::client::ExchangeResponse;
use crate::live::account::{
AllDexsState, AssetData, DexState, HistoricalOrders, Ledger, OpenOrders, OrderUpdate, SpotState, TwapHistory,
TwapSliceFills, TwapStates, UserEvent, UserFills, UserFundings, WebData3,
};
use crate::live::market::{Bbo, Book, Candle, Mids, PerpCtx, SpotCtx, Trade};
use crate::live::subscription::{BadSubscription, Subscription};
use crate::utils::Json;
use serde::Deserialize;
use serde_json::value::RawValue;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Frame {
Pong,
Ack(Ack),
Error(Refusal),
Post(Reply),
AllMids(Mids),
Book(Book),
Trades(Vec<Trade>),
Bbo(Bbo),
Candle(Candle),
AssetCtx {
coin: String,
ctx: PerpCtx,
},
SpotAssetCtx {
coin: String,
ctx: SpotCtx,
},
AssetData(AssetData),
UserFills(UserFills),
OrderUpdates(Vec<OrderUpdate>),
UserEvent(UserEvent),
UserFundings(UserFundings),
Ledger(Ledger),
ClearinghouseState(DexState),
AllDexsClearinghouseState(AllDexsState),
OpenOrders(OpenOrders),
SpotState(SpotState),
TwapStates(TwapStates),
TwapSliceFills(TwapSliceFills),
TwapHistory(TwapHistory),
Notification(String),
WebData3(WebData3),
AllDexsAssetCtxs(Vec<(String, Vec<PerpCtx>)>),
AssetCtxs {
dex: String,
ctxs: Vec<PerpCtx>,
},
SpotAssetCtxs(Vec<SpotCtx>),
OutcomeMetaUpdates(Json),
FastAssetCtxs(String),
HistoricalOrders(HistoricalOrders),
Other {
channel: String,
data: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Method {
Subscribe,
Unsubscribe,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ack {
pub method: Method,
pub echo: Json,
}
impl Ack {
pub fn subscription(&self) -> Result<Subscription, BadSubscription> {
Subscription::parse(&self.echo)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Refusal {
pub message: String,
}
impl Refusal {
#[must_use]
pub fn post_id(&self) -> Option<u64> {
if let Some((_, digits)) = self.message.rsplit_once("id=")
&& !digits.is_empty()
&& digits.bytes().all(|byte| byte.is_ascii_digit())
{
return digits.parse().ok();
}
let quoted = self.quoted()?;
match quoted.get("method").and_then(Json::as_str) {
Some("post") => quoted.get("id").and_then(Json::as_u64),
_ => None,
}
}
#[must_use]
pub fn subscription(&self) -> Option<Subscription> {
let quoted = self.quoted()?;
let subscription = match quoted.get("subscription") {
Some(inside) => inside,
None if ["Already subscribed", "Already unsubscribed", "Invalid subscription"]
.iter()
.any(|start| self.message.starts_with(start)) =>
{
"ed
}
None => return None,
};
Subscription::parse(subscription).ok()
}
fn quoted(&self) -> Option<Json> {
let at = self.message.find('{')?;
Json::parse(self.message.get(at..)?).ok()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reply {
pub id: u64,
pub answer: Answer,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Answer {
Info { kind: String, data: String },
Action(ExchangeResponse),
Error(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BadFrame {
pub channel: Option<String>,
pub detail: String,
}
impl fmt::Display for BadFrame {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.channel {
Some(channel) => write!(f, "a `{channel}` frame did not read: {}", self.detail),
None => write!(f, "not a frame: {}", self.detail),
}
}
}
impl std::error::Error for BadFrame {}
#[derive(Deserialize)]
struct Head<'a> {
#[serde(borrow)]
channel: Cow<'a, str>,
#[serde(default, borrow)]
data: Option<&'a RawValue>,
}
#[derive(Deserialize)]
struct RawAck<'a> {
method: Method,
#[serde(borrow)]
subscription: &'a RawValue,
}
#[derive(Deserialize)]
struct RawReply<'a> {
id: u64,
#[serde(borrow)]
response: RawResponse<'a>,
}
#[derive(Deserialize)]
struct RawResponse<'a> {
#[serde(rename = "type", borrow)]
kind: Cow<'a, str>,
#[serde(borrow)]
payload: &'a RawValue,
}
#[derive(Deserialize)]
struct RawInfo<'a> {
#[serde(rename = "type")]
kind: String,
#[serde(default, borrow)]
data: Option<&'a RawValue>,
}
#[derive(Deserialize)]
struct Active<C> {
coin: String,
ctx: C,
}
#[derive(Deserialize)]
struct DexCtxs {
dex: String,
ctxs: Vec<PerpCtx>,
}
#[derive(Deserialize)]
struct EveryDexCtxs {
ctxs: Vec<(String, Vec<PerpCtx>)>,
}
#[derive(Deserialize)]
struct Note {
notification: String,
}
pub fn decode(text: &str) -> Result<Frame, BadFrame> {
let head: Head<'_> = serde_json::from_str(text)
.map_err(|error| BadFrame { channel: None, detail: format!("{error} in {}", shown(text)) })?;
let channel = head.channel.as_ref();
let data = head.data.map(RawValue::get);
let typed = Typed { channel, data };
Ok(match channel {
"pong" => Frame::Pong,
"subscriptionResponse" => {
let ack: RawAck<'_> = typed.read()?;
let echo = Json::parse(ack.subscription.get()).map_err(|error| typed.fail(error.to_string()))?;
Frame::Ack(Ack { method: ack.method, echo })
}
"error" => Frame::Error(Refusal { message: typed.read()? }),
"post" => Frame::Post(typed.reply()?),
"allMids" => Frame::AllMids(typed.read()?),
"l2Book" => Frame::Book(typed.read()?),
"trades" => Frame::Trades(typed.read()?),
"bbo" => Frame::Bbo(typed.read()?),
"candle" => Frame::Candle(typed.read()?),
"activeAssetCtx" => {
let active: Active<PerpCtx> = typed.read()?;
Frame::AssetCtx { coin: active.coin, ctx: active.ctx }
}
"activeSpotAssetCtx" => {
let active: Active<SpotCtx> = typed.read()?;
Frame::SpotAssetCtx { coin: active.coin, ctx: active.ctx }
}
"activeAssetData" => Frame::AssetData(typed.read()?),
"userFills" => Frame::UserFills(typed.read()?),
"orderUpdates" => Frame::OrderUpdates(typed.read()?),
"user" => Frame::UserEvent(typed.read()?),
"userFundings" => Frame::UserFundings(typed.read()?),
"userNonFundingLedgerUpdates" => Frame::Ledger(typed.read()?),
"clearinghouseState" => Frame::ClearinghouseState(typed.read()?),
"allDexsClearinghouseState" => Frame::AllDexsClearinghouseState(typed.read()?),
"openOrders" => Frame::OpenOrders(typed.read()?),
"spotState" => Frame::SpotState(typed.read()?),
"twapStates" => Frame::TwapStates(typed.read()?),
"userTwapSliceFills" => Frame::TwapSliceFills(typed.read()?),
"userTwapHistory" => Frame::TwapHistory(typed.read()?),
"notification" => Frame::Notification(typed.read::<Note>()?.notification),
"webData3" => Frame::WebData3(typed.read()?),
"allDexsAssetCtxs" => Frame::AllDexsAssetCtxs(typed.read::<EveryDexCtxs>()?.ctxs),
"assetCtxs" => {
let dex: DexCtxs = typed.read()?;
Frame::AssetCtxs { dex: dex.dex, ctxs: dex.ctxs }
}
"spotAssetCtxs" => Frame::SpotAssetCtxs(typed.read()?),
"outcomeMetaUpdates" => {
Frame::OutcomeMetaUpdates(Json::parse(typed.data()?).map_err(|error| typed.fail(error.to_string()))?)
}
"fastAssetCtxs" => Frame::FastAssetCtxs(typed.read()?),
"userHistoricalOrders" => Frame::HistoricalOrders(typed.read()?),
other => Frame::Other { channel: other.to_owned(), data: data.unwrap_or_default().to_owned() },
})
}
struct Typed<'a> {
channel: &'a str,
data: Option<&'a str>,
}
impl<'a> Typed<'a> {
fn fail(&self, detail: String) -> BadFrame {
BadFrame { channel: Some(self.channel.to_owned()), detail }
}
fn data(&self) -> Result<&'a str, BadFrame> {
self.data.ok_or_else(|| self.fail("no data".to_owned()))
}
fn read<T: Deserialize<'a>>(&self) -> Result<T, BadFrame> {
let data = self.data()?;
serde_json::from_str(data).map_err(|error| self.fail(format!("{error} in {}", shown(data))))
}
fn reply(&self) -> Result<Reply, BadFrame> {
let data = self.data()?;
let reply: RawReply<'_> =
serde_json::from_str(data).map_err(|error| self.fail(format!("{error} in {}", shown(data))))?;
let payload = reply.response.payload.get();
let answer = match reply.response.kind.as_ref() {
"info" => {
let info: RawInfo<'_> = serde_json::from_str(payload).map_err(|error| self.fail(error.to_string()))?;
Answer::Info { kind: info.kind, data: info.data.map_or("null", RawValue::get).to_owned() }
}
"action" => Answer::Action(ExchangeResponse::parse(payload).map_err(|error| self.fail(error.to_string()))?),
"error" => Answer::Error(serde_json::from_str(payload).map_err(|error| self.fail(error.to_string()))?),
other => return Err(self.fail(format!("a post answered as `{other}`"))),
};
Ok(Reply { id: reply.id, answer })
}
}
fn shown(text: &str) -> String {
let start: String = text.chars().take(160).collect();
match start.len() < text.len() {
true => format!("{start}..."),
false => start,
}
}
#[cfg(test)]
#[path = "tests/frame.rs"]
mod tests;