use anyhow::Result;
use parley_core::pow;
use parley_mls::build_key_packages;
use std::io::Write as _;
use std::path::Path;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::client::Client;
use crate::state::{load_identity, load_party_keys, load_server, save_party_keys, save_server};
pub async fn run(
home: &Path,
server: Option<String>,
network: Option<String>,
count: usize,
handle: Option<String>,
) -> Result<()> {
let mut server_cfg = load_server(home).unwrap_or_default();
if let Some(url) = server {
server_cfg.server_url = url;
}
if let Some(net) = network {
server_cfg.network_id = net;
}
if server_cfg.server_url.is_empty() {
anyhow::bail!("no server URL configured. Run `parley register --server https://...`.");
}
if server_cfg.network_id.is_empty() {
server_cfg.network_id = "parley-mainnet".to_string();
}
save_server(home, &server_cfg)?;
let identity = load_identity(home)?;
let party = load_party_keys(home, &identity)?;
let client = Client::new(&server_cfg, &identity)?;
let pubkey = identity.pubkey()?;
let network = server_cfg.network()?;
let probe = client.register(1, 0, "").await;
let already_registered = probe
.as_ref()
.ok()
.and_then(|v| v.get("already_registered")?.as_bool())
.unwrap_or(false);
let info = client.network_info().await?;
let pow_version = info
.get("pow")
.and_then(|p| p.get("version"))
.and_then(|v| v.as_u64())
.unwrap_or(1) as u8;
let pow_difficulty = info
.get("pow")
.and_then(|p| p.get("difficulty"))
.and_then(|v| v.as_u64())
.unwrap_or(24) as u8;
let nonce = solve_with_animation(pow_version, &network, &pubkey, pow_difficulty)?;
let interactive = is_interactive_stdout();
if already_registered {
if interactive {
println!(" \x1b[2midentity: already registered (proof discarded)\x1b[0m");
} else {
println!("identity: already registered (proof discarded)");
}
} else {
client
.register(pow_version, pow_difficulty, &pow::encode_nonce(&nonce))
.await?;
if interactive {
println!(" \x1b[1;32midentity: registered\x1b[0m");
} else {
println!("identity: registered");
}
}
println!();
let bundles = build_key_packages(&party, count)?;
let packages: Vec<(Vec<u8>, Vec<u8>)> = bundles
.iter()
.map(|b| (b.package_id.to_vec(), b.blob.clone()))
.collect();
let bold = if interactive { "\x1b[1m" } else { "" };
let dim = if interactive { "\x1b[2m" } else { "" };
let reset = if interactive { "\x1b[0m" } else { "" };
match client.publish_key_packages(packages).await {
Ok(resp) => {
save_party_keys(home, &party)?;
println!(
" published {bold}{}{reset} KeyPackages ({} unclaimed on server)",
resp.get("published").and_then(|v| v.as_u64()).unwrap_or(0),
resp.get("unclaimed_now")
.and_then(|v| v.as_u64())
.unwrap_or(0),
);
}
Err(e) if e.to_string().contains("key_package_inventory_full") => {
println!(" {dim}KeyPackage inventory already full; no new packages needed.{reset}");
}
Err(e) => return Err(e),
}
println!(" {dim}server:{reset} {}", server_cfg.server_url);
println!(" {dim}pubkey:{reset} {}", identity.public_b64);
if let Some(h) = handle {
match client.claim_handle(&h).await {
Ok(resp) => {
let canonical = resp
.get("handle")
.and_then(|v| v.as_str())
.unwrap_or(h.as_str());
let already = resp
.get("already_yours")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if already {
println!("handle: '{canonical}' (already yours)");
} else {
println!("handle: '{canonical}' (claimed)");
}
}
Err(e) => {
eprintln!("warning: could not claim handle '{h}': {e}");
eprintln!(
"(your KeyPackages are still published; other agents can add you by pubkey)"
);
}
}
}
Ok(())
}
const MIN_ANIMATION: Duration = Duration::from_millis(2500);
fn is_interactive_stdout() -> bool {
use std::io::IsTerminal as _;
std::io::stdout().is_terminal()
}
fn solve_with_animation(
version: u8,
network: &parley_core::NetworkId,
pubkey: &parley_core::AgentPubkey,
difficulty: u8,
) -> Result<Vec<u8>> {
let interactive = is_interactive_stdout();
if interactive {
print_intro_banner(difficulty);
} else {
println!();
println!("PARLEY · IDENTITY REGISTRATION");
println!(
"Computing one-shot proof of work (difficulty {difficulty}). \
This is the protocol's anti-Sybil floor; expect ~1-3s of CPU."
);
println!();
}
let attempts = Arc::new(AtomicU64::new(0));
let best_bits = Arc::new(AtomicU32::new(0));
let done_flag = Arc::new(AtomicBool::new(false));
let solve_micros = Arc::new(AtomicU64::new(0));
let started = Instant::now();
let attempts_solver = Arc::clone(&attempts);
let best_solver = Arc::clone(&best_bits);
let done_solver = Arc::clone(&done_flag);
let solve_micros_solver = Arc::clone(&solve_micros);
let version_owned = version;
let difficulty_owned = difficulty;
let network_owned = network.clone();
let pubkey_owned = *pubkey;
let started_for_solver = started;
let solver = std::thread::spawn(move || {
let challenge = pow::challenge_bytes(
version_owned,
&network_owned,
&pubkey_owned,
difficulty_owned,
);
let need = u32::from(difficulty_owned);
let mut nonce: u64 = 0;
loop {
let nb = nonce.to_be_bytes();
let bits = pow::leading_zero_bits_of_hash(&challenge, &nb);
if bits > best_solver.load(Ordering::Relaxed) {
best_solver.store(bits, Ordering::Relaxed);
}
if bits >= need {
attempts_solver.store(nonce, Ordering::Relaxed);
let elapsed_us =
u64::try_from(started_for_solver.elapsed().as_micros()).unwrap_or(u64::MAX);
solve_micros_solver.store(elapsed_us, Ordering::Release);
done_solver.store(true, Ordering::Release);
return nb.to_vec();
}
nonce = nonce.wrapping_add(1);
if nonce.is_multiple_of(8192) {
attempts_solver.store(nonce, Ordering::Relaxed);
}
}
});
if interactive {
run_matrix_animation(&attempts, &best_bits, difficulty, &done_flag, started);
}
let nonce = solver
.join()
.map_err(|_| anyhow::anyhow!("solver thread panicked"))?;
let solve_elapsed = Duration::from_micros(solve_micros.load(Ordering::Acquire));
let count = attempts.load(Ordering::Relaxed);
if interactive {
println!(
" \x1b[1;32m✓\x1b[0m proof found in \x1b[1m{:.2}s\x1b[0m ({} hashes, {} leading zero bits)",
solve_elapsed.as_secs_f32(),
format_count(count),
difficulty,
);
} else {
println!(
"proof found in {:.2}s ({} hashes, {} leading zero bits)",
solve_elapsed.as_secs_f32(),
format_count(count),
difficulty,
);
}
println!();
Ok(nonce)
}
const RAIN_COLS: usize = 64;
const RAIN_ROWS: usize = 8;
const RAIN_TRAIL: usize = 7;
const RAIN_PAUSE: usize = 5; const RAIN_FRAME_MS: u64 = 55;
const TOTAL_RESERVED_LINES: usize = RAIN_ROWS + 2;
fn run_matrix_animation(
attempts: &Arc<AtomicU64>,
best_bits: &Arc<AtomicU32>,
target: u8,
done_flag: &Arc<AtomicBool>,
started: Instant,
) {
use rand::Rng as _;
let mut rng = rand::thread_rng();
let cycle_len: f32 = (RAIN_ROWS + RAIN_TRAIL + RAIN_PAUSE) as f32;
let cols: Vec<(f32, f32)> = (0..RAIN_COLS)
.map(|_| {
let speed = 0.25 + rng.gen::<f32>() * 0.85; let phase = rng.gen::<f32>() * cycle_len;
(speed, phase)
})
.collect();
print!("\x1b[?25l");
for _ in 0..TOTAL_RESERVED_LINES {
println!();
}
let chars: &[u8] = b"0123456789abcdef";
let mut frame: u64 = 0;
loop {
std::thread::sleep(Duration::from_millis(RAIN_FRAME_MS));
let elapsed = started.elapsed();
let solver_done = done_flag.load(Ordering::Acquire);
if solver_done && elapsed >= MIN_ANIMATION {
break;
}
print!("\x1b[{}A", TOTAL_RESERVED_LINES);
for row in 0..RAIN_ROWS {
print!("\x1b[2K\r ");
for &(speed, phase) in &cols {
let raw = frame as f32 * speed + phase;
let cycle_pos = raw.rem_euclid(cycle_len);
let head_pos = cycle_pos - RAIN_PAUSE as f32;
let dist = head_pos - row as f32;
if dist >= 0.0 && dist < RAIN_TRAIL as f32 {
let ch = chars[rng.gen_range(0..chars.len())] as char;
let color = if dist < 0.5 {
"\x1b[1;97m"
} else if dist < 1.5 {
"\x1b[1;92m" } else if dist < 3.5 {
"\x1b[32m" } else {
"\x1b[2;32m" };
print!("{color}{ch}\x1b[0m");
} else {
print!(" ");
}
}
println!();
}
print!("\x1b[2K\r");
println!();
let attempts_v = attempts.load(Ordering::Relaxed);
let bits = best_bits.load(Ordering::Relaxed);
let bar = bit_bar(bits, target);
let label = if solver_done {
"\x1b[1;32m FOUND \x1b[0m"
} else {
"\x1b[1;36m COMPUTING \x1b[0m"
};
print!("\x1b[2K\r");
println!(
" [{label}] {bar} best: \x1b[1m{bits:>2}\x1b[0m/{target} bits \
\x1b[1m{:>11}\x1b[0m hashes \x1b[2m{:.1}s\x1b[0m",
format_count(attempts_v),
elapsed.as_secs_f32()
);
let _ = std::io::stdout().flush();
frame += 1;
}
print!("\x1b[{}A", TOTAL_RESERVED_LINES);
for _ in 0..TOTAL_RESERVED_LINES {
println!("\x1b[2K");
}
print!("\x1b[{}A", TOTAL_RESERVED_LINES);
print!("\x1b[?25h"); let _ = std::io::stdout().flush();
}
fn print_intro_banner(_difficulty: u8) {
let cyan = "\x1b[1;36m";
let bold = "\x1b[1m";
let dim = "\x1b[2m";
let r = "\x1b[0m";
println!();
println!(" {cyan}◆{r} {bold}PARLEY · IDENTITY REGISTRATION{r}");
println!();
println!(" {dim}Establishing your identity on the network.{r}");
println!();
}
fn bit_bar(have: u32, target: u8) -> String {
let target = u32::from(target).max(1);
let filled = have.min(target) as usize;
let empty = (target as usize).saturating_sub(filled);
let mut s = String::with_capacity(target as usize + 16);
s.push_str("\x1b[32m");
for _ in 0..filled {
s.push('█');
}
s.push_str("\x1b[2m");
for _ in 0..empty {
s.push('·');
}
s.push_str("\x1b[0m");
s
}
fn format_count(n: u64) -> String {
let s = n.to_string();
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len() + s.len() / 3);
for (i, b) in bytes.iter().enumerate() {
if i > 0 && (bytes.len() - i).is_multiple_of(3) {
out.push(',');
}
out.push(*b as char);
}
out
}