Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate ASN.1 transaction format with server #53

Merged
merged 10 commits into from
Mar 27, 2024
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions kairos-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions kairos-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
31 changes: 21 additions & 10 deletions kairos-server/src/routes/deposit.rs
Original file line number Diff line number Diff line change
@@ -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<LockedBatchState>,
Json(Deposit { public_key, amount }): Json<Deposit>,
Json(body): Json<PayloadBody>,
) -> 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");
Expand Down
14 changes: 14 additions & 0 deletions kairos-server/src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>,
#[serde(deserialize_with = "hex_to_vec", serialize_with = "vec_to_hex")]
pub signature: Signature,
}
39 changes: 22 additions & 17 deletions kairos-server/src/routes/transfer.rs
Original file line number Diff line number Diff line change
@@ -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<LockedBatchState>,
Json(Transfer {
from,
signature,
to,
amount,
}): Json<Transfer>,
Json(body): Json<PayloadBody>,
) -> 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"),
Expand Down
34 changes: 20 additions & 14 deletions kairos-server/src/routes/withdraw.rs
Original file line number Diff line number Diff line change
@@ -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<LockedBatchState>,
Json(Withdrawal {
public_key,
signature,
amount,
}): Json<Withdrawal>,
Json(body): Json<PayloadBody>,
) -> 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");
Expand Down
7 changes: 3 additions & 4 deletions kairos-server/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RwLock<BatchState>>;

Expand Down
78 changes: 78 additions & 0 deletions kairos-server/src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use serde::de::{self, Visitor};
use serde::Deserializer;
use std::fmt;

// Custom field deserializer for hex-encoded string to Vec<u8>.
pub fn hex_to_vec<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
struct HexVisitor;

impl<'de> Visitor<'de> for HexVisitor {
type Value = Vec<u8>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string containing hex-encoded data")
}

fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
hex::decode(value).map_err(de::Error::custom)
}
}

deserializer.deserialize_str(HexVisitor)
}

// Custom field serializer for Vec<u8> to hex-encoded string.
pub fn vec_to_hex<S>(data: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
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<u8>,
}

#[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<HexEncoded, _> = 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.
}
}
Loading
Loading