Ember /api/live: the node lane's shape (timestamp_source passed through, header by default; the -32601 "method ... not found" message is the not-carried signal); tests

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-06 19:25:17 +00:00
parent 0276163301
commit f9c4ac3701

View file

@ -36,6 +36,9 @@ pub struct RecentBlock {
pub vote_key_hash: String,
pub is_chain_block: bool,
pub color: String,
/// "" or "merging_block" (the node lane: a merged non-chain block whose own header time was not at hand
/// carries its merging chain block's time, marked so blocks_per_minute stays honest)
pub timestamp_source: String,
}
pub fn parse_recent(v: &Value) -> Vec<RecentBlock> {
@ -44,7 +47,7 @@ pub fn parse_recent(v: &Value) -> Vec<RecentBlock> {
v.as_array()
.map(|a| {
a.iter()
.map(|b| RecentBlock { hash: s(b, "hash"), blue_score: n(b, "blue_score"), daa_score: n(b, "daa_score"), timestamp_ms: n(b, "timestamp_ms"), vote_key_hash: s(b, "vote_key_hash"), is_chain_block: b.get("is_chain_block").and_then(|x| x.as_bool()).unwrap_or(false), color: s(b, "color") })
.map(|b| RecentBlock { hash: s(b, "hash"), blue_score: n(b, "blue_score"), daa_score: n(b, "daa_score"), timestamp_ms: n(b, "timestamp_ms"), vote_key_hash: s(b, "vote_key_hash"), is_chain_block: b.get("is_chain_block").and_then(|x| x.as_bool()).unwrap_or(false), color: s(b, "color"), timestamp_source: s(b, "timestamp_source") })
.collect()
})
.unwrap_or_default()
@ -71,7 +74,7 @@ pub fn shape_from_blocks(blocks: &[RecentBlock], now_ms: u64, window_s: u64) ->
let rows: Vec<Value> = blocks
.iter()
.filter(|b| b.timestamp_ms >= win)
.map(|b| json!({ "hash": short(&b.hash), "blue_score": b.blue_score, "daa": b.daa_score, "timestamp_ms": b.timestamp_ms, "is_chain_block": b.is_chain_block, "color": if b.color.is_empty() { "pending" } else { b.color.as_str() }, "vote_key_hash": short(&b.vote_key_hash) }))
.map(|b| json!({ "hash": short(&b.hash), "blue_score": b.blue_score, "daa": b.daa_score, "timestamp_ms": b.timestamp_ms, "is_chain_block": b.is_chain_block, "color": if b.color.is_empty() { "pending" } else { b.color.as_str() }, "vote_key_hash": short(&b.vote_key_hash), "timestamp_source": if b.timestamp_source.is_empty() { "header" } else { b.timestamp_source.as_str() } }))
.collect();
json!({
"miners_10m": miners.len(),
@ -165,7 +168,8 @@ fn from_node(shared: &std::sync::Arc<Shared>) -> Option<Value> {
}
Ok(_) => None,
Err(e) => {
if e.contains("not found") || e.contains("-32601") || e.contains("Method") {
// the node's default arm: {"code": -32601, "message": "method igneum_getRecentBlocks not found"}
if e.contains("not found") || e.contains("-32601") {
cache().lock().unwrap().node_retry = Some(Instant::now() + Duration::from_secs(600));
}
None
@ -188,7 +192,7 @@ mod tests {
use super::*;
fn b(ts: u64, key: &str, chain: bool) -> RecentBlock {
RecentBlock { hash: format!("0x{:064x}", ts), blue_score: ts / 1000, daa_score: ts / 1000, timestamp_ms: ts, vote_key_hash: key.into(), is_chain_block: chain, color: if chain { "blue".into() } else { String::new() } }
RecentBlock { hash: format!("0x{:064x}", ts), blue_score: ts / 1000, daa_score: ts / 1000, timestamp_ms: ts, vote_key_hash: key.into(), is_chain_block: chain, color: if chain { "blue".into() } else { String::new() }, timestamp_source: String::new() }
}
#[test]
@ -206,14 +210,16 @@ mod tests {
assert_eq!(v["blocks"].as_array().unwrap().len(), 3, "the 120 s window");
assert_eq!(v["blocks"][2]["color"], "pending", "an unmerged block without a colour");
assert_eq!(v["blocks"][0]["color"], "blue");
assert_eq!(v["blocks"][0]["timestamp_source"], "header");
assert_eq!(v["miners"].as_array().unwrap().len(), 3);
}
#[test]
fn recent_blocks_parse_from_the_node_reply_and_an_empty_reply_is_empty() {
let v: Value = serde_json::from_str(r#"[{"hash":"0xab","blue_score":5,"daa_score":6,"timestamp_ms":7,"vote_key_hash":"cd","is_chain_block":true,"color":"blue"}]"#).unwrap();
let v: Value = serde_json::from_str(r#"[{"hash":"0xab","blue_score":5,"daa_score":6,"timestamp_ms":7,"vote_key_hash":"cd","is_chain_block":true,"color":"blue"},{"hash":"0xcd","blue_score":5,"daa_score":6,"timestamp_ms":7,"vote_key_hash":"ef","is_chain_block":false,"color":"red","timestamp_source":"merging_block"}]"#).unwrap();
let p = parse_recent(&v);
assert_eq!(p.len(), 1);
assert_eq!(p.len(), 2);
assert_eq!((p[1].color.as_str(), p[1].timestamp_source.as_str()), ("red", "merging_block"));
assert_eq!((p[0].blue_score, p[0].is_chain_block, p[0].color.as_str()), (5, true, "blue"));
assert!(parse_recent(&json!(null)).is_empty());
assert_eq!(short("0x1234567890abcdef"), "123456..abcdef");