diff --git a/Cargo.lock b/Cargo.lock index e42fd1ad..1e244a81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1709,6 +1709,8 @@ dependencies = [ "axum-extra", "axum-test", "dotenvy", + "hex", + "kairos-tx", "proptest", "serde", "serde_json", diff --git a/kairos-server/Cargo.toml b/kairos-server/Cargo.toml index d390e02e..c2f055a7 100644 --- a/kairos-server/Cargo.toml +++ b/kairos-server/Cargo.toml @@ -25,6 +25,8 @@ serde_json = "1" tokio = { version = "1", features = ["full", "tracing", "macros"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] } +hex = "0.4" +kairos-tx = { path = "../kairos-tx" } [dev-dependencies] proptest = "1" diff --git a/kairos-server/src/lib.rs b/kairos-server/src/lib.rs index 7bc45aae..a0fc419c 100644 --- a/kairos-server/src/lib.rs +++ b/kairos-server/src/lib.rs @@ -3,6 +3,8 @@ pub mod errors; pub mod routes; pub mod state; +mod utils; + use axum::Router; use axum_extra::routing::RouterExt; use state::LockedBatchState; diff --git a/kairos-server/src/routes/deposit.rs b/kairos-server/src/routes/deposit.rs index b77ccb7f..4b14a844 100644 --- a/kairos-server/src/routes/deposit.rs +++ b/kairos-server/src/routes/deposit.rs @@ -1,29 +1,40 @@ use std::ops::Deref; -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use axum::{extract::State, http::StatusCode, Json}; use axum_extra::routing::TypedPath; -use serde::{Deserialize, Serialize}; use tracing::*; -use crate::{state::LockedBatchState, AppErr, PublicKey}; +use kairos_tx::asn::{SigningPayload, TransactionBody}; + +use crate::routes::PayloadBody; +use crate::{state::LockedBatchState, AppErr}; #[derive(TypedPath, Debug, Clone, Copy)] #[typed_path("/api/v1/deposit")] pub struct DepositPath; -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub struct Deposit { - pub public_key: PublicKey, - pub amount: u64, -} - #[instrument(level = "trace", skip(state), ret)] pub async fn deposit_handler( _: DepositPath, state: State, - Json(Deposit { public_key, amount }): Json, + Json(body): Json, ) -> Result<(), AppErr> { + tracing::info!("parsing transaction data"); + let signing_payload: SigningPayload = + body.payload.as_slice().try_into().context("payload err")?; + let deposit = match signing_payload.body { + TransactionBody::Deposit(deposit) => deposit, + _ => { + return Err(AppErr::set_status( + anyhow!("invalid transaction type"), + StatusCode::BAD_REQUEST, + )) + } + }; + let amount = u64::try_from(deposit.amount).context("invalid amount")?; + let public_key = body.public_key; + tracing::info!("TODO: verifying deposit"); tracing::info!("TODO: adding deposit to batch"); diff --git a/kairos-server/src/routes/mod.rs b/kairos-server/src/routes/mod.rs index 1ebcb1d2..cc2b615e 100644 --- a/kairos-server/src/routes/mod.rs +++ b/kairos-server/src/routes/mod.rs @@ -5,3 +5,17 @@ pub mod withdraw; pub use deposit::deposit_handler; pub use transfer::transfer_handler; pub use withdraw::withdraw_handler; + +use crate::utils::{hex_to_vec, vec_to_hex}; +use crate::{PublicKey, Signature}; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct PayloadBody { + pub public_key: PublicKey, + #[serde(deserialize_with = "hex_to_vec", serialize_with = "vec_to_hex")] + pub payload: Vec, + #[serde(deserialize_with = "hex_to_vec", serialize_with = "vec_to_hex")] + pub signature: Signature, +} diff --git a/kairos-server/src/routes/transfer.rs b/kairos-server/src/routes/transfer.rs index dc18468a..5a40c2f3 100644 --- a/kairos-server/src/routes/transfer.rs +++ b/kairos-server/src/routes/transfer.rs @@ -1,34 +1,39 @@ -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use axum::{extract::State, http::StatusCode, Json}; use axum_extra::routing::TypedPath; -use serde::{Deserialize, Serialize}; use tracing::instrument; -use crate::{state::LockedBatchState, AppErr, PublicKey, Signature}; +use kairos_tx::asn::{SigningPayload, TransactionBody}; + +use crate::routes::PayloadBody; +use crate::{state::LockedBatchState, AppErr, PublicKey}; #[derive(TypedPath)] #[typed_path("/api/v1/transfer")] pub struct TransferPath; -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub struct Transfer { - pub from: PublicKey, - pub signature: Signature, - pub to: PublicKey, - pub amount: u64, -} - #[instrument(level = "trace", skip(state), ret)] pub async fn transfer_handler( _: TransferPath, State(state): State, - Json(Transfer { - from, - signature, - to, - amount, - }): Json, + Json(body): Json, ) -> Result<(), AppErr> { + tracing::info!("parsing transaction data"); + let signing_payload: SigningPayload = + body.payload.as_slice().try_into().context("payload err")?; + let transfer = match signing_payload.body { + TransactionBody::Transfer(transfer) => transfer, + _ => { + return Err(AppErr::set_status( + anyhow!("invalid transaction type"), + StatusCode::BAD_REQUEST, + )) + } + }; + let amount = u64::try_from(transfer.amount).context("invalid amount")?; + let from = body.public_key; + let to = PublicKey::from(transfer.recipient); + if amount == 0 { return Err(AppErr::set_status( anyhow!("transfer amount must be greater than 0"), diff --git a/kairos-server/src/routes/withdraw.rs b/kairos-server/src/routes/withdraw.rs index 7f746331..b669eee5 100644 --- a/kairos-server/src/routes/withdraw.rs +++ b/kairos-server/src/routes/withdraw.rs @@ -1,32 +1,38 @@ -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use axum::{extract::State, http::StatusCode, Json}; use axum_extra::routing::TypedPath; -use serde::{Deserialize, Serialize}; use tracing::*; +use kairos_tx::asn::{SigningPayload, TransactionBody}; + +use crate::routes::PayloadBody; use crate::{state::LockedBatchState, AppErr, PublicKey}; #[derive(Debug, TypedPath)] #[typed_path("/api/v1/withdraw")] pub struct WithdrawPath; -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub struct Withdrawal { - pub public_key: PublicKey, - pub signature: String, - pub amount: u64, -} - #[instrument(level = "trace", skip(state), ret)] pub async fn withdraw_handler( _: WithdrawPath, State(state): State, - Json(Withdrawal { - public_key, - signature, - amount, - }): Json, + Json(body): Json, ) -> Result<(), AppErr> { + tracing::info!("parsing transaction data"); + let signing_payload: SigningPayload = + body.payload.as_slice().try_into().context("payload err")?; + let withdrawal = match signing_payload.body { + TransactionBody::Withdrawal(withdrawal) => withdrawal, + _ => { + return Err(AppErr::set_status( + anyhow!("invalid transaction type"), + StatusCode::BAD_REQUEST, + )) + } + }; + let amount = u64::try_from(withdrawal.amount).context("invalid amount")?; + let public_key = body.public_key; + tracing::info!("TODO: verifying withdrawal signature"); tracing::info!("verifying withdrawal sender has sufficient funds"); diff --git a/kairos-server/src/state.rs b/kairos-server/src/state.rs index 12563194..3b4b3a71 100644 --- a/kairos-server/src/state.rs +++ b/kairos-server/src/state.rs @@ -5,10 +5,9 @@ use std::{ use tokio::sync::RwLock; -use crate::{ - routes::{deposit::Deposit, transfer::Transfer, withdraw::Withdrawal}, - PublicKey, -}; +use kairos_tx::asn::{Deposit, Transfer, Withdrawal}; + +use crate::PublicKey; pub type LockedBatchState = Arc>; diff --git a/kairos-server/src/utils.rs b/kairos-server/src/utils.rs new file mode 100644 index 00000000..ef811efa --- /dev/null +++ b/kairos-server/src/utils.rs @@ -0,0 +1,78 @@ +use serde::de::{self, Visitor}; +use serde::Deserializer; +use std::fmt; + +// Custom field deserializer for hex-encoded string to Vec. +pub fn hex_to_vec<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + struct HexVisitor; + + impl<'de> Visitor<'de> for HexVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string containing hex-encoded data") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + hex::decode(value).map_err(de::Error::custom) + } + } + + deserializer.deserialize_str(HexVisitor) +} + +// Custom field serializer for Vec to hex-encoded string. +pub fn vec_to_hex(data: &Vec, serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&hex::encode(data)) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::{Deserialize, Serialize}; + + // Define a dummy struct to use for (de)serialization testing. + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct HexEncoded { + #[serde(deserialize_with = "hex_to_vec", serialize_with = "vec_to_hex")] + data: Vec, + } + + #[test] + fn test_parsing_valid_hex() { + let json_str = r#"{"data": "48656c6c6f"}"#; // "Hello" in hex. + let expected = HexEncoded { + data: b"Hello".to_vec(), + }; + + let result: HexEncoded = serde_json::from_str(json_str).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_parsing_invalid_hex() { + let json_str = r#"{"data": "foobar"}"#; // Invalid hex characters. + + let result: Result = serde_json::from_str(json_str); + assert!(result.is_err()); + } + + #[test] + fn test_serializing_to_hex() { + let encoded = HexEncoded { + data: b"Hello".to_vec(), + }; + + let result = serde_json::to_string(&encoded).unwrap(); + assert_eq!(result, r#"{"data":"48656c6c6f"}"#); // "Hello" in hex. + } +} diff --git a/kairos-server/tests/transactions.rs b/kairos-server/tests/transactions.rs index 3ae42dae..d0a0b140 100644 --- a/kairos-server/tests/transactions.rs +++ b/kairos-server/tests/transactions.rs @@ -3,13 +3,10 @@ use std::sync::OnceLock; use axum_extra::routing::TypedPath; use axum_test::{TestServer, TestServerConfig}; use kairos_server::{ - routes::{ - deposit::{Deposit, DepositPath}, - transfer::{Transfer, TransferPath}, - withdraw::{WithdrawPath, Withdrawal}, - }, + routes::{deposit::DepositPath, transfer::TransferPath, withdraw::WithdrawPath, PayloadBody}, state::BatchState, }; +use kairos_tx::helpers::{make_deposit, make_transfer, make_withdrawal}; use tracing_subscriber::{prelude::*, EnvFilter}; static TEST_ENVIRONMENT: OnceLock<()> = OnceLock::new(); @@ -30,9 +27,12 @@ fn new_test_app() -> TestServer { async fn test_deposit_withdraw() { let server = new_test_app(); - let deposit = Deposit { + let nonce: u64 = 1; + let amount: u64 = 100; + let deposit = PayloadBody { public_key: "alice_key".into(), - amount: 100, + payload: make_deposit(nonce, amount).unwrap(), + signature: vec![], }; // no arguments @@ -55,10 +55,12 @@ async fn test_deposit_withdraw() { .await .assert_status_failure(); - let withdrawal = Withdrawal { + let nonce: u64 = 1; + let amount: u64 = 50; + let withdrawal = PayloadBody { public_key: "alice_key".into(), - signature: "TODO".into(), - amount: 50, + payload: make_withdrawal(nonce, amount).unwrap(), + signature: vec![], }; // first withdrawal @@ -68,23 +70,33 @@ async fn test_deposit_withdraw() { .await .assert_status_success(); + let nonce: u64 = 1; + let amount: u64 = 51; + let withdrawal = PayloadBody { + public_key: "alice_key".into(), + payload: make_withdrawal(nonce, amount).unwrap(), + signature: vec![], + }; + // withdrawal with insufficient funds server .post(WithdrawPath.to_uri().path()) - .json(&Withdrawal { - amount: 51, - ..withdrawal.clone() - }) + .json(&withdrawal) .await .assert_status_failure(); + let nonce: u64 = 1; + let amount: u64 = 50; + let withdrawal = PayloadBody { + public_key: "alice_key".into(), + payload: make_withdrawal(nonce, amount).unwrap(), + signature: vec![], + }; + // second withdrawal server .post(WithdrawPath.to_uri().path()) - .json(&Withdrawal { - amount: 50, - ..withdrawal.clone() - }) + .json(&withdrawal) .await .assert_status_success(); @@ -99,22 +111,29 @@ async fn test_deposit_withdraw() { async fn test_deposit_transfer_withdraw() { let server = new_test_app(); - let deposit = Deposit { + let nonce: u64 = 1; + let amount: u64 = 100; + let deposit = PayloadBody { public_key: "alice_key".into(), - amount: 100, + payload: make_deposit(nonce, amount).unwrap(), + signature: vec![], }; - let transfer = Transfer { - from: "alice_key".into(), - signature: "TODO".into(), - to: "bob_key".into(), - amount: 50, + let nonce: u64 = 1; + let amount: u64 = 50; + let recipient: &[u8] = "bob_key".as_bytes(); + let transfer = PayloadBody { + public_key: "alice_key".into(), + payload: make_transfer(nonce, recipient, amount).unwrap(), + signature: vec![], }; - let withdrawal = Withdrawal { + let nonce: u64 = 1; + let amount: u64 = 50; + let withdrawal = PayloadBody { public_key: "bob_key".into(), - signature: "TODO".into(), - amount: 50, + payload: make_withdrawal(nonce, amount).unwrap(), + signature: vec![], }; // deposit