Add member directory and aggregates, cut citation economics from spec (Draft 0.4)

This commit is contained in:
George Coles
2026-09-15 03:56:11 -04:00
parent 82a95fb907
commit fb45cfa8cf
13 changed files with 890 additions and 79 deletions
+95 -2
View File
@@ -1,8 +1,9 @@
use std::path::Path;
use std::time::Duration;
use anyhow::{Context, Result};
use anyhow::{Context, Result, anyhow};
use crate::config::Config;
use crate::config::{CLASS_ENRICHMENT, CLASS_SOURCE, Config, Member, load_members, save_members};
use crate::index::{Collection, LocalIndex, load_collections, save_collections};
use crate::message::{EXPOSURE_FULL, EXPOSURE_METADATA};
use crate::node;
@@ -91,6 +92,98 @@ pub async fn query(
Ok(())
}
pub fn member_add(config_path: &Path, name: &str, pubkey: &str, class: &str) -> Result<()> {
let config = Config::load(config_path)?;
let bytes = hex::decode(pubkey).context("pubkey must be hex")?;
if bytes.len() != 32 {
return Err(anyhow!("pubkey must be 32 bytes (64 hex characters)"));
}
let class = if class == CLASS_ENRICHMENT {
CLASS_ENRICHMENT
} else {
CLASS_SOURCE
};
let mut members = load_members(&config.members_path())?;
members.retain(|member| member.name != name && member.pubkey != pubkey);
members.push(Member {
name: name.to_string(),
pubkey: pubkey.to_ascii_lowercase(),
class: class.to_string(),
});
save_members(&config.members_path(), &members)?;
println!(
"listed {name} ({class}) in {}",
config.members_path().display()
);
Ok(())
}
pub fn member_remove(config_path: &Path, name: &str) -> Result<()> {
let config = Config::load(config_path)?;
let mut members = load_members(&config.members_path())?;
let before = members.len();
members.retain(|member| member.name != name);
if members.len() == before {
println!("no member named {name}");
return Ok(());
}
save_members(&config.members_path(), &members)?;
println!("removed {name}");
Ok(())
}
pub fn member_list(config_path: &Path) -> Result<()> {
let config = Config::load(config_path)?;
let members = load_members(&config.members_path())?;
if members.is_empty() {
println!(
"member directory {} is empty (open bootstrap mode: any valid signature is accepted)",
config.members_path().display()
);
return Ok(());
}
for member in members {
println!("{} [{}] {}", member.name, member.class, member.pubkey);
}
Ok(())
}
pub async fn aggregates(
config_path: &Path,
from: Option<&str>,
period: Option<&str>,
timeout_ms: Option<u64>,
) -> Result<()> {
let config = Config::load(config_path)?;
let base = format!("http://{}", config.node.listen);
let period = period
.map(str::to_string)
.unwrap_or_else(node::current_period);
match from {
Some(to) => {
let value = node::control_aggregate_request(&base, to, &period, timeout_ms).await?;
println!("{}", serde_json::to_string_pretty(&value)?);
}
None => {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(2))
.build()?;
let response = client
.get(format!("{base}/v1/local/aggregates?period={period}"))
.send()
.await
.context("calling local node (is `frxd serve` running?)")?;
let status = response.status();
let value: serde_json::Value = response.json().await?;
if !status.is_success() {
return Err(anyhow!("local node error {status}: {value}"));
}
println!("{}", serde_json::to_string_pretty(&value)?);
}
}
Ok(())
}
pub async fn status(config_path: &Path) -> Result<()> {
let config = Config::load(config_path)?;
let base = format!("http://{}", config.node.listen);