diff --git a/Cargo.lock b/Cargo.lock index 777e3af6..b1e87e22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1803,8 +1803,15 @@ dependencies = [ "clap", "hex", "kairos-crypto", + "kairos-server", + "kairos-test-utils", + "kairos-tx", "predicates", + "reqwest 0.12.4", + "serde", + "serde_json", "thiserror", + "tokio", ] [[package]] @@ -2759,6 +2766,7 @@ dependencies = [ "base64 0.22.0", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2 0.4.4", diff --git a/kairos-cli/Cargo.toml b/kairos-cli/Cargo.toml index 69535981..43122a0f 100644 --- a/kairos-cli/Cargo.toml +++ b/kairos-cli/Cargo.toml @@ -19,7 +19,14 @@ clap = { version = "4.5", features = ["derive", "deprecated"] } hex = "0.4" thiserror = "1" kairos-crypto = { path = "../kairos-crypto", features = ["fs"] } +kairos-tx = { path = "../kairos-tx" } +kairos-server = { path = "../kairos-server" } +reqwest = { version = "0.12", features = ["blocking", "json"] } +serde_json = "1.0" +serde = "1.0" [dev-dependencies] +tokio = { version = "1" } assert_cmd = "2" predicates = "3" +kairos-test-utils = { path = "../kairos-test-utils" } diff --git a/kairos-cli/bin/main.rs b/kairos-cli/bin/main.rs index 0de33073..bf49f34a 100644 --- a/kairos-cli/bin/main.rs +++ b/kairos-cli/bin/main.rs @@ -1,25 +1,9 @@ -use std::process; - use clap::Parser; -use kairos_cli::commands::{self, Command}; - -#[derive(Parser)] -#[command(name = "Kairos Client", about = "CLI for interacting with Kairos")] -struct Cli { - #[command(subcommand)] - command: Command, -} +use std::process; fn main() { - let cli = Cli::parse(); - - 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 { + let args = kairos_cli::Cli::parse(); + match kairos_cli::run(args) { Ok(output) => { println!("{}", output) } diff --git a/kairos-cli/src/client.rs b/kairos-cli/src/client.rs new file mode 100644 index 00000000..7f970c35 --- /dev/null +++ b/kairos-cli/src/client.rs @@ -0,0 +1,57 @@ +use kairos_server::routes::PayloadBody; + +use reqwest::{blocking, Url}; +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(PartialOrd, Ord, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub enum KairosClientError { + ResponseError(String), + ResponseErrorWithCode(u16, String), + DecodeError(String), + KairosServerError(String), +} + +impl std::error::Error for KairosClientError {} + +impl fmt::Display for KairosClientError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let json_string = serde_json::to_string(self).map_err(|_| fmt::Error)?; + write!(formatter, "{}", json_string) + } +} +impl From for KairosClientError { + fn from(error: reqwest::Error) -> Self { + let error_without_url = error.without_url(); + if error_without_url.is_decode() { + KairosClientError::DecodeError(error_without_url.to_string()) + } else { + match error_without_url.status() { + Option::None => Self::ResponseError(error_without_url.to_string()), + Option::Some(status_code) => { + Self::ResponseErrorWithCode(status_code.as_u16(), error_without_url.to_string()) + } + } + } + } +} + +pub fn submit_transaction_request( + base_url: &Url, + deposit_request: &PayloadBody, +) -> Result<(), KairosClientError> { + let client = blocking::Client::new(); + let url = base_url.join("/api/v1/deposit").unwrap(); + let response = client + .post(url) + .header("Content-Type", "application/json") + .json(deposit_request) + .send() + .map_err(Into::::into)?; + let status = response.status(); + if !status.is_success() { + Err(KairosClientError::KairosServerError(status.to_string())) + } else { + Ok(()) + } +} diff --git a/kairos-cli/src/commands/deposit.rs b/kairos-cli/src/commands/deposit.rs index b4b95c99..3b930b4d 100644 --- a/kairos-cli/src/commands/deposit.rs +++ b/kairos-cli/src/commands/deposit.rs @@ -1,9 +1,14 @@ +use crate::client; use crate::common::args::{AmountArg, PrivateKeyPathArg}; use crate::error::CliError; +use reqwest::Url; + use kairos_crypto::error::CryptoError; use kairos_crypto::implementations::Signer; use kairos_crypto::CryptoSigner; +use kairos_server::routes::PayloadBody; +use kairos_tx::asn::SigningPayload; use clap::Parser; @@ -15,14 +20,23 @@ pub struct Args { private_key_path: PrivateKeyPathArg, } -pub fn run(args: Args) -> Result { - let _amount: u64 = args.amount.field; - let _signer = +pub fn run(args: Args, kairos_server_address: Url) -> Result { + let amount: u64 = args.amount.field; + let signer = Signer::from_private_key_file(args.private_key_path.field).map_err(CryptoError::from)?; + let public_key = signer.to_public_key()?; - // TODO: Create transaction and sign it with `signer`. - - // TODO: Send transaction to the network, using Rust SDK. + let payload = SigningPayload::new_deposit(amount) + .try_into() + .expect("Failed serialize the deposit payload to bytes"); + let signature = signer.sign(&payload)?; + let deposit_request = PayloadBody { + public_key, + payload, + signature, + }; - Ok("ok".to_string()) + client::submit_transaction_request(&kairos_server_address, &deposit_request) + .map_err(Into::::into) + .map(|_| "ok".to_string()) } diff --git a/kairos-cli/src/commands/mod.rs b/kairos-cli/src/commands/mod.rs index 069a0721..13010da8 100644 --- a/kairos-cli/src/commands/mod.rs +++ b/kairos-cli/src/commands/mod.rs @@ -1,15 +1,3 @@ pub mod deposit; pub mod transfer; pub mod 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/src/error.rs b/kairos-cli/src/error.rs index 19558e20..83a253cb 100644 --- a/kairos-cli/src/error.rs +++ b/kairos-cli/src/error.rs @@ -1,8 +1,9 @@ +use crate::client::KairosClientError; +use kairos_crypto::error::CryptoError; + use hex::FromHexError; use thiserror::Error; -use kairos_crypto::error::CryptoError; - #[derive(Error, Debug)] pub enum CliError { /// Cryptography error. @@ -17,4 +18,10 @@ pub enum CliError { #[from] error: FromHexError, }, + /// Kairos HTTP client error + #[error("http client error: {error}")] + KairosClientError { + #[from] + error: KairosClientError, + }, } diff --git a/kairos-cli/src/lib.rs b/kairos-cli/src/lib.rs index 80647025..4a5c52d8 100644 --- a/kairos-cli/src/lib.rs +++ b/kairos-cli/src/lib.rs @@ -1,4 +1,42 @@ +pub mod client; pub mod commands; pub mod common; pub mod error; pub mod utils; + +use crate::error::CliError; + +use clap::{Parser, Subcommand}; +use reqwest::Url; + +#[derive(Parser)] +#[command(name = "Kairos Client", about = "CLI for interacting with Kairos")] +pub struct Cli { + #[command(subcommand)] + pub command: Command, + #[arg(long, value_name = "URL", default_value = "http://0.0.0.0:9999")] + pub kairos_server_address: Url, +} + +#[derive(Subcommand)] +pub enum Command { + #[command(about = "Deposits funds into your account")] + Deposit(commands::deposit::Args), + #[command(about = "Transfers funds to another account")] + Transfer(commands::transfer::Args), + #[command(about = "Withdraws funds from your account")] + Withdraw(commands::withdraw::Args), +} + +pub fn run( + Cli { + command, + kairos_server_address, + }: Cli, +) -> Result { + match command { + Command::Deposit(args) => commands::deposit::run(args, kairos_server_address), + Command::Transfer(args) => commands::transfer::run(args), + Command::Withdraw(args) => commands::withdraw::run(args), + } +} diff --git a/kairos-cli/tests/cli_tests.rs b/kairos-cli/tests/cli_tests.rs index 2893ba7d..739ec75e 100644 --- a/kairos-cli/tests/cli_tests.rs +++ b/kairos-cli/tests/cli_tests.rs @@ -8,17 +8,25 @@ fn fixture_path(relative_path: &str) -> PathBuf { path } -#[test] -fn deposit_successful_with_ed25519() { - let secret_key_path = fixture_path("ed25519/secret_key.pem"); +#[tokio::test] +async fn deposit_successful_with_ed25519() { + let kairos = kairos_test_utils::kairos::Kairos::run().await.unwrap(); - 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().success().stdout("ok\n"); + tokio::task::spawn_blocking(move || { + let secret_key_path = fixture_path("ed25519/secret_key.pem"); + + let mut cmd = Command::cargo_bin("kairos-cli").unwrap(); + cmd.arg("--kairos-server-address") + .arg(kairos.url.as_str()) + .arg("deposit") + .arg("--amount") + .arg("123") + .arg("--private-key") + .arg(secret_key_path); + cmd.assert().success().stdout("ok\n"); + }) + .await + .unwrap(); } #[test] diff --git a/kairos-server/src/lib.rs b/kairos-server/src/lib.rs index c8e9dd47..add7f1c5 100644 --- a/kairos-server/src/lib.rs +++ b/kairos-server/src/lib.rs @@ -31,7 +31,7 @@ pub async fn run(config: ServerConfig) { let listener = tokio::net::TcpListener::bind(config.socket_addr) .await - .unwrap(); + .unwrap_or_else(|err| panic!("Failed to bind to address {}: {}", config.socket_addr, err)); tracing::info!("listening on `{}`", listener.local_addr().unwrap()); axum::serve(listener, app) diff --git a/nixos/tests/end-to-end.nix b/nixos/tests/end-to-end.nix index ab58c19e..fe0b9f70 100644 --- a/nixos/tests/end-to-end.nix +++ b/nixos/tests/end-to-end.nix @@ -76,7 +76,7 @@ nixosTest { client.succeed("curl --fail-with-body -X POST http://kairos/api/v1/withdraw -H 'Content-Type: application/json' -d '{}'".format(json.dumps(withdraw_request))) # CLI with ed25519 - cli_output = client.succeed("kairos-cli deposit --amount 1000 --private-key ${testResources}/ed25519/secret_key.pem") + cli_output = client.succeed("kairos-cli --kairos-server-address http://kairos deposit --amount 1000 --private-key ${testResources}/ed25519/secret_key.pem") assert "ok\n" in cli_output cli_output = client.succeed("kairos-cli transfer --recipient '01a26419a7d82b2263deaedea32d35eee8ae1c850bd477f62a82939f06e80df356' --amount 1000 --private-key ${testResources}/ed25519/secret_key.pem") @@ -86,7 +86,7 @@ nixosTest { assert "ok\n" in cli_output # CLI with secp256k1 - cli_output = client.succeed("kairos-cli deposit --amount 1000 --private-key ${testResources}/secp256k1/secret_key.pem") + cli_output = client.succeed("kairos-cli --kairos-server-address http://kairos deposit --amount 1000 --private-key ${testResources}/secp256k1/secret_key.pem") assert "ok\n" in cli_output cli_output = client.succeed("kairos-cli transfer --recipient '01a26419a7d82b2263deaedea32d35eee8ae1c850bd477f62a82939f06e80df356' --amount 1000 --private-key ${testResources}/secp256k1/secret_key.pem")