use bitcoin_hashes::sha256::Hash as Sha256Hash;
use bitcoin_hashes::Hash;
use clap::{Arg, ArgMatches, Command};
use lightning_storage_server::client::{ClientError, PrivAuth, PrivClient};
use lightning_storage_server::Value;
use lssd::util::setup_logging;
use lssd::util::{init_secret_key, read_public_key, read_secret_key, state_file_path};
use secp256k1::{PublicKey, SecretKey};
use std::fs;
const CLIENT_APP_NAME: &str = "lss-cli";
#[tokio::main]
async fn ping_subcommand(rpc_url: &str) -> Result<(), Box<dyn std::error::Error>> {
let result = PrivClient::ping(rpc_url, "hello").await?;
println!("ping result: {}", result);
Ok(())
}
fn secret_key() -> Result<SecretKey, Box<dyn std::error::Error>> {
read_secret_key("client-key")
}
fn public_key() -> Result<PublicKey, Box<dyn std::error::Error>> {
read_public_key("client-key")
}
fn server_public_key() -> Result<PublicKey, Box<dyn std::error::Error>> {
let server_pubkey_file = state_file_path("server-pubkey")?;
let server_pubkey_hex = fs::read_to_string(server_pubkey_file)?;
Ok(PublicKey::from_slice(&hex::decode(server_pubkey_hex)?)?)
}
#[tokio::main]
async fn init_subcommand(rpc_url: &str) -> Result<(), Box<dyn std::error::Error>> {
init_secret_key("client-key")?;
let (server_key, _version) = PrivClient::get_info(rpc_url).await?;
let server_pubkey_file = state_file_path("server-pubkey")?;
fs::write(server_pubkey_file, hex::encode(&server_key.serialize()))?;
Ok(())
}
fn info_subcommand(_rpc_url: &str) -> Result<(), Box<dyn std::error::Error>> {
match public_key() {
Ok(pk) => println!("public key: {}", hex::encode(pk.serialize())),
Err(_) => println!("not initialized"),
}
match server_public_key() {
Ok(pk) => println!("server public key: {}", hex::encode(pk.serialize())),
Err(_) => println!("server public key not initialized"),
}
Ok(())
}
fn make_auth() -> Result<(PrivAuth, Vec<u8>), Box<dyn std::error::Error>> {
let secret_key = secret_key()?;
let hmac_secret = Sha256Hash::hash(&secret_key[..]).to_byte_array();
let auth = PrivAuth::new_for_client(&secret_key, &server_public_key()?);
Ok((auth, hmac_secret.to_vec()))
}
#[tokio::main]
async fn get_subcommand(
rpc_url: &str,
matches: &ArgMatches,
) -> Result<(), Box<dyn std::error::Error>> {
let prefix = matches.get_one::<String>("prefix").unwrap();
let (auth, hmac_secret) = make_auth()?;
let mut client = PrivClient::new(rpc_url, auth.clone()).await?;
let res = client.get(&hmac_secret, prefix.to_owned()).await?;
for (key, value) in res {
println!("key: {}, version: {} value: {}", key, value.version, hex::encode(value.value));
}
Ok(())
}
#[tokio::main]
async fn put_subcommand(
rpc_url: &str,
matches: &ArgMatches,
) -> Result<(), Box<dyn std::error::Error>> {
let key = matches.get_one::<String>("key").unwrap();
let version = *matches.get_one::<i64>("version").unwrap();
let value_hex = matches.get_one::<String>("value").unwrap();
let value = hex::decode(value_hex).unwrap();
let (auth, hmac_secret) = make_auth()?;
let mut client = PrivClient::new(rpc_url, auth.clone()).await?;
match client.put(&hmac_secret, vec![(key.to_owned(), Value { version, value })]).await {
Ok(()) => Ok(()),
Err(ClientError::PutConflict(conflicts)) => {
for (key, value) in conflicts {
println!(
"conflict key: {}, version: {} value: {}",
key,
value.version,
hex::encode(value.value)
);
}
Err("put conflict".into())
}
Err(e) => Err(e.into()),
}
}
fn parse_rpc_url(matches: &ArgMatches) -> String {
let raw_rpc_value = matches.get_one::<String>("rpc").expect("rpc");
let rpc_url = match raw_rpc_value.parse::<u16>() {
Ok(_) => {
let mut base_url = String::from("http://127.0.0.1:");
base_url.push_str(raw_rpc_value);
base_url
}
Err(_) => match url::Url::parse(raw_rpc_value) {
Ok(_) => String::from(raw_rpc_value),
_ => panic!("Invalid rpc_value"),
},
};
rpc_url
}
fn make_get_subapp() -> Command {
Command::new("get")
.about("get all keys/values at a key prefix")
.arg(Arg::new("prefix").num_args(1).required(true).help("key prefix"))
}
fn make_put_subapp() -> Command {
Command::new("put")
.about("put a versioned key/value")
.arg(Arg::new("key").num_args(1).required(true))
.arg(Arg::new("version").num_args(1).required(true).help("integer version"))
.arg(Arg::new("value").num_args(1).required(true).help("hex value"))
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let app = Command::new(CLIENT_APP_NAME)
.about("a CLI utility which communicates with a running Validating Lightning Signer server via gRPC")
.arg(
Arg::new("rpc")
.help("Either port number or uri")
.short('c')
.long("rpc")
.num_args(1)
.global(true)
.default_value("http://127.0.0.1:55551")
.value_parser(|value: &str| {
let is_port = value.parse::<u16>().is_ok();
let is_url = url::Url::parse(value).is_ok();
if is_port || is_url {
Ok("")
} else {
Err("Value is neither a port number nor a valid uri.")
}
}),
)
.subcommand(Command::new("ping"))
.subcommand(Command::new("init"))
.subcommand(Command::new("info"))
.subcommand(make_get_subapp())
.subcommand(make_put_subapp());
let matches = app.clone().get_matches();
setup_logging("lss-cli", "info");
let rpc_url = parse_rpc_url(&matches);
let rpc = rpc_url.as_str();
match matches.subcommand() {
Some(("ping", _)) => ping_subcommand(rpc)?,
Some(("init", _)) => init_subcommand(rpc)?,
Some(("info", _)) => info_subcommand(rpc)?,
Some(("get", submatches)) => get_subcommand(rpc, submatches)?,
Some(("put", submatches)) => put_subcommand(rpc, submatches)?,
Some((name, _)) => panic!("unimplemented command {}", name),
None => panic!("missing command, try 'help'"),
};
Ok(())
}