Separated fetch

This commit is contained in:
2026-07-24 01:30:18 +02:00
parent 7b0f80864b
commit 41804c8fd2
3 changed files with 82 additions and 86 deletions
+1 -86
View File
@@ -1,8 +1,3 @@
use std::collections::HashMap;
use pulse_wire::terminal::WatchListItem;
use serde_json::Value;
use crate::terminal::TerminalServer; use crate::terminal::TerminalServer;
const WATCH_LIST_SYMBOLS: &[&str] = &["BTC", "ETH", "SOL", "XRP"]; const WATCH_LIST_SYMBOLS: &[&str] = &["BTC", "ETH", "SOL", "XRP"];
@@ -38,7 +33,7 @@ impl Engine {
loop { loop {
refresh.tick().await; refresh.tick().await;
match fetch_watch_list(WATCH_LIST_SYMBOLS).await { match crate::fetch::fetch_watch_list(WATCH_LIST_SYMBOLS).await {
Ok(watch_list) => { Ok(watch_list) => {
if let Err(error) = terminal_server if let Err(error) = terminal_server
.broadcast( .broadcast(
@@ -63,83 +58,3 @@ impl Engine {
} }
} }
} }
async fn fetch_watch_list(symbols: &[&str]) -> Result<Vec<WatchListItem>, String> {
let response = hypersdk::hypercore::mainnet()
.meta_and_asset_ctxs(None)
.await
.map_err(|error| format!("Hyperliquid metaAndAssetCtxs request failed: {error}"))?;
normalize_watch_list(&response, symbols)
}
fn normalize_watch_list(response: &Value, symbols: &[&str]) -> Result<Vec<WatchListItem>, String> {
let response = response
.as_array()
.ok_or("metaAndAssetCtxs response must be an array")?;
if response.len() != 2 {
return Err(format!(
"metaAndAssetCtxs response must contain meta and contexts, got {} entries",
response.len()
));
}
let universe = response[0]["universe"]
.as_array()
.ok_or("metaAndAssetCtxs response is missing meta.universe")?;
let contexts = response[1]
.as_array()
.ok_or("metaAndAssetCtxs response contexts must be an array")?;
if universe.len() != contexts.len() {
return Err(format!(
"Hyperliquid returned {} instruments but {} contexts",
universe.len(),
contexts.len()
));
}
let mut by_symbol = HashMap::with_capacity(universe.len());
for (meta, context) in universe.iter().zip(contexts) {
let symbol = meta["name"]
.as_str()
.ok_or("instrument metadata is missing a name")?;
let price = number(context, "markPx")?;
let previous_day_price = number(context, "prevDayPx")?;
let volume_24h = number(context, "dayNtlVlm")?;
if previous_day_price <= 0.0 {
return Err(format!("{symbol} has an invalid previous-day price"));
}
by_symbol.insert(
symbol,
WatchListItem {
symbol: symbol.to_owned(),
price,
trend: ((price / previous_day_price) - 1.0) * 100.0,
volume_24h,
},
);
}
symbols
.iter()
.map(|symbol| {
by_symbol
.remove(*symbol)
.ok_or_else(|| format!("{symbol} is not in the Hyperliquid perpetual universe"))
})
.collect()
}
fn number(value: &Value, field: &str) -> Result<f64, String> {
let raw = value[field]
.as_str()
.ok_or_else(|| format!("asset context field {field} must be a string"))?;
raw.parse::<f64>()
.map_err(|error| format!("could not parse asset context field {field} ({raw}): {error}"))
}
+80
View File
@@ -0,0 +1,80 @@
use std::collections::HashMap;
use pulse_wire::terminal::WatchListItem;
use serde_json::Value;
fn number(value: &Value, field: &str) -> Result<f64, String> {
let raw = value[field]
.as_str()
.ok_or_else(|| format!("asset context field {field} must be a string"))?;
raw.parse::<f64>()
.map_err(|error| format!("could not parse asset context field {field} ({raw}): {error}"))
}
pub async fn fetch_watch_list(symbols: &[&str]) -> Result<Vec<WatchListItem>, String> {
let response = hypersdk::hypercore::mainnet()
.meta_and_asset_ctxs(None)
.await
.map_err(|error| format!("Hyperliquid metaAndAssetCtxs request failed: {error}"))?;
let response = response
.as_array()
.ok_or("metaAndAssetCtxs response must be an array")?;
if response.len() != 2 {
return Err(format!(
"metaAndAssetCtxs response must contain meta and contexts, got {} entries",
response.len()
));
}
let universe = response[0]["universe"]
.as_array()
.ok_or("metaAndAssetCtxs response is missing meta.universe")?;
let contexts = response[1]
.as_array()
.ok_or("metaAndAssetCtxs response contexts must be an array")?;
if universe.len() != contexts.len() {
return Err(format!(
"Hyperliquid returned {} instruments but {} contexts",
universe.len(),
contexts.len()
));
}
let mut by_symbol = HashMap::with_capacity(universe.len());
for (meta, context) in universe.iter().zip(contexts) {
let symbol = meta["name"]
.as_str()
.ok_or("instrument metadata is missing a name")?;
let price = number(context, "markPx")?;
let previous_day_price = number(context, "prevDayPx")?;
let volume_24h = number(context, "dayNtlVlm")?;
if previous_day_price <= 0.0 {
return Err(format!("{symbol} has an invalid previous-day price"));
}
by_symbol.insert(
symbol,
WatchListItem {
symbol: symbol.to_owned(),
price,
trend: ((price / previous_day_price) - 1.0) * 100.0,
volume_24h,
},
);
}
symbols
.iter()
.map(|symbol| {
by_symbol
.remove(*symbol)
.ok_or_else(|| format!("{symbol} is not in the Hyperliquid perpetual universe"))
})
.collect()
}
+1
View File
@@ -1,4 +1,5 @@
pub mod engine; pub mod engine;
pub mod fetch;
pub mod terminal; pub mod terminal;
#[tokio::main] #[tokio::main]