diff --git a/Cargo.lock b/Cargo.lock index e0753339..b03a31ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -412,6 +412,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -426,6 +427,18 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "clap_lex" version = "0.6.0" @@ -695,6 +708,15 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1028,6 +1050,7 @@ dependencies = [ "casper-types", "clap", "hex", + "predicates", "thiserror", ] @@ -1146,6 +1169,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1389,7 +1418,10 @@ checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" dependencies = [ "anstyle", "difflib", + "float-cmp", + "normalize-line-endings", "predicates-core", + "regex", ] [[package]] diff --git a/kairos-cli/Cargo.toml b/kairos-cli/Cargo.toml index 7a07e845..abf507c8 100644 --- a/kairos-cli/Cargo.toml +++ b/kairos-cli/Cargo.toml @@ -14,9 +14,10 @@ edition = "2021" [dependencies] casper-types = { version = "4.0.1", features = ["std"] } # TODO: Change `std` -> `std-fs-io` in the future version. -clap = "4.4.18" +clap = { version = "4.4.18", features = ["derive"] } hex = "0.4.3" thiserror = "1.0.56" [dev-dependencies] assert_cmd = "2.0.13" +predicates = "3.1.0" diff --git a/kairos-cli/bin/commands/deposit.rs b/kairos-cli/bin/commands/deposit.rs index 17dd2f48..a3967e6f 100644 --- a/kairos-cli/bin/commands/deposit.rs +++ b/kairos-cli/bin/commands/deposit.rs @@ -1,31 +1,24 @@ -use crate::common::{amount, private_key}; +use crate::common::args::{AmountArg, PrivateKeyPathArg}; use crate::crypto::signer::CasperSigner; use crate::error::CliError; -use clap::{ArgMatches, Command}; -pub struct Deposit; +use clap::Parser; -impl Deposit { - pub const NAME: &'static str = "deposit"; - pub const ABOUT: &'static str = "Deposits funds into your account"; - - pub fn new_cmd() -> Command { - Command::new(Self::NAME) - .about(Self::ABOUT) - .arg(amount::arg()) - .arg(private_key::arg()) - } - - pub fn run(matches: &ArgMatches) -> Result { - let _amount = amount::get(matches)?; - let private_key = private_key::get(matches)?; +#[derive(Parser, Debug)] +pub struct Args { + #[clap(flatten)] + amount: AmountArg, + #[clap(flatten)] + private_key_path: PrivateKeyPathArg, +} - let _signer = CasperSigner::from_key(private_key); +pub fn run(args: Args) -> Result { + let _amount: u64 = args.amount.field; + let _signer = CasperSigner::from_key_pathbuf(args.private_key_path.field)?; - // TODO: Create transaction and sign it with `signer`. + // TODO: Create transaction and sign it with `signer`. - // TODO: Send transaction to the network, using Rust SDK. + // TODO: Send transaction to the network, using Rust SDK. - Ok("ok".to_string()) - } + Ok("ok".to_string()) } diff --git a/kairos-cli/bin/commands/mod.rs b/kairos-cli/bin/commands/mod.rs index 3c6fdceb..069a0721 100644 --- a/kairos-cli/bin/commands/mod.rs +++ b/kairos-cli/bin/commands/mod.rs @@ -1,7 +1,15 @@ -mod deposit; -mod transfer; -mod withdraw; +pub mod deposit; +pub mod transfer; +pub mod withdraw; -pub use deposit::Deposit; -pub use transfer::Transfer; -pub use withdraw::Withdraw; +use clap::Subcommand; + +#[derive(Subcommand)] +pub enum Command { + #[command(about = "Deposits funds into your account")] + Deposit(deposit::Args), + #[command(about = "Transfers funds to another account")] + Transfer(transfer::Args), + #[command(about = "Withdraws funds from your account")] + Withdraw(withdraw::Args), +} diff --git a/kairos-cli/bin/commands/transfer.rs b/kairos-cli/bin/commands/transfer.rs index e8da7e38..74112948 100644 --- a/kairos-cli/bin/commands/transfer.rs +++ b/kairos-cli/bin/commands/transfer.rs @@ -1,59 +1,31 @@ -use crate::common::{amount, private_key}; +use crate::common::args::{AmountArg, PrivateKeyPathArg}; use crate::crypto::public_key::CasperPublicKey; use crate::crypto::signer::CasperSigner; use crate::error::CliError; -use clap::{Arg, ArgMatches, Command}; +use crate::utils::parse_hex_string; -pub struct Transfer; +use clap::Parser; -const ARG_NAME: &str = "recipient"; -const ARG_SHORT: char = 'r'; -const ARG_VALUE_NAME: &str = "PUBLIC_KEY"; +type StdVec = Vec; -pub mod recipient { - use super::*; - - pub fn arg() -> Arg { - Arg::new(ARG_NAME) - .long(ARG_NAME) - .short(ARG_SHORT) - .required(true) - .value_name(ARG_VALUE_NAME) - } - - pub fn get(matches: &ArgMatches) -> Result { - let value = matches - .get_one::("recipient") - .map(String::as_str) - .unwrap(); - - CasperPublicKey::from_hex(value).map_err(|error| CliError::CryptoError { error }) - } +#[derive(Parser)] +pub struct Args { + #[arg(long, short, value_name = "PUBLIC_KEY", value_parser = parse_hex_string)] + recipient: StdVec, + #[clap(flatten)] + amount: AmountArg, + #[clap(flatten)] + private_key_path: PrivateKeyPathArg, } -impl Transfer { - pub const NAME: &'static str = "transfer"; - pub const ABOUT: &'static str = "Transfers funds to another account"; - - pub fn new_cmd() -> Command { - Command::new(Self::NAME) - .about(Self::ABOUT) - .arg(recipient::arg()) - .arg(amount::arg()) - .arg(private_key::arg()) - } - - pub fn run(matches: &ArgMatches) -> Result { - let _recipient = recipient::get(matches)?; - let _amount = amount::get(matches)?; - let private_key = private_key::get(matches)?; - - let _signer = CasperSigner::from_key(private_key); +pub fn run(args: Args) -> Result { + let _recipient = CasperPublicKey::from_bytes(args.recipient.as_ref())?; + let _amount: u64 = args.amount.field; + let _signer = CasperSigner::from_key_pathbuf(args.private_key_path.field)?; - // TODO: Create transaction and sign it with `signer`. + // TODO: Create transaction and sign it with `signer`. - // TODO: Send transaction to the network, using Rust SDK. + // TODO: Send transaction to the network, using Rust SDK. - Ok("ok".to_string()) - } + Ok("ok".to_string()) } diff --git a/kairos-cli/bin/commands/withdraw.rs b/kairos-cli/bin/commands/withdraw.rs index 481cf894..bdb0c932 100644 --- a/kairos-cli/bin/commands/withdraw.rs +++ b/kairos-cli/bin/commands/withdraw.rs @@ -1,31 +1,24 @@ -use crate::common::{amount, private_key}; +use crate::common::args::{AmountArg, PrivateKeyPathArg}; use crate::crypto::signer::CasperSigner; use crate::error::CliError; -use clap::{ArgMatches, Command}; -pub struct Withdraw; +use clap::Parser; -impl Withdraw { - pub const NAME: &'static str = "withdraw"; - pub const ABOUT: &'static str = "Withdraws funds from your account"; - - pub fn new_cmd() -> Command { - Command::new(Self::NAME) - .about(Self::ABOUT) - .arg(amount::arg()) - .arg(private_key::arg()) - } - - pub fn run(matches: &ArgMatches) -> Result { - let _amount = amount::get(matches)?; - let private_key = private_key::get(matches)?; +#[derive(Parser)] +pub struct Args { + #[clap(flatten)] + amount: AmountArg, + #[clap(flatten)] + private_key_path: PrivateKeyPathArg, +} - let _signer = CasperSigner::from_key(private_key); +pub fn run(args: Args) -> Result { + let _amount: u64 = args.amount.field; + let _signer = CasperSigner::from_key_pathbuf(args.private_key_path.field)?; - // TODO: Create transaction and sign it with `signer`. + // TODO: Create transaction and sign it with `signer`. - // TODO: Send transaction to the network, using Rust SDK. + // TODO: Send transaction to the network, using Rust SDK. - Ok("ok".to_string()) - } + Ok("ok".to_string()) } diff --git a/kairos-cli/bin/common/args.rs b/kairos-cli/bin/common/args.rs index 73e268ac..696dfa12 100644 --- a/kairos-cli/bin/common/args.rs +++ b/kairos-cli/bin/common/args.rs @@ -1,61 +1,15 @@ -use crate::error::CliError; -use clap::{Arg, ArgMatches}; +use std::path::PathBuf; -pub mod amount { - use super::*; +use clap::Args; - const ARG_NAME: &str = "amount"; - const ARG_SHORT: char = 'a'; - const ARG_VALUE_NAME: &str = "NUM_MOTES"; - - pub fn arg() -> Arg { - Arg::new(ARG_NAME) - .long(ARG_NAME) - .short(ARG_SHORT) - .required(true) - .value_name(ARG_VALUE_NAME) - } - - pub fn get(matches: &ArgMatches) -> Result { - let value = matches - .get_one::(ARG_NAME) - .map(String::as_str) - .ok_or(CliError::MissingArgument { context: ARG_NAME })?; - - let amount = value - .parse::() - .map_err(|_| CliError::FailedToParseU64 { context: "amount" })?; - - Ok(amount) - } +#[derive(Args, Debug)] +pub struct AmountArg { + #[arg(name = "amount", long, short, value_name = "NUM_MOTES")] + pub field: u64, } -pub mod private_key { - use crate::crypto::private_key::CasperPrivateKey; - - use super::*; - - const ARG_NAME: &str = "private-key"; - const ARG_SHORT: char = 'k'; - const ARG_VALUE_NAME: &str = "FILE_PATH"; - - pub fn arg() -> Arg { - Arg::new(ARG_NAME) - .long(ARG_NAME) - .short(ARG_SHORT) - .required(true) - .value_name(ARG_VALUE_NAME) - } - - pub fn get(matches: &ArgMatches) -> Result { - let value = matches - .get_one::(ARG_NAME) - .map(String::as_str) - .ok_or(CliError::MissingArgument { context: ARG_NAME })?; - - let private_key = - CasperPrivateKey::from_file(value).map_err(|error| CliError::CryptoError { error })?; - - Ok(private_key) - } +#[derive(Args, Debug)] +pub struct PrivateKeyPathArg { + #[arg(name = "private-key", long, short = 'k', value_name = "FILE_PATH")] + pub field: PathBuf, } diff --git a/kairos-cli/bin/common/mod.rs b/kairos-cli/bin/common/mod.rs index 6a85263a..6e10f4ad 100644 --- a/kairos-cli/bin/common/mod.rs +++ b/kairos-cli/bin/common/mod.rs @@ -1,4 +1 @@ -mod args; - -pub use args::amount; -pub use args::private_key; +pub mod args; diff --git a/kairos-cli/bin/crypto/error.rs b/kairos-cli/bin/crypto/error.rs index 62ce5f66..c713903e 100644 --- a/kairos-cli/bin/crypto/error.rs +++ b/kairos-cli/bin/crypto/error.rs @@ -2,9 +2,13 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum CryptoError { - /// Failed to parse a public key from a formatted string. + /// Unable to load a file from the given path. + #[error("failed to load key from file")] + KeyLoad, + /// Failed to parse a public key from a raw data. #[error("failed to parse private key")] - FailedToParseKey {}, - #[error("failed to serialize signature/key")] - Serialization {}, + FailedToParseKey, + /// Invalid public key (hexdigest) or other encoding related error. + #[error("failed to serialize/deserialize '{context}'")] + Serialization { context: &'static str }, } diff --git a/kairos-cli/bin/crypto/private_key.rs b/kairos-cli/bin/crypto/private_key.rs index 7fcc937c..ca424199 100644 --- a/kairos-cli/bin/crypto/private_key.rs +++ b/kairos-cli/bin/crypto/private_key.rs @@ -1,11 +1,18 @@ +use casper_types::file_utils::read_file; + use crate::crypto::error::CryptoError; pub struct CasperPrivateKey(pub casper_types::SecretKey); impl CasperPrivateKey { pub fn from_file(file_path: &str) -> Result { - let secret_key = casper_types::SecretKey::from_file(file_path) - .map_err(|_e| CryptoError::FailedToParseKey {})?; + let data = read_file(file_path).map_err(|_e| CryptoError::KeyLoad)?; + let secret_key = + casper_types::SecretKey::from_pem(data).map_err(|_e| CryptoError::FailedToParseKey)?; Ok(Self(secret_key)) } } + +pub fn parse_private_key(file_path: &str) -> Result { + CasperPrivateKey::from_file(file_path) +} diff --git a/kairos-cli/bin/crypto/public_key.rs b/kairos-cli/bin/crypto/public_key.rs index 1a58b849..c4bb478a 100644 --- a/kairos-cli/bin/crypto/public_key.rs +++ b/kairos-cli/bin/crypto/public_key.rs @@ -1,17 +1,18 @@ use crate::crypto::error::CryptoError; use casper_types::bytesrepr::FromBytes; use casper_types::bytesrepr::ToBytes; -use casper_types::crypto; #[derive(Clone)] pub struct CasperPublicKey(pub casper_types::PublicKey); impl CasperPublicKey { - pub fn from_hex(hex_str: &str) -> Result { - let bytes = hex::decode(hex_str).map_err(|_e| CryptoError::Serialization {})?; - let (public_key, _) = - crypto::PublicKey::from_bytes(&bytes).map_err(|_e| CryptoError::Serialization {})?; - + pub fn from_bytes(bytes: &[u8]) -> Result { + let (public_key, _remainder) = + casper_types::PublicKey::from_bytes(bytes).map_err(|_e| { + CryptoError::Serialization { + context: "public key", + } + })?; Ok(Self(public_key)) } @@ -21,8 +22,8 @@ impl CasperPublicKey { #[allow(unused)] fn to_bytes(&self) -> Result, CryptoError> { - self.0 - .to_bytes() - .map_err(|_e| CryptoError::Serialization {}) + self.0.to_bytes().map_err(|_e| CryptoError::Serialization { + context: "public key", + }) } } diff --git a/kairos-cli/bin/crypto/signer.rs b/kairos-cli/bin/crypto/signer.rs index ba56c0a7..08d64f28 100644 --- a/kairos-cli/bin/crypto/signer.rs +++ b/kairos-cli/bin/crypto/signer.rs @@ -1,6 +1,6 @@ -#![allow(unused)] +use std::path::PathBuf; -use super::private_key::CasperPrivateKey; +use super::private_key::{parse_private_key, CasperPrivateKey}; use super::public_key::CasperPublicKey; use crate::crypto::error::CryptoError; use casper_types::{bytesrepr::ToBytes, SecretKey}; @@ -11,8 +11,9 @@ pub struct CasperSigner { public_key: CasperPublicKey, } +#[allow(unused)] impl CasperSigner { - pub fn from_key(secret_key: CasperPrivateKey) -> Self { + fn from_key_raw(secret_key: CasperPrivateKey) -> Self { // Derive the public key. let public_key = CasperPublicKey::from_key(PublicKey::from(&secret_key.0)); @@ -24,20 +25,29 @@ impl CasperSigner { pub fn from_file(secret_key_path: &str) -> Result { let secret_key = - SecretKey::from_file(secret_key_path).map_err(|_| CryptoError::FailedToParseKey {})?; + SecretKey::from_file(secret_key_path).map_err(|_e| CryptoError::FailedToParseKey)?; - Ok(Self::from_key(CasperPrivateKey(secret_key))) + Ok(Self::from_key_raw(CasperPrivateKey(secret_key))) } - fn get_public_key(&self) -> CasperPublicKey { + pub fn from_key_pathbuf(secret_key_path: PathBuf) -> Result { + let private_key_path_str: &str = secret_key_path.to_str().ok_or(CryptoError::KeyLoad)?; + let private_key = parse_private_key(private_key_path_str)?; + + Ok(Self::from_key_raw(private_key)) + } + + pub fn get_public_key(&self) -> CasperPublicKey { self.public_key.clone() } - fn sign_message(&self, message: &[u8]) -> Result, CryptoError> { + pub fn sign_message(&self, message: &[u8]) -> Result, CryptoError> { let signature = crypto::sign(message, &self.secret_key.0, &self.public_key.0); let bytes = signature .to_bytes() - .map_err(|_e| CryptoError::Serialization {})?; + .map_err(|_e| CryptoError::Serialization { + context: "signature", + })?; Ok(bytes) } diff --git a/kairos-cli/bin/error.rs b/kairos-cli/bin/error.rs index 11c01afc..3d68c6ed 100644 --- a/kairos-cli/bin/error.rs +++ b/kairos-cli/bin/error.rs @@ -1,15 +1,21 @@ -use crate::crypto::error::CryptoError; +use hex::FromHexError; use thiserror::Error; +use crate::crypto::error::CryptoError; + #[derive(Error, Debug)] pub enum CliError { - /// Unable to find argument by name. - #[error("missing argument '{context}'")] - MissingArgument { context: &'static str }, - /// Failed to parse amount from string. - #[error("failed to parse '{context}' as u64")] - FailedToParseU64 { context: &'static str }, /// Cryptography error. #[error("cryptography error: {error}")] - CryptoError { error: CryptoError }, + CryptoError { + #[from] + error: CryptoError, + }, + // TODO: Add error for "Failed to parse hex string: {}" + /// Failed to parse hex string. + #[error("failed to parse hex string: {error}")] + ParseError { + #[from] + error: FromHexError, + }, } diff --git a/kairos-cli/bin/main.rs b/kairos-cli/bin/main.rs index b1aa75a1..79b20880 100644 --- a/kairos-cli/bin/main.rs +++ b/kairos-cli/bin/main.rs @@ -2,36 +2,27 @@ mod commands; mod common; mod crypto; mod error; +mod utils; -use clap::Command; -use commands::{Deposit, Transfer, Withdraw}; use std::process; -fn cli() -> Command { - Command::new("Kairos Client") - .about("CLI for interacting with Kairos") - .subcommand(Deposit::new_cmd()) - .subcommand(Transfer::new_cmd()) - .subcommand(Withdraw::new_cmd()) +use clap::Parser; +use commands::Command; + +#[derive(Parser)] +#[command(name = "Kairos Client", about = "CLI for interacting with Kairos")] +struct Cli { + #[command(subcommand)] + command: Command, } fn main() { - let arg_matches = cli().get_matches(); - let (subcommand_name, matches) = arg_matches.subcommand().unwrap_or_else(|| { - // No subcommand provided by user. - let _ = cli().print_long_help(); - process::exit(1); - }); + let cli = Cli::parse(); - let result = match subcommand_name { - Deposit::NAME => Deposit::run(matches), - Transfer::NAME => Transfer::run(matches), - Withdraw::NAME => Withdraw::run(matches), - _ => { - // This should not happen, unless we missed some subcommand. - let _ = cli().print_long_help(); - process::exit(1); - } + let result = match cli.command { + Command::Deposit(args) => commands::deposit::run(args), + Command::Transfer(args) => commands::transfer::run(args), + Command::Withdraw(args) => commands::withdraw::run(args), }; match result { diff --git a/kairos-cli/bin/utils.rs b/kairos-cli/bin/utils.rs new file mode 100644 index 00000000..07c23c11 --- /dev/null +++ b/kairos-cli/bin/utils.rs @@ -0,0 +1,6 @@ +use crate::error::CliError; + +/// Custom parser function to convert a hexadecimal string to a byte array. +pub fn parse_hex_string(s: &str) -> Result, CliError> { + hex::decode(s).map_err(|e| e.into()) +} diff --git a/kairos-cli/tests/cli_tests.rs b/kairos-cli/tests/cli_tests.rs index e0e98110..284799c7 100644 --- a/kairos-cli/tests/cli_tests.rs +++ b/kairos-cli/tests/cli_tests.rs @@ -62,7 +62,7 @@ fn deposit_invalid_amount() { .arg(secret_key_path); cmd.assert() .failure() - .stderr("failed to parse 'amount' as u64\n"); + .stderr(predicates::str::contains("invalid value")); } #[test] @@ -77,7 +77,22 @@ fn deposit_invalid_private_key_path() { .arg(secret_key_path); cmd.assert() .failure() - .stderr("cryptography error: failed to parse private key\n"); + .stderr(predicates::str::contains("failed to load key from file")); +} + +#[test] +fn deposit_invalid_private_key_content() { + let secret_key_path = fixture_path("invalid.pem"); // Invalid content + + let mut cmd = Command::cargo_bin("kairos-cli").unwrap(); + cmd.arg("deposit") + .arg("--amount") + .arg("123") + .arg("--private-key") + .arg(secret_key_path); + cmd.assert() + .failure() + .stderr(predicates::str::contains("failed to parse private key")); } #[test] @@ -94,5 +109,5 @@ fn transfer_invalid_recipient() { .arg(secret_key_path); cmd.assert() .failure() - .stderr("cryptography error: failed to serialize signature/key\n"); + .stderr(predicates::str::contains("failed to parse hex string")); } diff --git a/kairos-cli/tests/fixtures/invalid.pem b/kairos-cli/tests/fixtures/invalid.pem new file mode 100644 index 00000000..f9129d7a --- /dev/null +++ b/kairos-cli/tests/fixtures/invalid.pem @@ -0,0 +1 @@ +Ooops!