pub trait WsTransport {
type Inbound;
type Outbound;
type Error: std::fmt::Debug;
fn connect(&mut self) -> impl std::future::Future<Output = Result<(), Self::Error>>;
fn send(
&mut self,
msg: Self::Outbound,
) -> impl std::future::Future<Output = Result<(), Self::Error>>;
fn recv(
&mut self,
) -> impl std::future::Future<Output = Option<Result<Self::Inbound, Self::Error>>>;
fn close(&mut self) -> impl std::future::Future<Output = Result<(), Self::Error>>;
fn is_connected(&self) -> bool;
}
use crate::ws::inbound::HyperliquidWsInboundMessage;
use crate::ws::outbound::HyperliquidWsOutboundMessage;
use futures_util::{SinkExt, StreamExt};
use tokio_tungstenite::{connect_async, tungstenite::Message};
const HYPERLIQUID_WS_URL: &str = "wss://api.hyperliquid.xyz/ws";
type TungsteniteStream =
tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
pub(crate) struct HyperliquidWs {
url: String,
stream: Option<TungsteniteStream>,
closed: bool,
}
impl HyperliquidWs {
pub(crate) fn new() -> Self {
Self {
url: HYPERLIQUID_WS_URL.to_string(),
stream: None,
closed: false,
}
}
}
#[derive(Debug)]
pub(crate) enum WsError {
NotConnected,
Io(String),
Parse(String),
Closed,
}
impl std::fmt::Display for WsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WsError::NotConnected => write!(f, "websocket not connected"),
WsError::Io(e) => write!(f, "websocket I/O error: {e}"),
WsError::Parse(e) => write!(f, "websocket parse error: {e}"),
WsError::Closed => write!(f, "websocket connection closed"),
}
}
}
impl std::error::Error for WsError {}
impl WsTransport for HyperliquidWs {
type Inbound = HyperliquidWsInboundMessage;
type Outbound = HyperliquidWsOutboundMessage;
type Error = WsError;
async fn connect(&mut self) -> Result<(), Self::Error> {
let (ws, _) = connect_async(&self.url)
.await
.map_err(|e| WsError::Io(e.to_string()))?;
self.stream = Some(ws);
self.closed = false;
Ok(())
}
async fn send(&mut self, msg: Self::Outbound) -> Result<(), Self::Error> {
let stream = self.stream.as_mut().ok_or(WsError::NotConnected)?;
let json = msg.to_json();
stream
.send(Message::Text(json.into()))
.await
.map_err(|e| WsError::Io(e.to_string()))
}
async fn recv(&mut self) -> Option<Result<Self::Inbound, Self::Error>> {
let stream = self.stream.as_mut()?;
loop {
match stream.next().await {
None => {
self.closed = true;
return Some(Err(WsError::Closed));
}
Some(Err(e)) => {
self.closed = true;
return Some(Err(WsError::Io(e.to_string())));
}
Some(Ok(Message::Text(text))) => {
let Ok(env) =
serde_json::from_str::<crate::ws::inbound::HyperliquidWsEnvelope>(&text)
else {
return Some(Err(WsError::Parse(format!(
"invalid envelope: {text:.100}"
))));
};
match HyperliquidWsInboundMessage::try_from(env) {
Ok(msg) => return Some(Ok(msg)),
Err(e) => return Some(Err(WsError::Parse(e.to_string()))),
}
}
Some(Ok(Message::Ping(_))) => {
}
Some(Ok(Message::Pong(_))) => {}
Some(Ok(Message::Close(_))) => {
self.closed = true;
return Some(Err(WsError::Closed));
}
Some(Ok(Message::Binary(_))) => {
}
Some(Ok(Message::Frame(_))) => {}
}
}
}
async fn close(&mut self) -> Result<(), Self::Error> {
if let Some(stream) = self.stream.as_mut() {
stream
.close(None)
.await
.map_err(|e| WsError::Io(e.to_string()))
} else {
Ok(())
}
}
fn is_connected(&self) -> bool {
self.stream.is_some() && !self.closed
}
}