From 27684cf3c1b5a3f75cbb015fe3ebe3e79593723f Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 27 Apr 2024 11:51:48 -0700 Subject: [PATCH 1/4] refactor: move handlers up a level --- clientd-stateless/examples/cashu_encoding.rs | 1 - clientd-stateless/src/main.rs | 35 ++-- .../src/router/{handlers => }/check.rs | 0 clientd-stateless/src/router/handlers/mod.rs | 171 ----------------- .../src/router/{handlers => }/info.rs | 0 .../src/router/{handlers => }/keys.rs | 0 .../src/router/{handlers => }/keysets.rs | 0 .../src/router/{handlers => }/melt/method.rs | 2 +- .../src/router/{handlers => }/melt/mod.rs | 0 .../src/router/{handlers => }/melt/quote.rs | 0 .../src/router/{handlers => }/mint/method.rs | 2 +- .../src/router/{handlers => }/mint/mod.rs | 0 .../src/router/{handlers => }/mint/quote.rs | 0 clientd-stateless/src/router/mod.rs | 172 +++++++++++++++++- .../src/router/{handlers => }/swap.rs | 0 15 files changed, 186 insertions(+), 197 deletions(-) delete mode 100644 clientd-stateless/examples/cashu_encoding.rs rename clientd-stateless/src/router/{handlers => }/check.rs (100%) delete mode 100644 clientd-stateless/src/router/handlers/mod.rs rename clientd-stateless/src/router/{handlers => }/info.rs (100%) rename clientd-stateless/src/router/{handlers => }/keys.rs (100%) rename clientd-stateless/src/router/{handlers => }/keysets.rs (100%) rename clientd-stateless/src/router/{handlers => }/melt/method.rs (99%) rename clientd-stateless/src/router/{handlers => }/melt/mod.rs (100%) rename clientd-stateless/src/router/{handlers => }/melt/quote.rs (100%) rename clientd-stateless/src/router/{handlers => }/mint/method.rs (98%) rename clientd-stateless/src/router/{handlers => }/mint/mod.rs (100%) rename clientd-stateless/src/router/{handlers => }/mint/quote.rs (100%) rename clientd-stateless/src/router/{handlers => }/swap.rs (100%) diff --git a/clientd-stateless/examples/cashu_encoding.rs b/clientd-stateless/examples/cashu_encoding.rs deleted file mode 100644 index 8b13789..0000000 --- a/clientd-stateless/examples/cashu_encoding.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/clientd-stateless/src/main.rs b/clientd-stateless/src/main.rs index 941bd38..403acd7 100644 --- a/clientd-stateless/src/main.rs +++ b/clientd-stateless/src/main.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use anyhow::Result; use axum::http::Method; use fedimint_core::api::InviteCode; -use router::handlers; +use router::{check, info, keys, keysets, melt, mint, swap}; use tower_http::cors::{Any, CorsLayer}; use tower_http::trace::TraceLayer; use tracing::info; @@ -156,31 +156,22 @@ async fn main() -> Result<()> { /// - DLEQ in BlindedSignature for Mint to User fn cashu_v1_rest() -> Router { Router::new() - .route("/keys", get(handlers::keys::handle_keys)) - .route( - "/keys/:keyset_id", - get(handlers::keys::handle_keys_keyset_id), - ) - .route("/keysets", get(handlers::keysets::handle_keysets)) - .route("/swap", post(handlers::swap::handle_swap)) - .route( - "/mint/quote/:method", - get(handlers::mint::quote::handle_method), - ) + .route("/keys", get(keys::handle_keys)) + .route("/keys/:keyset_id", get(keys::handle_keys_keyset_id)) + .route("/keysets", get(keysets::handle_keysets)) + .route("/swap", post(swap::handle_swap)) + .route("/mint/quote/:method", get(mint::quote::handle_method)) .route( "/mint/quote/:method/:quote_id", - get(handlers::mint::quote::handle_method_quote_id), - ) - .route("/mint/:method", post(handlers::mint::method::handle_method)) - .route( - "/melt/quote/:method", - get(handlers::melt::quote::handle_method), + get(mint::quote::handle_method_quote_id), ) + .route("/mint/:method", post(mint::method::handle_method)) + .route("/melt/quote/:method", get(melt::quote::handle_method)) .route( "/melt/quote/:method/:quote_id", - get(handlers::melt::quote::handle_method_quote_id), + get(melt::quote::handle_method_quote_id), ) - .route("/melt/:method", post(handlers::melt::method::handle_method)) - .route("/info", get(handlers::info::handle_info)) - .route("/check", post(handlers::check::handle_check)) + .route("/melt/:method", post(melt::method::handle_method)) + .route("/info", get(info::handle_info)) + .route("/check", post(check::handle_check)) } diff --git a/clientd-stateless/src/router/handlers/check.rs b/clientd-stateless/src/router/check.rs similarity index 100% rename from clientd-stateless/src/router/handlers/check.rs rename to clientd-stateless/src/router/check.rs diff --git a/clientd-stateless/src/router/handlers/mod.rs b/clientd-stateless/src/router/handlers/mod.rs deleted file mode 100644 index f4f3512..0000000 --- a/clientd-stateless/src/router/handlers/mod.rs +++ /dev/null @@ -1,171 +0,0 @@ -use std::collections::BTreeMap; -use std::fmt; -use std::str::FromStr; - -use anyhow::anyhow; -use base64::Engine; -use bitcoin::secp256k1::{Secp256k1, SecretKey}; -use bitcoin::KeyPair; -use fedimint_core::api::InviteCode; -use fedimint_core::config::FederationIdPrefix; -use fedimint_core::db::DatabaseValue; -use fedimint_core::module::registry::ModuleDecoderRegistry; -use fedimint_core::{Amount, TieredMulti}; -use fedimint_mint_client::{OOBNotes, SpendableNote}; -use serde::de::Error; -use serde::{Deserialize, Serialize}; -use tbs::Signature; - -pub mod check; -pub mod info; -pub mod keys; -pub mod keysets; -pub mod melt; -pub mod mint; -pub mod swap; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Proof { - // Amount unassociated with the unit - amount: u64, - // keyset id -> FederationId - id: String, - // secret -> hex encoded spend key's secret key - secret: String, - // signature -> hex encoded BLS signature - #[allow(non_snake_case)] - C: String, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Token { - mint: String, - proofs: Vec, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct TokenV3 { - pub token: Vec, - pub unit: Option, - pub memo: Option, -} - -impl TokenV3 { - /// Serializes the `Token` struct to a base64 URL-safe string without - /// padding and with the version prefix. - pub fn serialize(&self) -> Result { - let json = serde_json::to_string(self) - .map_err(|e| serde_json::Error::custom(format!("Failed to serialize token: {}", e)))?; - let base64_token = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json.as_bytes()); - Ok(format!("cashuA{}", base64_token)) - } - - /// Deserializes a base64 URL-safe string without padding (with version - /// prefix) back to a `Token` struct. - pub fn deserialize(encoded: &str) -> Result { - if !encoded.starts_with("cashuA") { - return Err(serde_json::Error::custom("Invalid token format")); - } - let base64_token = &encoded[6..]; - let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(base64_token.as_bytes()) - .map_err(|e| { - serde_json::Error::custom(format!("Failed to decode base64 token: {}", e)) - })?; - let json = String::from_utf8(bytes).map_err(|e| { - serde_json::Error::custom(format!("Failed to decode base64 token: {}", e)) - })?; - serde_json::from_str(&json) - } - - pub fn from_oobnotes(notes: OOBNotes, invite_code: InviteCode) -> Result { - let mut token = TokenV3 { - token: vec![], - // Always msats - unit: Some("msat".to_string()), - // Federation Invite Code - memo: Some(invite_code.to_string()), - }; - for (amount, note) in notes.notes().iter() { - let mut proofs = vec![]; - for spendable_note in note.iter() { - let proof = Proof { - amount: amount.msats, - // stick the federation id prefix here instead of keyset - id: notes.federation_id_prefix().to_string(), - secret: hex::encode(spendable_note.spend_key.secret_key().to_bytes()), - C: hex::encode(spendable_note.signature.to_bytes()), - }; - proofs.push(proof); - } - token.token.push(Token { - mint: notes.federation_id_prefix().to_string(), - proofs, - }); - } - Ok(token) - } - - fn to_oobnotes(&self, modules: &ModuleDecoderRegistry) -> Result { - let federation_id_prefix = match self.token.first().map(|t| &t.proofs[0].id) { - Some(id) => FederationIdPrefix::from_str(id)?, - None => return Err(anyhow!("No token found")), - }; - let secp = Secp256k1::new(); - let mut notes_map = BTreeMap::>::new(); - for t in self.token.iter() { - for proof in t.proofs.iter() { - let signature_bytes = hex::decode(&proof.C) - .map_err(|e| anyhow!("Failed to decode spendable note signature: {}", e))?; - let signature = Signature::from_bytes(&signature_bytes, modules)?; - let secret_key_bytes = hex::decode(&proof.secret) - .map_err(|e| anyhow!("Failed to decode spendable note spend key: {}", e))?; - let sk = SecretKey::from_bytes(&secret_key_bytes, modules) - .map_err(|e| anyhow!("Failed to decode spendable note spend key: {}", e))?; - let spend_key = KeyPair::from_secret_key(&secp, &sk); - let spendable_note = SpendableNote { - signature, - spend_key, - }; - let amount = Amount::from_msats(proof.amount); - notes_map.entry(amount).or_default().push(spendable_note); - } - } - let tiered_notes = TieredMulti::new(notes_map); - Ok(OOBNotes::new(federation_id_prefix, tiered_notes)) - } -} - -impl FromStr for TokenV3 { - type Err = serde_json::Error; - - /// Parses a string to create a `Token` struct. - /// Assumes the string is a base64 URL-safe encoded JSON of the `Token` with - /// `cashuA` prefix. - fn from_str(s: &str) -> Result { - TokenV3::deserialize(s) - } -} - -impl fmt::Display for TokenV3 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - match self.serialize() { - Ok(serialized) => write!(f, "{}", serialized), - Err(_) => Err(fmt::Error), - } - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum Unit { - Msat, - Sat, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "lowercase")] -pub enum Method { - Bolt11, - Onchain, -} diff --git a/clientd-stateless/src/router/handlers/info.rs b/clientd-stateless/src/router/info.rs similarity index 100% rename from clientd-stateless/src/router/handlers/info.rs rename to clientd-stateless/src/router/info.rs diff --git a/clientd-stateless/src/router/handlers/keys.rs b/clientd-stateless/src/router/keys.rs similarity index 100% rename from clientd-stateless/src/router/handlers/keys.rs rename to clientd-stateless/src/router/keys.rs diff --git a/clientd-stateless/src/router/handlers/keysets.rs b/clientd-stateless/src/router/keysets.rs similarity index 100% rename from clientd-stateless/src/router/handlers/keysets.rs rename to clientd-stateless/src/router/keysets.rs diff --git a/clientd-stateless/src/router/handlers/melt/method.rs b/clientd-stateless/src/router/melt/method.rs similarity index 99% rename from clientd-stateless/src/router/handlers/melt/method.rs rename to clientd-stateless/src/router/melt/method.rs index 5553c4d..a3bebc8 100644 --- a/clientd-stateless/src/router/handlers/melt/method.rs +++ b/clientd-stateless/src/router/melt/method.rs @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize}; use tracing::{error, info}; use crate::error::AppError; -use crate::router::handlers::{Method, Unit}; +use crate::router::{Method, Unit}; use crate::state::AppState; #[derive(Debug, Deserialize)] diff --git a/clientd-stateless/src/router/handlers/melt/mod.rs b/clientd-stateless/src/router/melt/mod.rs similarity index 100% rename from clientd-stateless/src/router/handlers/melt/mod.rs rename to clientd-stateless/src/router/melt/mod.rs diff --git a/clientd-stateless/src/router/handlers/melt/quote.rs b/clientd-stateless/src/router/melt/quote.rs similarity index 100% rename from clientd-stateless/src/router/handlers/melt/quote.rs rename to clientd-stateless/src/router/melt/quote.rs diff --git a/clientd-stateless/src/router/handlers/mint/method.rs b/clientd-stateless/src/router/mint/method.rs similarity index 98% rename from clientd-stateless/src/router/handlers/mint/method.rs rename to clientd-stateless/src/router/mint/method.rs index 3c7eb13..b37d06e 100644 --- a/clientd-stateless/src/router/handlers/mint/method.rs +++ b/clientd-stateless/src/router/mint/method.rs @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize}; use tracing::error; use crate::error::AppError; -use crate::router::handlers::{Method, Unit}; +use crate::router::{Method, Unit}; use crate::state::AppState; #[derive(Debug, Deserialize)] diff --git a/clientd-stateless/src/router/handlers/mint/mod.rs b/clientd-stateless/src/router/mint/mod.rs similarity index 100% rename from clientd-stateless/src/router/handlers/mint/mod.rs rename to clientd-stateless/src/router/mint/mod.rs diff --git a/clientd-stateless/src/router/handlers/mint/quote.rs b/clientd-stateless/src/router/mint/quote.rs similarity index 100% rename from clientd-stateless/src/router/handlers/mint/quote.rs rename to clientd-stateless/src/router/mint/quote.rs diff --git a/clientd-stateless/src/router/mod.rs b/clientd-stateless/src/router/mod.rs index c3d4495..f4f3512 100644 --- a/clientd-stateless/src/router/mod.rs +++ b/clientd-stateless/src/router/mod.rs @@ -1 +1,171 @@ -pub mod handlers; +use std::collections::BTreeMap; +use std::fmt; +use std::str::FromStr; + +use anyhow::anyhow; +use base64::Engine; +use bitcoin::secp256k1::{Secp256k1, SecretKey}; +use bitcoin::KeyPair; +use fedimint_core::api::InviteCode; +use fedimint_core::config::FederationIdPrefix; +use fedimint_core::db::DatabaseValue; +use fedimint_core::module::registry::ModuleDecoderRegistry; +use fedimint_core::{Amount, TieredMulti}; +use fedimint_mint_client::{OOBNotes, SpendableNote}; +use serde::de::Error; +use serde::{Deserialize, Serialize}; +use tbs::Signature; + +pub mod check; +pub mod info; +pub mod keys; +pub mod keysets; +pub mod melt; +pub mod mint; +pub mod swap; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Proof { + // Amount unassociated with the unit + amount: u64, + // keyset id -> FederationId + id: String, + // secret -> hex encoded spend key's secret key + secret: String, + // signature -> hex encoded BLS signature + #[allow(non_snake_case)] + C: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Token { + mint: String, + proofs: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TokenV3 { + pub token: Vec, + pub unit: Option, + pub memo: Option, +} + +impl TokenV3 { + /// Serializes the `Token` struct to a base64 URL-safe string without + /// padding and with the version prefix. + pub fn serialize(&self) -> Result { + let json = serde_json::to_string(self) + .map_err(|e| serde_json::Error::custom(format!("Failed to serialize token: {}", e)))?; + let base64_token = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json.as_bytes()); + Ok(format!("cashuA{}", base64_token)) + } + + /// Deserializes a base64 URL-safe string without padding (with version + /// prefix) back to a `Token` struct. + pub fn deserialize(encoded: &str) -> Result { + if !encoded.starts_with("cashuA") { + return Err(serde_json::Error::custom("Invalid token format")); + } + let base64_token = &encoded[6..]; + let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(base64_token.as_bytes()) + .map_err(|e| { + serde_json::Error::custom(format!("Failed to decode base64 token: {}", e)) + })?; + let json = String::from_utf8(bytes).map_err(|e| { + serde_json::Error::custom(format!("Failed to decode base64 token: {}", e)) + })?; + serde_json::from_str(&json) + } + + pub fn from_oobnotes(notes: OOBNotes, invite_code: InviteCode) -> Result { + let mut token = TokenV3 { + token: vec![], + // Always msats + unit: Some("msat".to_string()), + // Federation Invite Code + memo: Some(invite_code.to_string()), + }; + for (amount, note) in notes.notes().iter() { + let mut proofs = vec![]; + for spendable_note in note.iter() { + let proof = Proof { + amount: amount.msats, + // stick the federation id prefix here instead of keyset + id: notes.federation_id_prefix().to_string(), + secret: hex::encode(spendable_note.spend_key.secret_key().to_bytes()), + C: hex::encode(spendable_note.signature.to_bytes()), + }; + proofs.push(proof); + } + token.token.push(Token { + mint: notes.federation_id_prefix().to_string(), + proofs, + }); + } + Ok(token) + } + + fn to_oobnotes(&self, modules: &ModuleDecoderRegistry) -> Result { + let federation_id_prefix = match self.token.first().map(|t| &t.proofs[0].id) { + Some(id) => FederationIdPrefix::from_str(id)?, + None => return Err(anyhow!("No token found")), + }; + let secp = Secp256k1::new(); + let mut notes_map = BTreeMap::>::new(); + for t in self.token.iter() { + for proof in t.proofs.iter() { + let signature_bytes = hex::decode(&proof.C) + .map_err(|e| anyhow!("Failed to decode spendable note signature: {}", e))?; + let signature = Signature::from_bytes(&signature_bytes, modules)?; + let secret_key_bytes = hex::decode(&proof.secret) + .map_err(|e| anyhow!("Failed to decode spendable note spend key: {}", e))?; + let sk = SecretKey::from_bytes(&secret_key_bytes, modules) + .map_err(|e| anyhow!("Failed to decode spendable note spend key: {}", e))?; + let spend_key = KeyPair::from_secret_key(&secp, &sk); + let spendable_note = SpendableNote { + signature, + spend_key, + }; + let amount = Amount::from_msats(proof.amount); + notes_map.entry(amount).or_default().push(spendable_note); + } + } + let tiered_notes = TieredMulti::new(notes_map); + Ok(OOBNotes::new(federation_id_prefix, tiered_notes)) + } +} + +impl FromStr for TokenV3 { + type Err = serde_json::Error; + + /// Parses a string to create a `Token` struct. + /// Assumes the string is a base64 URL-safe encoded JSON of the `Token` with + /// `cashuA` prefix. + fn from_str(s: &str) -> Result { + TokenV3::deserialize(s) + } +} + +impl fmt::Display for TokenV3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self.serialize() { + Ok(serialized) => write!(f, "{}", serialized), + Err(_) => Err(fmt::Error), + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Unit { + Msat, + Sat, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum Method { + Bolt11, + Onchain, +} diff --git a/clientd-stateless/src/router/handlers/swap.rs b/clientd-stateless/src/router/swap.rs similarity index 100% rename from clientd-stateless/src/router/handlers/swap.rs rename to clientd-stateless/src/router/swap.rs From e910ae29044d84c73536b83879321f298e916c6e Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Tue, 30 Apr 2024 12:35:51 -0700 Subject: [PATCH 2/4] feat: cashu stuff --- Cargo.lock | 2 +- clientd-stateless/Cargo.toml | 6 +- clientd-stateless/src/cashu.rs | 163 +++++++++++++++++++ clientd-stateless/src/main.rs | 3 +- clientd-stateless/src/router/melt/method.rs | 2 +- clientd-stateless/src/router/mint/method.rs | 2 +- clientd-stateless/src/router/mod.rs | 164 -------------------- 7 files changed, 173 insertions(+), 169 deletions(-) create mode 100644 clientd-stateless/src/cashu.rs diff --git a/Cargo.lock b/Cargo.lock index 5e1bc41..1b15a8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -698,7 +698,7 @@ dependencies = [ ] [[package]] -name = "clientd-statless" +name = "clientd-stateless" version = "0.3.3" dependencies = [ "anyhow", diff --git a/clientd-stateless/Cargo.toml b/clientd-stateless/Cargo.toml index 16fb465..0e88f3b 100644 --- a/clientd-stateless/Cargo.toml +++ b/clientd-stateless/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "clientd-statless" +name = "clientd-stateless" version = "0.3.3" edition = "2021" description = "A stateless fedimint client daemon" @@ -7,6 +7,10 @@ repository = "https://github.com/fedimint/fedimint-clientd" keywords = ["fedimint", "bitcoin", "lightning", "ecash"] license = "MIT" +[[example]] +name = "cashu_encoding" +path = "examples/cashu_encoding.rs" + [dependencies] anyhow = "1.0.75" axum = { version = "0.7.1", features = ["json", "ws"] } diff --git a/clientd-stateless/src/cashu.rs b/clientd-stateless/src/cashu.rs new file mode 100644 index 0000000..e92acb9 --- /dev/null +++ b/clientd-stateless/src/cashu.rs @@ -0,0 +1,163 @@ +use std::collections::BTreeMap; +use std::fmt; +use std::str::FromStr; + +use anyhow::anyhow; +use base64::Engine; +use bitcoin::secp256k1::{Secp256k1, SecretKey}; +use bitcoin::KeyPair; +use fedimint_core::api::InviteCode; +use fedimint_core::config::FederationIdPrefix; +use fedimint_core::db::DatabaseValue; +use fedimint_core::module::registry::ModuleDecoderRegistry; +use fedimint_core::{Amount, TieredMulti}; +use fedimint_mint_client::{OOBNotes, SpendableNote}; +use serde::de::Error; +use serde::{Deserialize, Serialize}; +use tbs::Signature; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Proof { + // Amount unassociated with the unit + amount: u64, + // keyset id -> FederationId + id: String, + // secret -> hex encoded spend key's secret key + secret: String, + // signature -> hex encoded BLS signature + #[allow(non_snake_case)] + C: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Token { + mint: String, + proofs: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TokenV3 { + pub token: Vec, + pub unit: Option, + pub memo: Option, +} + +impl TokenV3 { + /// Serializes the `Token` struct to a base64 URL-safe string without + /// padding and with the version prefix. + pub fn serialize(&self) -> Result { + let json = serde_json::to_string(self) + .map_err(|e| serde_json::Error::custom(format!("Failed to serialize token: {}", e)))?; + let base64_token = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json.as_bytes()); + Ok(format!("cashuA{}", base64_token)) + } + + /// Deserializes a base64 URL-safe string without padding (with version + /// prefix) back to a `Token` struct. + pub fn deserialize(encoded: &str) -> Result { + if !encoded.starts_with("cashuA") { + return Err(serde_json::Error::custom("Invalid token format")); + } + let base64_token = &encoded[6..]; + let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(base64_token.as_bytes()) + .map_err(|e| { + serde_json::Error::custom(format!("Failed to decode base64 token: {}", e)) + })?; + let json = String::from_utf8(bytes).map_err(|e| { + serde_json::Error::custom(format!("Failed to decode base64 token: {}", e)) + })?; + serde_json::from_str(&json) + } + + pub fn from_oobnotes(notes: OOBNotes, invite_code: InviteCode) -> Result { + let mut token = TokenV3 { + token: vec![], + // Always msats + unit: Some("msat".to_string()), + // Federation Invite Code + memo: Some(invite_code.to_string()), + }; + for (amount, note) in notes.notes().iter() { + let mut proofs = vec![]; + for spendable_note in note.iter() { + let proof = Proof { + amount: amount.msats, + // stick the federation id prefix here instead of keyset + id: notes.federation_id_prefix().to_string(), + secret: hex::encode(spendable_note.spend_key.secret_key().to_bytes()), + C: hex::encode(spendable_note.signature.to_bytes()), + }; + proofs.push(proof); + } + token.token.push(Token { + mint: notes.federation_id_prefix().to_string(), + proofs, + }); + } + Ok(token) + } + + fn to_oobnotes(&self, modules: &ModuleDecoderRegistry) -> Result { + let federation_id_prefix = match self.token.first().map(|t| &t.proofs[0].id) { + Some(id) => FederationIdPrefix::from_str(id)?, + None => return Err(anyhow!("No token found")), + }; + let secp = Secp256k1::new(); + let mut notes_map = BTreeMap::>::new(); + for t in self.token.iter() { + for proof in t.proofs.iter() { + let signature_bytes = hex::decode(&proof.C) + .map_err(|e| anyhow!("Failed to decode spendable note signature: {}", e))?; + let signature = Signature::from_bytes(&signature_bytes, modules)?; + let secret_key_bytes = hex::decode(&proof.secret) + .map_err(|e| anyhow!("Failed to decode spendable note spend key: {}", e))?; + let sk = SecretKey::from_bytes(&secret_key_bytes, modules) + .map_err(|e| anyhow!("Failed to decode spendable note spend key: {}", e))?; + let spend_key = KeyPair::from_secret_key(&secp, &sk); + let spendable_note = SpendableNote { + signature, + spend_key, + }; + let amount = Amount::from_msats(proof.amount); + notes_map.entry(amount).or_default().push(spendable_note); + } + } + let tiered_notes = TieredMulti::new(notes_map); + Ok(OOBNotes::new(federation_id_prefix, tiered_notes)) + } +} + +impl FromStr for TokenV3 { + type Err = serde_json::Error; + + /// Parses a string to create a `Token` struct. + /// Assumes the string is a base64 URL-safe encoded JSON of the `Token` with + /// `cashuA` prefix. + fn from_str(s: &str) -> Result { + TokenV3::deserialize(s) + } +} + +impl fmt::Display for TokenV3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self.serialize() { + Ok(serialized) => write!(f, "{}", serialized), + Err(_) => Err(fmt::Error), + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Unit { + Msat, + Sat, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum Method { + Bolt11, + Onchain, +} diff --git a/clientd-stateless/src/main.rs b/clientd-stateless/src/main.rs index 403acd7..ee2d414 100644 --- a/clientd-stateless/src/main.rs +++ b/clientd-stateless/src/main.rs @@ -9,8 +9,9 @@ use tower_http::cors::{Any, CorsLayer}; use tower_http::trace::TraceLayer; use tracing::info; +mod cashu; mod error; -mod router; +pub mod router; mod state; mod utils; diff --git a/clientd-stateless/src/router/melt/method.rs b/clientd-stateless/src/router/melt/method.rs index a3bebc8..9ed7bd2 100644 --- a/clientd-stateless/src/router/melt/method.rs +++ b/clientd-stateless/src/router/melt/method.rs @@ -14,8 +14,8 @@ use lightning_invoice::Bolt11Invoice; use serde::{Deserialize, Serialize}; use tracing::{error, info}; +use crate::cashu::{Method, Unit}; use crate::error::AppError; -use crate::router::{Method, Unit}; use crate::state::AppState; #[derive(Debug, Deserialize)] diff --git a/clientd-stateless/src/router/mint/method.rs b/clientd-stateless/src/router/mint/method.rs index b37d06e..ac4e79c 100644 --- a/clientd-stateless/src/router/mint/method.rs +++ b/clientd-stateless/src/router/mint/method.rs @@ -14,8 +14,8 @@ use lightning_invoice::{Bolt11InvoiceDescription, Description}; use serde::{Deserialize, Serialize}; use tracing::error; +use crate::cashu::{Method, Unit}; use crate::error::AppError; -use crate::router::{Method, Unit}; use crate::state::AppState; #[derive(Debug, Deserialize)] diff --git a/clientd-stateless/src/router/mod.rs b/clientd-stateless/src/router/mod.rs index f4f3512..86d3f25 100644 --- a/clientd-stateless/src/router/mod.rs +++ b/clientd-stateless/src/router/mod.rs @@ -1,21 +1,3 @@ -use std::collections::BTreeMap; -use std::fmt; -use std::str::FromStr; - -use anyhow::anyhow; -use base64::Engine; -use bitcoin::secp256k1::{Secp256k1, SecretKey}; -use bitcoin::KeyPair; -use fedimint_core::api::InviteCode; -use fedimint_core::config::FederationIdPrefix; -use fedimint_core::db::DatabaseValue; -use fedimint_core::module::registry::ModuleDecoderRegistry; -use fedimint_core::{Amount, TieredMulti}; -use fedimint_mint_client::{OOBNotes, SpendableNote}; -use serde::de::Error; -use serde::{Deserialize, Serialize}; -use tbs::Signature; - pub mod check; pub mod info; pub mod keys; @@ -23,149 +5,3 @@ pub mod keysets; pub mod melt; pub mod mint; pub mod swap; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Proof { - // Amount unassociated with the unit - amount: u64, - // keyset id -> FederationId - id: String, - // secret -> hex encoded spend key's secret key - secret: String, - // signature -> hex encoded BLS signature - #[allow(non_snake_case)] - C: String, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Token { - mint: String, - proofs: Vec, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct TokenV3 { - pub token: Vec, - pub unit: Option, - pub memo: Option, -} - -impl TokenV3 { - /// Serializes the `Token` struct to a base64 URL-safe string without - /// padding and with the version prefix. - pub fn serialize(&self) -> Result { - let json = serde_json::to_string(self) - .map_err(|e| serde_json::Error::custom(format!("Failed to serialize token: {}", e)))?; - let base64_token = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json.as_bytes()); - Ok(format!("cashuA{}", base64_token)) - } - - /// Deserializes a base64 URL-safe string without padding (with version - /// prefix) back to a `Token` struct. - pub fn deserialize(encoded: &str) -> Result { - if !encoded.starts_with("cashuA") { - return Err(serde_json::Error::custom("Invalid token format")); - } - let base64_token = &encoded[6..]; - let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(base64_token.as_bytes()) - .map_err(|e| { - serde_json::Error::custom(format!("Failed to decode base64 token: {}", e)) - })?; - let json = String::from_utf8(bytes).map_err(|e| { - serde_json::Error::custom(format!("Failed to decode base64 token: {}", e)) - })?; - serde_json::from_str(&json) - } - - pub fn from_oobnotes(notes: OOBNotes, invite_code: InviteCode) -> Result { - let mut token = TokenV3 { - token: vec![], - // Always msats - unit: Some("msat".to_string()), - // Federation Invite Code - memo: Some(invite_code.to_string()), - }; - for (amount, note) in notes.notes().iter() { - let mut proofs = vec![]; - for spendable_note in note.iter() { - let proof = Proof { - amount: amount.msats, - // stick the federation id prefix here instead of keyset - id: notes.federation_id_prefix().to_string(), - secret: hex::encode(spendable_note.spend_key.secret_key().to_bytes()), - C: hex::encode(spendable_note.signature.to_bytes()), - }; - proofs.push(proof); - } - token.token.push(Token { - mint: notes.federation_id_prefix().to_string(), - proofs, - }); - } - Ok(token) - } - - fn to_oobnotes(&self, modules: &ModuleDecoderRegistry) -> Result { - let federation_id_prefix = match self.token.first().map(|t| &t.proofs[0].id) { - Some(id) => FederationIdPrefix::from_str(id)?, - None => return Err(anyhow!("No token found")), - }; - let secp = Secp256k1::new(); - let mut notes_map = BTreeMap::>::new(); - for t in self.token.iter() { - for proof in t.proofs.iter() { - let signature_bytes = hex::decode(&proof.C) - .map_err(|e| anyhow!("Failed to decode spendable note signature: {}", e))?; - let signature = Signature::from_bytes(&signature_bytes, modules)?; - let secret_key_bytes = hex::decode(&proof.secret) - .map_err(|e| anyhow!("Failed to decode spendable note spend key: {}", e))?; - let sk = SecretKey::from_bytes(&secret_key_bytes, modules) - .map_err(|e| anyhow!("Failed to decode spendable note spend key: {}", e))?; - let spend_key = KeyPair::from_secret_key(&secp, &sk); - let spendable_note = SpendableNote { - signature, - spend_key, - }; - let amount = Amount::from_msats(proof.amount); - notes_map.entry(amount).or_default().push(spendable_note); - } - } - let tiered_notes = TieredMulti::new(notes_map); - Ok(OOBNotes::new(federation_id_prefix, tiered_notes)) - } -} - -impl FromStr for TokenV3 { - type Err = serde_json::Error; - - /// Parses a string to create a `Token` struct. - /// Assumes the string is a base64 URL-safe encoded JSON of the `Token` with - /// `cashuA` prefix. - fn from_str(s: &str) -> Result { - TokenV3::deserialize(s) - } -} - -impl fmt::Display for TokenV3 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - match self.serialize() { - Ok(serialized) => write!(f, "{}", serialized), - Err(_) => Err(fmt::Error), - } - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum Unit { - Msat, - Sat, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "lowercase")] -pub enum Method { - Bolt11, - Onchain, -} From 0fb92a6ea07002874b5b3929d150705edd43e22e Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Tue, 30 Apr 2024 13:13:40 -0700 Subject: [PATCH 3/4] feat: impl keysets --- clientd-stateless/Cargo.toml | 4 ---- clientd-stateless/src/cashu.rs | 23 +++++++++++++++++++++-- clientd-stateless/src/router/keysets.rs | 22 +++++++++++++++++++--- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/clientd-stateless/Cargo.toml b/clientd-stateless/Cargo.toml index 0e88f3b..1f795d1 100644 --- a/clientd-stateless/Cargo.toml +++ b/clientd-stateless/Cargo.toml @@ -7,10 +7,6 @@ repository = "https://github.com/fedimint/fedimint-clientd" keywords = ["fedimint", "bitcoin", "lightning", "ecash"] license = "MIT" -[[example]] -name = "cashu_encoding" -path = "examples/cashu_encoding.rs" - [dependencies] anyhow = "1.0.75" axum = { version = "0.7.1", features = ["json", "ws"] } diff --git a/clientd-stateless/src/cashu.rs b/clientd-stateless/src/cashu.rs index e92acb9..bce91c7 100644 --- a/clientd-stateless/src/cashu.rs +++ b/clientd-stateless/src/cashu.rs @@ -7,7 +7,7 @@ use base64::Engine; use bitcoin::secp256k1::{Secp256k1, SecretKey}; use bitcoin::KeyPair; use fedimint_core::api::InviteCode; -use fedimint_core::config::FederationIdPrefix; +use fedimint_core::config::{FederationId, FederationIdPrefix}; use fedimint_core::db::DatabaseValue; use fedimint_core::module::registry::ModuleDecoderRegistry; use fedimint_core::{Amount, TieredMulti}; @@ -148,7 +148,7 @@ impl fmt::Display for TokenV3 { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum Unit { Msat, @@ -161,3 +161,22 @@ pub enum Method { Bolt11, Onchain, } + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub struct Keyset { + id: String, + unit: Unit, + active: bool, +} + +impl From for Keyset { + fn from(federation_id: FederationId) -> Self { + let as_str = format!("00{}", federation_id.to_string()); + Keyset { + id: as_str, + unit: Unit::Msat, + active: true, + } + } +} diff --git a/clientd-stateless/src/router/keysets.rs b/clientd-stateless/src/router/keysets.rs index 89b6e0d..a721c40 100644 --- a/clientd-stateless/src/router/keysets.rs +++ b/clientd-stateless/src/router/keysets.rs @@ -1,10 +1,26 @@ use axum::extract::State; +use axum::Json; +use serde::Serialize; +use crate::cashu::Keyset; use crate::error::AppError; use crate::state::AppState; +#[derive(Serialize)] +#[serde(rename_all = "lowercase")] +pub struct KeysetsResponse { + keysets: Vec, +} + #[axum_macros::debug_handler] -pub async fn handle_keysets(State(_state): State) -> Result<(), AppError> { - // TODO: Implement this function - Ok(()) +pub async fn handle_keysets( + State(state): State, +) -> Result, AppError> { + let mut keysets = Vec::::new(); + let ids = state.multimint.ids().await; + for id in ids { + keysets.push(Keyset::from(id)) + } + + Ok(Json(KeysetsResponse { keysets })) } From 3bd12ca8c586bbe1434f5f036eedf7637a676145 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Thu, 2 May 2024 15:38:36 -0500 Subject: [PATCH 4/4] chore: bump --- multimint/Cargo.toml | 2 +- multimint/src/lib.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/multimint/Cargo.toml b/multimint/Cargo.toml index 4e484cd..52881be 100644 --- a/multimint/Cargo.toml +++ b/multimint/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multimint" -version = "0.3.3" +version = "0.3.4" edition = "2021" description = "A library for managing fedimint clients across multiple federations" license = "MIT" diff --git a/multimint/src/lib.rs b/multimint/src/lib.rs index d61442b..83ba159 100644 --- a/multimint/src/lib.rs +++ b/multimint/src/lib.rs @@ -78,6 +78,7 @@ use fedimint_wallet_client::WalletClientModule; use tokio::sync::Mutex; use tracing::warn; use types::InfoResponse; +pub use {fedimint_core, fedimint_ln_client, fedimint_mint_client, fedimint_wallet_client}; pub mod client; pub mod db;