From 31671c0e3cc1e8fa29dd3b33bd5bb50cfb6cb56b Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 10:58:48 -0700 Subject: [PATCH] fix: remove stateless until useful/supported --- Cargo.lock | 35 ---- Cargo.toml | 2 +- clientd-stateless/Cargo.toml | 45 ----- clientd-stateless/src/cashu.rs | 182 -------------------- clientd-stateless/src/error.rs | 49 ------ clientd-stateless/src/main.rs | 178 ------------------- clientd-stateless/src/router/check.rs | 34 ---- clientd-stateless/src/router/info.rs | 111 ------------ clientd-stateless/src/router/keys.rs | 17 -- clientd-stateless/src/router/keysets.rs | 26 --- clientd-stateless/src/router/melt/method.rs | 182 -------------------- clientd-stateless/src/router/melt/mod.rs | 2 - clientd-stateless/src/router/melt/quote.rs | 16 -- clientd-stateless/src/router/mint/method.rs | 128 -------------- clientd-stateless/src/router/mint/mod.rs | 2 - clientd-stateless/src/router/mint/quote.rs | 16 -- clientd-stateless/src/router/mod.rs | 7 - clientd-stateless/src/router/swap.rs | 52 ------ clientd-stateless/src/state.rs | 50 ------ clientd-stateless/src/utils.rs | 11 -- 20 files changed, 1 insertion(+), 1144 deletions(-) delete mode 100644 clientd-stateless/Cargo.toml delete mode 100644 clientd-stateless/src/cashu.rs delete mode 100644 clientd-stateless/src/error.rs delete mode 100644 clientd-stateless/src/main.rs delete mode 100644 clientd-stateless/src/router/check.rs delete mode 100644 clientd-stateless/src/router/info.rs delete mode 100644 clientd-stateless/src/router/keys.rs delete mode 100644 clientd-stateless/src/router/keysets.rs delete mode 100644 clientd-stateless/src/router/melt/method.rs delete mode 100644 clientd-stateless/src/router/melt/mod.rs delete mode 100644 clientd-stateless/src/router/melt/quote.rs delete mode 100644 clientd-stateless/src/router/mint/method.rs delete mode 100644 clientd-stateless/src/router/mint/mod.rs delete mode 100644 clientd-stateless/src/router/mint/quote.rs delete mode 100644 clientd-stateless/src/router/mod.rs delete mode 100644 clientd-stateless/src/router/swap.rs delete mode 100644 clientd-stateless/src/state.rs delete mode 100644 clientd-stateless/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 149a43c..e74afd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -709,41 +709,6 @@ dependencies = [ "os_str_bytes", ] -[[package]] -name = "clientd-stateless" -version = "0.3.5" -dependencies = [ - "anyhow", - "async-utility", - "axum", - "axum-macros", - "axum-otel-metrics", - "base64 0.22.0", - "bitcoin 0.29.2", - "bitcoin_hashes 0.13.0", - "chrono", - "clap", - "dotenv", - "fedimint", - "fedimint-tbs", - "futures-util", - "hex", - "itertools 0.12.1", - "lazy_static", - "lightning-invoice", - "lnurl-rs", - "multimint 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "reqwest 0.12.4", - "serde", - "serde_json", - "time", - "tokio", - "tower-http", - "tracing", - "tracing-subscriber", - "url", -] - [[package]] name = "concurrent-queue" version = "2.4.0" diff --git a/Cargo.toml b/Cargo.toml index 1d6e1b8..d29ed0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["multimint", "fedimint-clientd", "clientd-stateless"] +members = ["multimint", "fedimint-clientd"] resolver = "2" [workspace.package] diff --git a/clientd-stateless/Cargo.toml b/clientd-stateless/Cargo.toml deleted file mode 100644 index ecc8ab7..0000000 --- a/clientd-stateless/Cargo.toml +++ /dev/null @@ -1,45 +0,0 @@ -[package] -name = "clientd-stateless" -description = "A stateless fedimint client daemon" -version.workspace = true -edition.workspace = true -repository.workspace = true -keywords.workspace = true -license.workspace = true -readme.workspace = true -authors.workspace = true - -[dependencies] -anyhow = "1.0.75" -axum = { version = "0.7.1", features = ["json", "ws"] } -axum-macros = "0.4.0" -dotenv = "0.15.0" -fedimint = "0.0.1" -serde = "1.0.193" -serde_json = "1.0.108" -tokio = { version = "1.34.0", features = ["full"] } -tracing = "0.1.40" -tracing-subscriber = "0.3.18" -url = "2.5.0" -lazy_static = "1.4.0" -async-utility = "0.2.0" -tower-http = { version = "0.5.2", features = ["cors", "auth", "trace"] } -bitcoin = "0.29.2" -itertools = "0.12.0" -lnurl-rs = { version = "0.5.0", features = ["async"], default-features = false } -reqwest = { version = "0.12.3", features = [ - "json", - "rustls-tls", -], default-features = false } -lightning-invoice = { version = "0.26.0", features = ["serde"] } -bitcoin_hashes = "0.13.0" -time = { version = "0.3.25", features = ["formatting"] } -chrono = "0.4.31" -futures-util = "0.3.30" -clap = { version = "3", features = ["derive", "env"] } -multimint = { version = "0.3.6" } -# multimint = { path = "../multimint" } -axum-otel-metrics = "0.8.0" -base64 = "0.22.0" -hex = "0.4.3" -fedimint-tbs = "0.3.0" diff --git a/clientd-stateless/src/cashu.rs b/clientd-stateless/src/cashu.rs deleted file mode 100644 index bba10b0..0000000 --- a/clientd-stateless/src/cashu.rs +++ /dev/null @@ -1,182 +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 multimint::fedimint_core::api::InviteCode; -use multimint::fedimint_core::config::{FederationId, FederationIdPrefix}; -use multimint::fedimint_core::db::DatabaseValue; -use multimint::fedimint_core::module::registry::ModuleDecoderRegistry; -use multimint::fedimint_core::{Amount, TieredMulti}; -use multimint::fedimint_mint_client::{OOBNotes, SpendableNote}; -use serde::de::Error; -use serde::{Deserialize, Serialize}; -use tbs::Signature; - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[allow(non_snake_case)] -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 - 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, Serialize)] -#[serde(rename_all = "lowercase")] -pub enum Unit { - Msat, - Sat, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "lowercase")] -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/error.rs b/clientd-stateless/src/error.rs deleted file mode 100644 index 7ecc428..0000000 --- a/clientd-stateless/src/error.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::fmt; - -use axum::http::StatusCode; -use axum::response::{IntoResponse, Response}; -use serde_json::json; - -pub struct AppError { - pub error: anyhow::Error, - pub status: StatusCode, -} - -impl AppError { - pub fn new(status: StatusCode, error: impl Into) -> Self { - Self { - error: error.into(), - status, - } - } -} - -// Tell axum how to convert `AppError` into a response. -impl IntoResponse for AppError { - fn into_response(self) -> Response { - (self.status, format!("Something went wrong: {}", self.error)).into_response() - } -} - -impl fmt::Display for AppError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let error_json = json!({ - "error": self.error.to_string(), - "status": self.status.as_u16(), - }); - - write!(f, "{}", error_json) - } -} - -impl From for AppError -where - E: Into, -{ - fn from(err: E) -> Self { - Self { - error: err.into(), - status: StatusCode::INTERNAL_SERVER_ERROR, // default status code - } - } -} diff --git a/clientd-stateless/src/main.rs b/clientd-stateless/src/main.rs deleted file mode 100644 index a9fcc06..0000000 --- a/clientd-stateless/src/main.rs +++ /dev/null @@ -1,178 +0,0 @@ -use std::path::PathBuf; -use std::str::FromStr; - -use anyhow::Result; -use axum::http::Method; -use multimint::fedimint_core::api::InviteCode; -use router::{check, info, keys, keysets, melt, mint, swap}; -use tower_http::cors::{Any, CorsLayer}; -use tower_http::trace::TraceLayer; -use tracing::info; - -mod cashu; -mod error; -pub mod router; -mod state; -mod utils; - -use axum::routing::{get, post}; -use axum::Router; -use axum_otel_metrics::HttpMetricsLayerBuilder; -use clap::{Parser, Subcommand}; -use state::AppState; - -#[derive(Subcommand)] -enum Commands { - Start, - Stop, -} - -#[derive(Parser)] -#[clap(version = "1.0", author = "Kody Low")] -struct Cli { - /// Federation invite code - #[clap(long, env = "FEDIMINT_CLIENTD_INVITE_CODE", required = false)] - invite_code: String, - - /// Path to FM database - #[clap(long, env = "FEDIMINT_CLIENTD_DB_PATH", required = true)] - db_path: PathBuf, - - /// Password - #[clap(long, env = "FEDIMINT_CLIENTD_PASSWORD", required = true)] - password: String, - - /// Addr - #[clap(long, env = "FEDIMINT_CLIENTD_ADDR", required = true)] - addr: String, - - /// Manual secret - #[clap(long, env = "FEDIMINT_CLIENTD_MANUAL_SECRET", required = false)] - manual_secret: Option, -} - -// const PID_FILE: &str = "/tmp/fedimint_http.pid"; - -#[tokio::main] -async fn main() -> Result<()> { - tracing_subscriber::fmt::init(); - dotenv::dotenv().ok(); - - let cli: Cli = Cli::parse(); - - let mut state = AppState::new(cli.db_path).await?; - - let manual_secret = match cli.manual_secret { - Some(secret) => Some(secret), - None => match std::env::var("FEDIMINT_CLIENTD_MANUAL_SECRET") { - Ok(secret) => Some(secret), - Err(_) => None, - }, - }; - - match InviteCode::from_str(&cli.invite_code) { - Ok(invite_code) => { - let federation_id = state - .multimint - .register_new(invite_code, manual_secret) - .await?; - info!("Created client for federation id: {:?}", federation_id); - } - Err(e) => { - info!( - "No federation invite code provided, skipping client creation: {}", - e - ); - } - } - - if state.multimint.all().await.is_empty() { - return Err(anyhow::anyhow!("No clients found, must have at least one client to start the server. Try providing a federation invite code with the `--invite-code` flag or setting the `FEDIMINT_CLIENTD_INVITE_CODE` environment variable.")); - } - - let app = Router::new().nest("/v1", cashu_v1_rest()).with_state(state); - info!("Starting stateless server"); - - let cors = CorsLayer::new() - // allow `GET` and `POST` when accessing the resource - .allow_methods([Method::GET, Method::POST]) - // allow requests from any origin - .allow_origin(Any) - // allow auth header - .allow_headers(Any); - - let metrics = HttpMetricsLayerBuilder::new() - .with_service_name("fedimint-clientd".to_string()) - .build(); - - let app = app - .layer(cors) - .layer(TraceLayer::new_for_http()) // tracing requests - // no traces for routes bellow - .route("/health", get(|| async { "Server is up and running!" })) // for health check - // metrics for all routes above - .merge(metrics.routes()) - .layer(metrics); - - let listener = tokio::net::TcpListener::bind(format!("{}", &cli.addr)) - .await - .map_err(|e| anyhow::anyhow!("Failed to bind to address, should be a valid address and port like 127.0.0.1:3333: {e}"))?; - info!("fedimint-clientd Listening on {}", &cli.addr); - axum::serve(listener, app) - .await - .map_err(|e| anyhow::anyhow!("Failed to start server: {e}"))?; - - Ok(()) -} - -/// Implements Cashu V1 API Routes: -/// -/// REQUIRED -/// NUT-01 Mint Public Key Exchange && NUT-02 Keysets and Keyset IDs -/// - `/cashu/v1/keys` -/// - `/cashu/v1/keys/{keyset_id}` -/// - `/cashu/v1/keysets` -/// NUT-03 Swap Tokens (Equivalent to `reissue` command) -/// - `/cashu/v1/swap` -/// NUT-04 Mint Tokens: supports `bolt11` and `onchain` methods -/// - `/cashu/v1/mint/quote/{method}` -/// - `/cashu/v1/mint/quote/{method}/{quote_id}` -/// - `/cashu/v1/mint/{method}` -/// NUT-05 Melting Tokens: supports `bolt11` and `onchain` methods -/// - `/cashu/v1/melt/quote/{method}` -/// - `/cashu/v1/melt/quote/{method}/{quote_id}` -/// - `/cashu/v1/melt/{method}` -/// NUT-06 Mint Information -/// - `/cashu/v1/info` -/// -/// OPTIONAL -/// NUT-07 Token State Check -/// - `/cashu/v1/check` -/// NUT-08 Lightning Fee Return -/// - Modification of NUT-05 Melt -/// NUT-10 Spending Conditions -/// NUT-11 Pay to Public Key (P2PK) -/// - Fedimint already does this -/// NUT-12 Offline Ecash Signature Validation -/// - DLEQ in BlindedSignature for Mint to User -fn cashu_v1_rest() -> Router { - Router::new() - .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(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(melt::quote::handle_method_quote_id), - ) - .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/check.rs b/clientd-stateless/src/router/check.rs deleted file mode 100644 index cedc595..0000000 --- a/clientd-stateless/src/router/check.rs +++ /dev/null @@ -1,34 +0,0 @@ -use axum::extract::State; -use axum::Json; -use multimint::fedimint_core::Amount; -use multimint::fedimint_mint_client::{MintClientModule, OOBNotes}; -use serde::{Deserialize, Serialize}; - -use crate::error::AppError; -use crate::state::AppState; - -#[derive(Debug, Deserialize)] -pub struct CheckRequest { - pub notes: OOBNotes, -} - -#[derive(Debug, Serialize)] -pub struct CheckResponse { - pub amount_msat: Amount, -} - -#[axum_macros::debug_handler] -pub async fn handle_check( - State(state): State, - Json(req): Json, -) -> Result, AppError> { - let client = state - .get_client_by_prefix(&req.notes.federation_id_prefix()) - .await?; - let amount_msat = client - .get_first_module::() - .validate_notes(req.notes) - .await?; - - Ok(Json(CheckResponse { amount_msat })) -} diff --git a/clientd-stateless/src/router/info.rs b/clientd-stateless/src/router/info.rs deleted file mode 100644 index a6042e8..0000000 --- a/clientd-stateless/src/router/info.rs +++ /dev/null @@ -1,111 +0,0 @@ -use std::collections::BTreeMap; -use std::env; -use std::str::FromStr; - -use axum::extract::State; -use axum::Json; -use multimint::fedimint_core::config::FederationId; -use serde::Serialize; - -use crate::error::AppError; -use crate::state::AppState; - -#[derive(Debug, Serialize)] -pub struct Contact { - pub method: String, - pub value: String, -} - -#[derive(Debug, Serialize)] -pub struct NutMethod { - pub method: String, - pub value: String, -} - -#[derive(Debug, Serialize)] -pub struct Nut { - pub methods: Option>, - pub disabled: Option, - pub supported: Option, -} - -#[derive(Debug, Serialize)] -pub struct CashuNUT06InfoResponse { - pub name: String, - pub pubkey: String, - pub version: String, - pub description: String, - pub description_long: String, - pub contact: Vec, - pub motd: String, - pub nuts: BTreeMap, -} - -#[axum_macros::debug_handler] -pub async fn handle_info( - State(state): State, -) -> Result, AppError> { - let federation_id = env::var("FEDIMINT_CLIENTD_ACTIVE_FEDERATION_ID") - .map_err(|e| anyhow::anyhow!("Failed to get active federation id: {}", e))?; - let client = state - .get_client(FederationId::from_str(&federation_id)?) - .await?; - - let config = client.get_config(); - let mut nuts = BTreeMap::new(); - - nuts.insert( - "4".to_string(), - Nut { - methods: Some(vec![NutMethod { - method: "bolt11".to_string(), - value: "sat".to_string(), - }]), - disabled: Some(false), - supported: None, - }, - ); - - nuts.insert( - "5".to_string(), - Nut { - methods: Some(vec![NutMethod { - method: "bolt11".to_string(), - value: "sat".to_string(), - }]), - disabled: None, - supported: None, - }, - ); - - for &i in &[7, 8, 9, 10, 12] { - nuts.insert( - i.to_string(), - Nut { - methods: None, - disabled: None, - supported: Some(true), - }, - ); - } - - let response = CashuNUT06InfoResponse { - name: config - .global - .federation_name() - .unwrap_or_default() - .to_string(), - pubkey: config.global.calculate_federation_id().to_string(), - version: format!("{:?}", config.global.consensus_version), - description: "Cashu <-> Fedimint Soon (tm)".to_string(), - description_long: "Cashu <-> Fedimint Soon (tm)".to_string(), - contact: vec![Contact { - method: "xmpp".to_string(), - value: "local@localhost".to_string(), - }], - motd: "Cashu <-> Fedimint Soon (tm)".to_string(), - nuts, - }; - - Ok(Json(response)) -} diff --git a/clientd-stateless/src/router/keys.rs b/clientd-stateless/src/router/keys.rs deleted file mode 100644 index eeb6c78..0000000 --- a/clientd-stateless/src/router/keys.rs +++ /dev/null @@ -1,17 +0,0 @@ -use anyhow::Result; -use axum::extract::State; - -use crate::error::AppError; -use crate::state::AppState; - -#[axum_macros::debug_handler] -pub async fn handle_keys(State(_state): State) -> Result<(), AppError> { - // TODO: Implement this function - Ok(()) -} - -#[axum_macros::debug_handler] -pub async fn handle_keys_keyset_id(State(_state): State) -> Result<(), AppError> { - // TODO: Implement this function - Ok(()) -} diff --git a/clientd-stateless/src/router/keysets.rs b/clientd-stateless/src/router/keysets.rs deleted file mode 100644 index a721c40..0000000 --- a/clientd-stateless/src/router/keysets.rs +++ /dev/null @@ -1,26 +0,0 @@ -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> { - let mut keysets = Vec::::new(); - let ids = state.multimint.ids().await; - for id in ids { - keysets.push(Keyset::from(id)) - } - - Ok(Json(KeysetsResponse { keysets })) -} diff --git a/clientd-stateless/src/router/melt/method.rs b/clientd-stateless/src/router/melt/method.rs deleted file mode 100644 index a048a0c..0000000 --- a/clientd-stateless/src/router/melt/method.rs +++ /dev/null @@ -1,182 +0,0 @@ -use std::str::FromStr; - -use anyhow::anyhow; -use axum::extract::{Path, State}; -use axum::http::StatusCode; -use axum::Json; -use futures_util::StreamExt; -use lightning_invoice::Bolt11Invoice; -use multimint::fedimint_client::ClientHandleArc; -use multimint::fedimint_core::config::FederationId; -use multimint::fedimint_core::Amount; -use multimint::fedimint_ln_client::{LightningClientModule, OutgoingLightningPayment}; -use multimint::fedimint_wallet_client::{WalletClientModule, WithdrawState}; -use serde::{Deserialize, Serialize}; -use tracing::{error, info}; - -use crate::cashu::{Method, Unit}; -use crate::error::AppError; -use crate::state::AppState; - -#[derive(Debug, Deserialize)] -pub struct PostMeltQuoteMethodRequest { - pub request: String, - pub amount: Amount, - pub unit: Unit, - pub federation_id: FederationId, -} - -#[derive(Debug, Serialize)] -pub struct PostMeltQuoteMethodResponse { - pub quote: String, - pub amount: Amount, - pub fee_reserve: Amount, - pub paid: bool, - pub expiry: u64, -} - -#[axum_macros::debug_handler] -pub async fn handle_method( - Path(method): Path, - State(state): State, - Json(req): Json, -) -> Result, AppError> { - let client = state.get_client(req.federation_id).await?; - let res = match method { - Method::Bolt11 => match req.unit { - Unit::Msat => melt_bolt11(client, req.request, req.amount).await, - Unit::Sat => melt_bolt11(client, req.request, req.amount * 1000).await, - }, - Method::Onchain => match req.unit { - Unit::Msat => { - let amount_sat = bitcoin::Amount::from_sat(req.amount.try_into_sats()?); - melt_onchain(client, req.request, amount_sat).await - } - Unit::Sat => { - let amount_sat = req.amount * 1000; - let amount_sat = bitcoin::Amount::from_sat(amount_sat.try_into_sats()?); - melt_onchain(client, req.request, amount_sat).await - } - }, - }?; - - Ok(Json(res)) -} - -pub async fn melt_bolt11( - client: ClientHandleArc, - request: String, - amount_msat: Amount, -) -> Result { - let lightning_module = client.get_first_module::(); - let gateway_id = match lightning_module.list_gateways().await.first() { - Some(gateway_announcement) => gateway_announcement.info.gateway_id, - None => { - error!("No gateways available"); - return Err(AppError::new( - StatusCode::INTERNAL_SERVER_ERROR, - anyhow!("No gateways available"), - )); - } - }; - let gateway = lightning_module - .select_gateway(&gateway_id) - .await - .ok_or_else(|| { - error!("Failed to select gateway"); - AppError::new( - StatusCode::INTERNAL_SERVER_ERROR, - anyhow!("Failed to select gateway"), - ) - })?; - - let bolt11 = Bolt11Invoice::from_str(&request)?; - let bolt11_amount = Amount::from_msats( - bolt11 - .amount_milli_satoshis() - .ok_or_else(|| anyhow!("Cannot pay amountless invoices",))?, - ); - - if bolt11_amount != amount_msat { - return Err(AppError::new( - StatusCode::BAD_REQUEST, - anyhow!( - "Invoice amount ({}) does not match request amount ({})", - bolt11_amount, - amount_msat - ), - )); - } - - let OutgoingLightningPayment { - payment_type, - contract_id: _, - fee, - } = lightning_module - .pay_bolt11_invoice(Some(gateway), bolt11, ()) - .await?; - - let operation_id = payment_type.operation_id(); - info!("Gateway fee: {fee}, payment operation id: {operation_id}"); - - Ok(PostMeltQuoteMethodResponse { - quote: operation_id.to_string(), - amount: amount_msat, - fee_reserve: fee, - paid: false, - expiry: 0, - }) -} - -async fn melt_onchain( - client: ClientHandleArc, - request: String, - amount_sat: bitcoin::Amount, -) -> Result { - let address = bitcoin::Address::from_str(&request) - .map_err(|e| anyhow::anyhow!("Onchain request must be a valid bitcoin address: {e}"))?; - let wallet_module = client.get_first_module::(); - let fees = wallet_module - .get_withdraw_fees(address.clone(), amount_sat) - .await?; - let absolute_fees = fees.amount(); - - info!("Attempting withdraw with fees: {fees:?}"); - - let operation_id = wallet_module - .withdraw(address, amount_sat, fees, ()) - .await?; - - let mut updates = wallet_module - .subscribe_withdraw_updates(operation_id) - .await? - .into_stream(); - - while let Some(update) = updates.next().await { - info!("Update: {update:?}"); - - match update { - WithdrawState::Succeeded(_txid) => { - return Ok(PostMeltQuoteMethodResponse { - quote: operation_id.to_string(), - amount: amount_sat.into(), - fee_reserve: absolute_fees.into(), - paid: true, - expiry: 0, - }); - } - WithdrawState::Failed(e) => { - return Err(AppError::new( - StatusCode::INTERNAL_SERVER_ERROR, - anyhow!("Onchain melt failed: {:?}", e), - )); - } - _ => continue, - }; - } - - Err(AppError::new( - StatusCode::INTERNAL_SERVER_ERROR, - anyhow!("Update stream ended without outcome"), - )) -} diff --git a/clientd-stateless/src/router/melt/mod.rs b/clientd-stateless/src/router/melt/mod.rs deleted file mode 100644 index dd31219..0000000 --- a/clientd-stateless/src/router/melt/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod method; -pub mod quote; diff --git a/clientd-stateless/src/router/melt/quote.rs b/clientd-stateless/src/router/melt/quote.rs deleted file mode 100644 index d5c2714..0000000 --- a/clientd-stateless/src/router/melt/quote.rs +++ /dev/null @@ -1,16 +0,0 @@ -use axum::extract::State; - -use crate::error::AppError; -use crate::state::AppState; - -#[axum_macros::debug_handler] -pub async fn handle_method(State(_state): State) -> Result<(), AppError> { - // TODO: Implement this function - Ok(()) -} - -#[axum_macros::debug_handler] -pub async fn handle_method_quote_id(State(_state): State) -> Result<(), AppError> { - // TODO: Implement this function - Ok(()) -} diff --git a/clientd-stateless/src/router/mint/method.rs b/clientd-stateless/src/router/mint/method.rs deleted file mode 100644 index c8aa9bb..0000000 --- a/clientd-stateless/src/router/mint/method.rs +++ /dev/null @@ -1,128 +0,0 @@ -use std::time::Duration; - -use anyhow::anyhow; -use axum::extract::{Path, State}; -use axum::http::StatusCode; -use axum::Json; -use lightning_invoice::{Bolt11InvoiceDescription, Description}; -use multimint::fedimint_client::ClientHandleArc; -use multimint::fedimint_core::config::FederationId; -use multimint::fedimint_core::time::now; -use multimint::fedimint_core::Amount; -use multimint::fedimint_ln_client::LightningClientModule; -use multimint::fedimint_wallet_client::WalletClientModule; -use serde::{Deserialize, Serialize}; -use tracing::error; - -use crate::cashu::{Method, Unit}; -use crate::error::AppError; -use crate::state::AppState; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PostMintQuoteMethodRequest { - pub amount: Amount, - pub unit: Unit, - pub federation_id: FederationId, -} - -#[derive(Debug, Serialize)] -pub struct PostMintQuoteMethodResponse { - pub quote: String, - pub request: String, - pub paid: bool, - pub expiry: u64, -} - -#[axum_macros::debug_handler] -pub async fn handle_method( - Path(method): Path, - State(state): State, - Json(req): Json, -) -> Result, AppError> { - let client = state.get_client(req.federation_id).await?; - let res = match method { - Method::Bolt11 => match req.unit { - Unit::Msat => mint_bolt11(client, req.amount).await, - Unit::Sat => mint_bolt11(client, req.amount * 1000).await, - }, - Method::Onchain => match req.unit { - Unit::Msat => Err(AppError::new( - StatusCode::BAD_REQUEST, - anyhow!("Unsupported unit for onchain mint, use sat instead"), - )), - Unit::Sat => mint_onchain(client, req.amount * 1000).await, - }, - }?; - - Ok(Json(res)) -} - -const DEFAULT_MINT_EXPIRY_OFFSET: u64 = 3600; -const DEFAULT_MINT_DESCRIPTION: &str = "Cashu mint operation"; - -pub async fn mint_bolt11( - client: ClientHandleArc, - amount_msat: Amount, -) -> Result { - let valid_until = now() + Duration::from_secs(DEFAULT_MINT_EXPIRY_OFFSET); - let expiry_time = crate::utils::system_time_to_u64(valid_until)?; - let lightning_module = client.get_first_module::(); - let gateway_id = match lightning_module.list_gateways().await.first() { - Some(gateway_announcement) => gateway_announcement.info.gateway_id, - None => { - error!("No gateways available"); - return Err(AppError::new( - StatusCode::INTERNAL_SERVER_ERROR, - anyhow!("No gateways available"), - )); - } - }; - let gateway = lightning_module - .select_gateway(&gateway_id) - .await - .ok_or_else(|| { - error!("Failed to select gateway"); - AppError::new( - StatusCode::INTERNAL_SERVER_ERROR, - anyhow!("Failed to select gateway"), - ) - })?; - - let (operation_id, invoice, _) = lightning_module - .create_bolt11_invoice( - amount_msat, - Bolt11InvoiceDescription::Direct(&Description::new( - DEFAULT_MINT_DESCRIPTION.to_string(), - )?), - Some(expiry_time), - (), - Some(gateway), - ) - .await?; - - Ok(PostMintQuoteMethodResponse { - quote: operation_id.to_string(), - request: invoice.to_string(), - paid: false, - expiry: expiry_time, - }) -} - -async fn mint_onchain( - client: ClientHandleArc, - _amount_sat: Amount, -) -> Result { - let wallet_client = client.get_first_module::(); - let valid_until = now() + Duration::from_secs(DEFAULT_MINT_EXPIRY_OFFSET); - let expiry_time = crate::utils::system_time_to_u64(valid_until)?; - - let (operation_id, address) = wallet_client.get_deposit_address(valid_until, ()).await?; - - Ok(PostMintQuoteMethodResponse { - quote: operation_id.to_string(), - request: address.to_string(), - paid: false, - expiry: expiry_time, - }) -} diff --git a/clientd-stateless/src/router/mint/mod.rs b/clientd-stateless/src/router/mint/mod.rs deleted file mode 100644 index dd31219..0000000 --- a/clientd-stateless/src/router/mint/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod method; -pub mod quote; diff --git a/clientd-stateless/src/router/mint/quote.rs b/clientd-stateless/src/router/mint/quote.rs deleted file mode 100644 index d5c2714..0000000 --- a/clientd-stateless/src/router/mint/quote.rs +++ /dev/null @@ -1,16 +0,0 @@ -use axum::extract::State; - -use crate::error::AppError; -use crate::state::AppState; - -#[axum_macros::debug_handler] -pub async fn handle_method(State(_state): State) -> Result<(), AppError> { - // TODO: Implement this function - Ok(()) -} - -#[axum_macros::debug_handler] -pub async fn handle_method_quote_id(State(_state): State) -> Result<(), AppError> { - // TODO: Implement this function - Ok(()) -} diff --git a/clientd-stateless/src/router/mod.rs b/clientd-stateless/src/router/mod.rs deleted file mode 100644 index 86d3f25..0000000 --- a/clientd-stateless/src/router/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod check; -pub mod info; -pub mod keys; -pub mod keysets; -pub mod melt; -pub mod mint; -pub mod swap; diff --git a/clientd-stateless/src/router/swap.rs b/clientd-stateless/src/router/swap.rs deleted file mode 100644 index e99742b..0000000 --- a/clientd-stateless/src/router/swap.rs +++ /dev/null @@ -1,52 +0,0 @@ -use anyhow::anyhow; -use axum::extract::State; -use axum::http::StatusCode; -use axum::Json; -use futures_util::StreamExt; -use multimint::fedimint_core::config::FederationId; -use multimint::fedimint_core::Amount; -use multimint::fedimint_mint_client::{MintClientModule, OOBNotes, ReissueExternalNotesState}; -use serde::{Deserialize, Serialize}; -use tracing::info; - -use crate::error::AppError; -use crate::state::AppState; - -#[derive(Debug, Deserialize)] -pub struct SwapRequest { - pub notes: OOBNotes, - pub federation_id: FederationId, -} - -#[derive(Debug, Serialize)] -pub struct SwapResponse { - pub amount_msat: Amount, -} - -#[axum_macros::debug_handler] -pub async fn handle_swap( - State(state): State, - Json(req): Json, -) -> Result, AppError> { - let amount_msat = req.notes.total_amount(); - - let client = state.get_client(req.federation_id).await?; - let mint = client.get_first_module::(); - - let operation_id = mint.reissue_external_notes(req.notes, ()).await?; - let mut updates = mint - .subscribe_reissue_external_notes(operation_id) - .await? - .into_stream(); - - while let Some(update) = updates.next().await { - let update_clone = update.clone(); - if let ReissueExternalNotesState::Failed(e) = update { - Err(AppError::new(StatusCode::INTERNAL_SERVER_ERROR, anyhow!(e)))?; - } - - info!("Update: {update_clone:?}"); - } - - Ok(Json(SwapResponse { amount_msat })) -} diff --git a/clientd-stateless/src/state.rs b/clientd-stateless/src/state.rs deleted file mode 100644 index ffc7487..0000000 --- a/clientd-stateless/src/state.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::path::PathBuf; - -use anyhow::{anyhow, Result}; -use axum::http::StatusCode; -use multimint::fedimint_client::ClientHandleArc; -use multimint::fedimint_core::config::{FederationId, FederationIdPrefix}; -use multimint::MultiMint; - -use crate::error::AppError; -#[derive(Debug, Clone)] -pub struct AppState { - pub multimint: MultiMint, -} - -impl AppState { - pub async fn new(fm_db_path: PathBuf) -> Result { - let clients = MultiMint::new(fm_db_path).await?; - clients.update_gateway_caches().await?; - Ok(Self { multimint: clients }) - } - - // Helper function to get a specific client from the state or default - pub async fn get_client( - &self, - federation_id: FederationId, - ) -> Result { - match self.multimint.get(&federation_id).await { - Some(client) => Ok(client), - None => Err(AppError::new( - StatusCode::BAD_REQUEST, - anyhow!("No client found for federation id"), - )), - } - } - - pub async fn get_client_by_prefix( - &self, - federation_id_prefix: &FederationIdPrefix, - ) -> Result { - let client = self.multimint.get_by_prefix(federation_id_prefix).await; - - match client { - Some(client) => Ok(client), - None => Err(AppError::new( - StatusCode::BAD_REQUEST, - anyhow!("No client found for federation id prefix"), - )), - } - } -} diff --git a/clientd-stateless/src/utils.rs b/clientd-stateless/src/utils.rs deleted file mode 100644 index 5321a72..0000000 --- a/clientd-stateless/src/utils.rs +++ /dev/null @@ -1,11 +0,0 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - -use anyhow::{anyhow, Result}; - -// Helper function to convert SystemTime to u64 -pub fn system_time_to_u64(time: SystemTime) -> Result { - match time.duration_since(UNIX_EPOCH) { - Ok(duration) => Ok(duration.as_secs()), // Converts to seconds - Err(_) => Err(anyhow!("some error")), - } -}