use bitcoin::{BlockHash, Txid};
use clap::{Parser, Subcommand};
use std::{
io::{self, Write},
net::SocketAddr,
path::PathBuf,
process::ExitCode,
};
use urma_names::name::Name;
use urma_node::{Error, client::Client, model::ChainPoint};
#[derive(Parser)]
#[command(about = "Control an explicitly configured authenticated loopback URMA node")]
struct Args {
#[arg(long)]
endpoint: SocketAddr,
#[arg(long)]
token_file: PathBuf,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Status,
Object {
root: Txid,
},
Pin {
root: Txid,
},
Unpin {
root: Txid,
},
Prune,
OverlayOmissions,
OverlayBook,
NamesStatus {
genesis: Txid,
},
NamesRegister {
genesis: Txid,
#[arg(value_parser = parse_height)]
height: u64,
hash: BlockHash,
},
NamesStep {
genesis: Txid,
},
NamesResolve {
genesis: Txid,
#[arg(value_parser = parse_name)]
name: Name,
},
}
fn parse_height(text: &str) -> Result<u64, String> {
if text.is_empty() || !text.bytes().all(|byte| byte.is_ascii_digit()) {
return Err("height must be a decimal integer from 0 through i64::MAX".into());
}
let height = text
.parse::<u64>()
.map_err(|_| "height must be a decimal integer from 0 through i64::MAX".to_owned())?;
if height > i64::MAX as u64 {
return Err("height must be a decimal integer from 0 through i64::MAX".into());
}
Ok(height)
}
fn parse_name(text: &str) -> Result<Name, String> {
Name::parse(text).map_err(|error| error.to_string())
}
fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
if !std::fs::symlink_metadata(&args.token_file)?.is_file() {
return Err(io::Error::other("token file must be a regular file").into());
}
let bytes = urma_io::read_shared(&args.token_file, 256)?;
let token = std::str::from_utf8(&bytes)?.trim();
let client = Client::new(args.endpoint, token.to_owned())?;
let value = (|| -> Result<serde_json::Value, Error> {
Ok(match args.command {
Command::Status => client.status_json()?,
Command::Object { root } => serde_json::to_value(client.object_status(root)?)?,
Command::Pin { root } => serde_json::to_value(client.set_pin(root, true)?)?,
Command::Unpin { root } => serde_json::to_value(client.set_pin(root, false)?)?,
Command::Prune => serde_json::to_value(client.prune_cache()?)?,
Command::OverlayOmissions => serde_json::to_value(client.overlay_omissions()?)?,
Command::OverlayBook => serde_json::to_value(client.overlay_book()?)?,
Command::NamesStatus { genesis } => {
serde_json::to_value(client.names_status(genesis)?)?
}
Command::NamesRegister {
genesis,
height,
hash,
} => {
serde_json::to_value(client.register_names(genesis, ChainPoint { height, hash })?)?
}
Command::NamesStep { genesis } => serde_json::to_value(client.names_step(genesis)?)?,
Command::NamesResolve { genesis, name } => {
serde_json::to_value(client.names_resolve(genesis, &name)?)?
}
})
})()
.map_err(|error| io::Error::other(error.to_string().replace(token, "[redacted]")))?;
let mut output = io::stdout().lock();
serde_json::to_writer(&mut output, &value)?;
output.write_all(b"\n")?;
output.flush()?;
Ok(())
}
fn main() -> ExitCode {
match run(Args::parse()) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("urma-node-control: {error}");
ExitCode::FAILURE
}
}
}