Skip to content

Commit 07acb5c

Browse files
HathorNodesclaude
andcommitted
feat: derive Serialize on UserData + UserFundings + constituents (Hathor #215)
The Hathor Rust LiveExecutor's info_bridge::msg_to_value translates SDK WSS Message variants to serde_json::Value envelopes that downstream callbacks consume. UserData (fills) and UserFundings need to round-trip through serde_json so the trailing-stop + partial-TP logic in Tick 3 of hyperliquid-dex#171 can read fill data from the bridge envelope. Refs: SekhmetsWrath/hathor-nodes-bot#215, SekhmetsWrath/hathor-nodes-bot#171 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d05a532 commit 07acb5c

2 files changed

Lines changed: 97 additions & 10 deletions

File tree

src/ws/message_types.rs

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use serde::Deserialize;
1+
use serde::{Deserialize, Serialize};
22

33
use crate::ws::sub_structs::*;
44

@@ -17,7 +17,7 @@ pub struct AllMids {
1717
pub data: AllMidsData,
1818
}
1919

20-
#[derive(Deserialize, Clone, Debug)]
20+
#[derive(Serialize, Deserialize, Clone, Debug)]
2121
pub struct User {
2222
pub data: UserData,
2323
}
@@ -37,7 +37,7 @@ pub struct OrderUpdates {
3737
pub data: Vec<OrderUpdate>,
3838
}
3939

40-
#[derive(Deserialize, Clone, Debug)]
40+
#[derive(Serialize, Deserialize, Clone, Debug)]
4141
pub struct UserFundings {
4242
pub data: UserFundingsData,
4343
}
@@ -76,3 +76,90 @@ pub struct ActiveAssetData {
7676
pub struct Bbo {
7777
pub data: BboData,
7878
}
79+
80+
#[cfg(test)]
81+
mod tests {
82+
//! Round-trip tests for the `User` and `UserFundings` WSS message
83+
//! wrappers. The Hathor `InfoBridge::msg_to_value` translates these
84+
//! into `serde_json::Value` envelopes for downstream callbacks; the
85+
//! trailing-stop / partial-TP logic depends on the inner `fills` and
86+
//! `fundings` payloads reaching the callback (Hathor #215).
87+
use super::*;
88+
use crate::ws::sub_structs::{TradeInfo, UserData, UserFunding, UserFundingsData};
89+
use alloy::primitives::Address;
90+
91+
fn sample_address() -> Address {
92+
"0x0000000000000000000000000000000000000001"
93+
.parse()
94+
.expect("static valid address")
95+
}
96+
97+
fn sample_trade_info() -> TradeInfo {
98+
TradeInfo {
99+
coin: "BTC".to_string(),
100+
side: "B".to_string(),
101+
px: "50000.0".to_string(),
102+
sz: "0.01".to_string(),
103+
time: 1_700_000_000_000,
104+
hash: "0xabc".to_string(),
105+
start_position: "0.0".to_string(),
106+
dir: "Open Long".to_string(),
107+
closed_pnl: "0.0".to_string(),
108+
oid: 42,
109+
cloid: None,
110+
crossed: true,
111+
fee: "0.5".to_string(),
112+
fee_token: "USDC".to_string(),
113+
tid: 7,
114+
}
115+
}
116+
117+
#[test]
118+
fn user_data_round_trips_with_fills() {
119+
let user = User {
120+
data: UserData::Fills(vec![sample_trade_info()]),
121+
};
122+
let value = serde_json::to_value(&user).expect("serialize User");
123+
124+
// Wrapper serialises as `{"data": {"fills": [...]}}` because
125+
// `UserData` is an internally-tagged enum with `Fills` variant
126+
// becoming `{"fills": [...]}` under serde's default enum repr.
127+
let fills = value
128+
.pointer("/data/fills")
129+
.and_then(|v| v.as_array())
130+
.expect("fills array present at /data/fills");
131+
assert_eq!(fills.len(), 1, "one fill expected");
132+
assert_eq!(fills[0]["coin"], "BTC");
133+
assert_eq!(fills[0]["px"], "50000.0");
134+
assert_eq!(fills[0]["side"], "B");
135+
assert_eq!(fills[0]["oid"], 42);
136+
assert_eq!(fills[0]["tid"], 7);
137+
}
138+
139+
#[test]
140+
fn user_fundings_round_trips() {
141+
let fundings = UserFundings {
142+
data: UserFundingsData {
143+
is_snapshot: Some(true),
144+
user: sample_address(),
145+
fundings: vec![UserFunding {
146+
time: 1_700_000_000_000,
147+
coin: "BTC".to_string(),
148+
usdc: "1.5".to_string(),
149+
szi: "0.01".to_string(),
150+
funding_rate: "0.0001".to_string(),
151+
}],
152+
},
153+
};
154+
let value = serde_json::to_value(&fundings).expect("serialize UserFundings");
155+
156+
let inner = value
157+
.pointer("/data/fundings")
158+
.and_then(|v| v.as_array())
159+
.expect("fundings array present at /data/fundings");
160+
assert_eq!(inner.len(), 1);
161+
assert_eq!(inner[0]["coin"], "BTC");
162+
assert_eq!(inner[0]["usdc"], "1.5");
163+
assert_eq!(inner[0]["fundingRate"], "0.0001");
164+
}
165+
}

src/ws/sub_structs.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub struct AllMidsData {
3636
pub mids: HashMap<String, String>,
3737
}
3838

39-
#[derive(Deserialize, Clone, Debug)]
39+
#[derive(Serialize, Deserialize, Clone, Debug)]
4040
#[serde(rename_all = "camelCase")]
4141
pub struct TradeInfo {
4242
pub coin: String,
@@ -56,15 +56,15 @@ pub struct TradeInfo {
5656
pub tid: u64,
5757
}
5858

59-
#[derive(Deserialize, Clone, Debug)]
59+
#[derive(Serialize, Deserialize, Clone, Debug)]
6060
#[serde(rename_all = "camelCase")]
6161
pub struct UserFillsData {
6262
pub is_snapshot: Option<bool>,
6363
pub user: Address,
6464
pub fills: Vec<TradeInfo>,
6565
}
6666

67-
#[derive(Deserialize, Clone, Debug)]
67+
#[derive(Serialize, Deserialize, Clone, Debug)]
6868
#[serde(rename_all = "camelCase")]
6969
pub enum UserData {
7070
Fills(Vec<TradeInfo>),
@@ -73,7 +73,7 @@ pub enum UserData {
7373
NonUserCancel(Vec<NonUserCancel>),
7474
}
7575

76-
#[derive(Deserialize, Clone, Debug)]
76+
#[derive(Serialize, Deserialize, Clone, Debug)]
7777
pub struct Liquidation {
7878
pub lid: u64,
7979
pub liquidator: String,
@@ -82,7 +82,7 @@ pub struct Liquidation {
8282
pub liquidated_account_value: String,
8383
}
8484

85-
#[derive(Deserialize, Clone, Debug)]
85+
#[derive(Serialize, Deserialize, Clone, Debug)]
8686
pub struct NonUserCancel {
8787
pub coin: String,
8888
pub oid: u64,
@@ -133,15 +133,15 @@ pub struct BasicOrder {
133133
pub cloid: Option<String>,
134134
}
135135

136-
#[derive(Deserialize, Clone, Debug)]
136+
#[derive(Serialize, Deserialize, Clone, Debug)]
137137
#[serde(rename_all = "camelCase")]
138138
pub struct UserFundingsData {
139139
pub is_snapshot: Option<bool>,
140140
pub user: Address,
141141
pub fundings: Vec<UserFunding>,
142142
}
143143

144-
#[derive(Deserialize, Clone, Debug)]
144+
#[derive(Serialize, Deserialize, Clone, Debug)]
145145
#[serde(rename_all = "camelCase")]
146146
pub struct UserFunding {
147147
pub time: u64,

0 commit comments

Comments
 (0)