use pulse_sdk::prelude::*; use rust_decimal::Decimal; use serde_json::Value; use std::collections::HashMap; use crate::engine::WatchList; fn number(value: &Value, field: &str) -> Result { let raw = value[field] .as_str() .ok_or_else(|| format!("asset context field {field} must be a string"))?; raw.parse::() .map_err(|error| format!("could not parse asset context field {field} ({raw}): {error}")) } pub async fn fetch_watch_list( client: &hypersdk::hypercore::HttpClient, symbols: &[String], ) -> Result { let response = client .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() )); } // Build symbol -> perp asset index let mut name_to_index = HashMap::with_capacity(universe.len()); for (index, meta) in universe.iter().enumerate() { let symbol = meta["name"] .as_str() .ok_or("instrument metadata is missing a name")?; name_to_index.insert(symbol.to_owned(), index); } // Now only process requested symbols let mut items = Vec::with_capacity(symbols.len()); for symbol in symbols { let index = *name_to_index .get(symbol) .ok_or_else(|| format!("{symbol} is not in the Hyperliquid perpetual universe"))?; let context = &contexts[index]; let price = number(context, "markPx")?; let previous_day_price = number(context, "prevDayPx")?; let volume_24h = number(context, "dayNtlVlm")?; if previous_day_price.is_zero() || previous_day_price.is_sign_negative() { return Err(format!("{symbol} has an invalid previous-day price")); } items.push(MarketItem { symbol: symbol.clone(), price: price, volume_24h: volume_24h, trend: ((price / previous_day_price) - >::from(1)) * >::from(100), }); } Ok(WatchList { items, name_to_index, }) }