#![cfg(feature = "cli")]
use crate::{Keyring, Keystore, ss58};
use anyhow::{Result, anyhow};
use clap::Parser;
use colored::{ColoredString, Colorize};
use schnorrkel::{PublicKey, Signature};
use std::{fs, path::PathBuf};
#[derive(Clone, Debug, Parser)]
pub enum Command {
Add {
path: PathBuf,
#[clap(short, long)]
convert_to_vara: bool,
},
New {
name: String,
#[arg(short, long)]
passphrase: String,
#[arg(long)]
vanity: Option<String>,
},
#[clap(visible_alias = "l")]
List {
#[arg(short, long)]
primary: bool,
#[arg(short, long)]
force_vara: bool,
},
Use {
key: String,
},
Sign {
#[clap(short, long, default_value = "gring.vara")]
ctx: String,
message: String,
#[clap(short, long)]
passphrase: String,
},
Verify {
#[clap(short, long, default_value = "gring.vara")]
ctx: String,
message: String,
signature: String,
#[arg(short, long)]
address: Option<String>,
},
}
impl Command {
pub fn store() -> Result<PathBuf> {
let app = env!("CARGO_PKG_NAME");
let store = dirs::data_dir()
.ok_or_else(|| anyhow!("Failed to locate app directory."))?
.join(app);
fs::create_dir_all(&store).inspect_err(|_| {
tracing::error!("Failed to create keyring store at {store:?}");
})?;
tracing::info!(
"keyring store: {}",
store.display().to_string().underline().dimmed()
);
Ok(store)
}
pub fn run(self) -> Result<()> {
let mut keyring = Keyring::load(Command::store()?)?;
match self {
Command::Add {
path,
convert_to_vara,
} => {
let mut keystore = serde_json::from_str::<Keystore>(&fs::read_to_string(&path)?)?;
if convert_to_vara {
keystore.address = ss58::recode(&keystore.address).map_err(|e| anyhow!(e))?;
}
let name = path
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or(keystore.meta.name.clone());
keyring
.list()
.iter()
.find(|k| k.meta.name == keystore.meta.name)
.map_or_else(
|| {
fs::write(
keyring.store.join(&name).with_extension("json"),
serde_json::to_string_pretty(&keystore)?,
)?;
println!(
"Key {} has been imported!",
keystore.meta.name.cyan().bold()
);
Ok(())
},
|_| {
Err(anyhow!(
"Key {} already exists.",
keystore.meta.name.yellow().bold()
))
},
)?;
}
Command::New {
mut name,
vanity,
passphrase,
} => {
if name.len() > 16 {
return Err(anyhow!("Name must be less than 16 characters."));
}
let raw_name = name.clone();
let path = {
let mut path = keyring.store.join(&name).with_extension("json");
let mut count = 0;
while path.exists() {
name = format!("{}-{}", &raw_name, count);
path = keyring.store.join(&name).with_extension("json");
count += 1;
}
path
};
if name != raw_name {
tracing::info!(
"Key {} exists, auto switching to {}",
raw_name.underline(),
name.underline().cyan()
);
}
let (keystore, keypair) =
keyring.create(&name, vanity.as_deref(), Some(passphrase.as_ref()))?;
println!("{:<16}{}", "Name:", name.bold());
println!("{:<16}{}", "VARA Address: ", keystore.address);
println!("{:<16}0x{}", "Public Key:", hex::encode(keypair.public));
println!(
"Drag {} to the polkadot.js extension to import it.",
path.display().to_string().underline()
);
}
Command::List {
primary,
force_vara,
} => {
if primary {
let mut key = keyring.primary()?;
if force_vara {
key.address = ss58::recode(&key.address).map_err(|e| anyhow!(e))?;
}
Self::print_key(&key);
return Ok(());
}
println!("| {:<18} | {:<49} |", "Name".bold(), "Address".bold());
println!("| {} | {} |", "-".repeat(18), "-".repeat(49));
for key in keyring.list() {
let mut name: ColoredString = key.meta.name.clone().into();
let mut address: ColoredString = key.address.clone().into();
if force_vara {
address = ss58::recode(&address).map_err(|e| anyhow!(e))?.into();
}
if key.meta.name == keyring.primary {
name = name.cyan();
address = address.cyan();
};
println!("| {name:<18} | {address} |");
}
}
Command::Use { key } => {
let key = keyring.set_primary(key)?;
println!("The primary key has been updated to:");
Self::print_key(&key);
}
Command::Sign {
ctx,
message,
passphrase,
} => {
let key = keyring.primary()?;
let pair = key.decrypt_scrypt(passphrase.as_ref()).map_err(|e| {
anyhow!("Incorrect passphrase, failed to decrypt keystore, {e}")
})?;
let sig = pair
.sign(schnorrkel::signing_context(ctx.as_bytes()).bytes(message.as_bytes()));
println!("{:<16}{}", "Key:", key.meta.name.green().bold());
println!("{:<16}{}", "SS58 Address:", key.address);
println!("{:<16}{ctx}", "Context:");
println!("{:<16}{message}", "Message:");
println!("{:<16}0x{}", "Signature:", hex::encode(sig.to_bytes()));
}
Command::Verify {
ctx,
message,
signature,
address,
} => {
let pk_bytes = if let Some(address) = address {
if let Some(encoded) = address.strip_prefix("0x") {
hex::decode(encoded).map_err(Into::into)
} else {
ss58::decode(&address).map_err(|e| anyhow!(e))
}
} else {
let key = keyring.primary()?;
ss58::decode(&key.address).map_err(|e| anyhow!(e))
}?;
let pk = PublicKey::from_bytes(&pk_bytes)
.map_err(|e| anyhow!("Failed to decode public key, {e}"))?;
let result = if pk
.verify(
schnorrkel::signing_context(ctx.as_bytes()).bytes(message.as_bytes()),
&Signature::from_bytes(&hex::decode(signature.trim_start_matches("0x"))?)
.map_err(|e| anyhow!("Failed to decode signature, {e}"))?,
)
.is_ok()
{
"Verified".green().bold()
} else {
"Not Verified".red().bold()
};
println!("{:<16}{result}", "Result:");
println!("{:<16}{ctx}", "Context:");
println!("{:<16}{message}", "Message:");
println!("{:<16}0x{signature}", "Signature:");
println!("{:<16}0x{}", "Public Key:", hex::encode(&pk_bytes));
println!("{:<16}{}", "SS58 Address:", ss58::encode(&pk_bytes)?);
}
}
Ok(())
}
fn print_key(key: &Keystore) {
println!("Name: {}", key.meta.name.to_string().bold());
println!("VARA Address: {}", key.address.to_string().underline());
}
}