From bd4792bf256b667d84c927d13e357856734432d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijan=20Petri=C4=8Devi=C4=87?= Date: Fri, 3 May 2024 14:09:25 -0500 Subject: [PATCH 1/7] kairos-cli: implement client --- Cargo.lock | 4 +++ kairos-cli/Cargo.toml | 4 +++ kairos-cli/src/client.rs | 58 ++++++++++++++++++++++++++++++++++++++++ kairos-cli/src/error.rs | 11 ++++++-- kairos-cli/src/lib.rs | 1 + 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 kairos-cli/src/client.rs diff --git a/Cargo.lock b/Cargo.lock index 969ea226..ccc68776 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1803,7 +1803,11 @@ dependencies = [ "clap", "hex", "kairos-crypto", + "kairos-server", "predicates", + "reqwest 0.12.4", + "serde", + "serde_json", "thiserror", ] diff --git a/kairos-cli/Cargo.toml b/kairos-cli/Cargo.toml index 69535981..28049504 100644 --- a/kairos-cli/Cargo.toml +++ b/kairos-cli/Cargo.toml @@ -19,6 +19,10 @@ clap = { version = "4.5", features = ["derive", "deprecated"] } hex = "0.4" thiserror = "1" kairos-crypto = { path = "../kairos-crypto", features = ["fs"] } +kairos-server = { path = "../kairos-server" } +reqwest = { version = "0.12", features = ["json"] } +serde_json = "1.0" +serde = "1.0" [dev-dependencies] assert_cmd = "2" diff --git a/kairos-cli/src/client.rs b/kairos-cli/src/client.rs new file mode 100644 index 00000000..c3dc077f --- /dev/null +++ b/kairos-cli/src/client.rs @@ -0,0 +1,58 @@ +use kairos_server::routes::PayloadBody; + +use reqwest::{Client, 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 async fn submit_transaction_request( + client: &Client, + base_url: &Url, + deposit_request: &PayloadBody, +) -> Result<(), KairosClientError> { + let url = base_url.join("/api/v1/deposit").unwrap(); + let response = client + .post(url) + .header("Content-Type", "application/json") + .json(deposit_request) + .send() + .await + .map_err(Into::::into)?; + let status = response.status(); + if !status.is_success() { + Err(KairosClientError::ResponseError(status.to_string())) + } else { + Ok(()) + } +} 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..6e70eb13 100644 --- a/kairos-cli/src/lib.rs +++ b/kairos-cli/src/lib.rs @@ -1,3 +1,4 @@ +pub mod client; pub mod commands; pub mod common; pub mod error; From fa7568ae6cae04d7d8438405473243b348d14609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijan=20Petri=C4=8Devi=C4=87?= Date: Fri, 3 May 2024 14:10:46 -0500 Subject: [PATCH 2/7] kairos-cli: implement call to deposit endpoint and integration test --- Cargo.lock | 3 +++ kairos-cli/Cargo.toml | 3 +++ kairos-cli/bin/main.rs | 22 +++-------------- kairos-cli/src/commands/deposit.rs | 34 +++++++++++++++++++++----- kairos-cli/src/commands/mod.rs | 12 --------- kairos-cli/src/lib.rs | 39 ++++++++++++++++++++++++++++++ kairos-cli/tests/cli_tests.rs | 28 +++++++++++++-------- 7 files changed, 94 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ccc68776..63841aec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1804,11 +1804,14 @@ dependencies = [ "hex", "kairos-crypto", "kairos-server", + "kairos-test-utils", + "kairos-tx", "predicates", "reqwest 0.12.4", "serde", "serde_json", "thiserror", + "tokio", ] [[package]] diff --git a/kairos-cli/Cargo.toml b/kairos-cli/Cargo.toml index 28049504..9e8ead66 100644 --- a/kairos-cli/Cargo.toml +++ b/kairos-cli/Cargo.toml @@ -19,7 +19,9 @@ 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" } +tokio = { version = "1" } reqwest = { version = "0.12", features = ["json"] } serde_json = "1.0" serde = "1.0" @@ -27,3 +29,4 @@ serde = "1.0" [dev-dependencies] 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/commands/deposit.rs b/kairos-cli/src/commands/deposit.rs index b4b95c99..425e4eb6 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,31 @@ 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)?; - // TODO: Create transaction and sign it with `signer`. + let client = reqwest::Client::new(); + let public_key = signer.to_public_key()?; - // 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()) + tokio::runtime::Runtime::new() + .unwrap() + .block_on(client::submit_transaction_request( + &client, + &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/lib.rs b/kairos-cli/src/lib.rs index 6e70eb13..5e8427f0 100644 --- a/kairos-cli/src/lib.rs +++ b/kairos-cli/src/lib.rs @@ -3,3 +3,42 @@ 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")] + pub kairos_server_address: Option, +} + +#[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 { + let kairos_server_address = + kairos_server_address.unwrap_or(Url::parse("http://0.0.0.0:9999").unwrap()); + 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] From 2a4916eb01696f2990199ec37ac83cd4664e2486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijan=20Petri=C4=8Devi=C4=87?= Date: Fri, 3 May 2024 14:31:59 -0500 Subject: [PATCH 3/7] nixos/end-to-end: use kairos-server address when depositing --- nixos/tests/end-to-end.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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") From 31b4c715ca55533de1d583e8ad434c55f397b78c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijan=20Petri=C4=8Devi=C4=87?= Date: Mon, 6 May 2024 09:47:12 -0500 Subject: [PATCH 4/7] kairos-cli: use blocking client --- Cargo.lock | 1 + kairos-cli/Cargo.toml | 4 ++-- kairos-cli/src/client.rs | 7 +++---- kairos-cli/src/commands/deposit.rs | 10 +--------- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 63841aec..bc0eda5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2765,6 +2765,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 9e8ead66..43122a0f 100644 --- a/kairos-cli/Cargo.toml +++ b/kairos-cli/Cargo.toml @@ -21,12 +21,12 @@ thiserror = "1" kairos-crypto = { path = "../kairos-crypto", features = ["fs"] } kairos-tx = { path = "../kairos-tx" } kairos-server = { path = "../kairos-server" } -tokio = { version = "1" } -reqwest = { version = "0.12", features = ["json"] } +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/src/client.rs b/kairos-cli/src/client.rs index c3dc077f..f4bb4ff3 100644 --- a/kairos-cli/src/client.rs +++ b/kairos-cli/src/client.rs @@ -1,6 +1,6 @@ use kairos_server::routes::PayloadBody; -use reqwest::{Client, Url}; +use reqwest::{blocking, Url}; use serde::{Deserialize, Serialize}; use std::fmt; @@ -36,18 +36,17 @@ impl From for KairosClientError { } } -pub async fn submit_transaction_request( - client: &Client, +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() - .await .map_err(Into::::into)?; let status = response.status(); if !status.is_success() { diff --git a/kairos-cli/src/commands/deposit.rs b/kairos-cli/src/commands/deposit.rs index 425e4eb6..3b930b4d 100644 --- a/kairos-cli/src/commands/deposit.rs +++ b/kairos-cli/src/commands/deposit.rs @@ -24,8 +24,6 @@ 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 client = reqwest::Client::new(); let public_key = signer.to_public_key()?; let payload = SigningPayload::new_deposit(amount) @@ -38,13 +36,7 @@ pub fn run(args: Args, kairos_server_address: Url) -> Result { signature, }; - tokio::runtime::Runtime::new() - .unwrap() - .block_on(client::submit_transaction_request( - &client, - &kairos_server_address, - &deposit_request, - )) + client::submit_transaction_request(&kairos_server_address, &deposit_request) .map_err(Into::::into) .map(|_| "ok".to_string()) } From 9d0ae91ca85582c932c3a81a8c527628baaf8392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijan=20Petri=C4=8Devi=C4=87?= Date: Mon, 6 May 2024 10:52:11 -0500 Subject: [PATCH 5/7] kairos-cli: map to KairosServerError when server returns no-success --- kairos-cli/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kairos-cli/src/client.rs b/kairos-cli/src/client.rs index f4bb4ff3..7f970c35 100644 --- a/kairos-cli/src/client.rs +++ b/kairos-cli/src/client.rs @@ -50,7 +50,7 @@ pub fn submit_transaction_request( .map_err(Into::::into)?; let status = response.status(); if !status.is_success() { - Err(KairosClientError::ResponseError(status.to_string())) + Err(KairosClientError::KairosServerError(status.to_string())) } else { Ok(()) } From 0c980efa500346d61c26d90b31d252e9e8403b5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijan=20Petri=C4=8Devi=C4=87?= Date: Mon, 6 May 2024 10:56:34 -0500 Subject: [PATCH 6/7] kairos-cli: use clap default_value --- kairos-cli/src/lib.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/kairos-cli/src/lib.rs b/kairos-cli/src/lib.rs index 5e8427f0..4a5c52d8 100644 --- a/kairos-cli/src/lib.rs +++ b/kairos-cli/src/lib.rs @@ -14,8 +14,8 @@ use reqwest::Url; pub struct Cli { #[command(subcommand)] pub command: Command, - #[arg(long, value_name = "URL")] - pub kairos_server_address: Option, + #[arg(long, value_name = "URL", default_value = "http://0.0.0.0:9999")] + pub kairos_server_address: Url, } #[derive(Subcommand)] @@ -34,8 +34,6 @@ pub fn run( kairos_server_address, }: Cli, ) -> Result { - let kairos_server_address = - kairos_server_address.unwrap_or(Url::parse("http://0.0.0.0:9999").unwrap()); match command { Command::Deposit(args) => commands::deposit::run(args, kairos_server_address), Command::Transfer(args) => commands::transfer::run(args), From 1da52086a0839defc3fc02f42108566e92df052a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijan=20Petri=C4=8Devi=C4=87?= Date: Tue, 7 May 2024 11:28:28 -0500 Subject: [PATCH 7/7] kairos-server: output the bind address on failure --- kairos-server/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)