From 8522cf392396086ca180ae839a10b8fe89be9867 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 27 Apr 2024 09:35:49 -0700 Subject: [PATCH 1/4] feat: stateless-client --- Cargo.lock | 38 ++++ Cargo.toml | 10 +- clientd-stateless/Cargo.toml | 46 +++++ clientd-stateless/src/error.rs | 49 +++++ clientd-stateless/src/main.rs | 186 ++++++++++++++++++ .../src/router/handlers/check.rs | 34 ++++ clientd-stateless/src/router/handlers/info.rs | 104 ++++++++++ clientd-stateless/src/router/handlers/keys.rs | 17 ++ .../src/router/handlers/keysets.rs | 10 + .../src/router/handlers/melt/method.rs | 182 +++++++++++++++++ .../src/router/handlers/melt/mod.rs | 2 + .../src/router/handlers/melt/quote.rs | 16 ++ .../src/router/handlers/mint/method.rs | 128 ++++++++++++ .../src/router/handlers/mint/mod.rs | 2 + .../src/router/handlers/mint/quote.rs | 16 ++ clientd-stateless/src/router/handlers/mod.rs | 23 +++ clientd-stateless/src/router/handlers/swap.rs | 52 +++++ clientd-stateless/src/router/mod.rs | 1 + clientd-stateless/src/state.rs | 70 +++++++ clientd-stateless/src/utils.rs | 11 ++ 20 files changed, 995 insertions(+), 2 deletions(-) create mode 100644 clientd-stateless/Cargo.toml create mode 100644 clientd-stateless/src/error.rs create mode 100644 clientd-stateless/src/main.rs create mode 100644 clientd-stateless/src/router/handlers/check.rs create mode 100644 clientd-stateless/src/router/handlers/info.rs create mode 100644 clientd-stateless/src/router/handlers/keys.rs create mode 100644 clientd-stateless/src/router/handlers/keysets.rs create mode 100644 clientd-stateless/src/router/handlers/melt/method.rs create mode 100644 clientd-stateless/src/router/handlers/melt/mod.rs create mode 100644 clientd-stateless/src/router/handlers/melt/quote.rs create mode 100644 clientd-stateless/src/router/handlers/mint/method.rs create mode 100644 clientd-stateless/src/router/handlers/mint/mod.rs create mode 100644 clientd-stateless/src/router/handlers/mint/quote.rs create mode 100644 clientd-stateless/src/router/handlers/mod.rs create mode 100644 clientd-stateless/src/router/handlers/swap.rs create mode 100644 clientd-stateless/src/router/mod.rs create mode 100644 clientd-stateless/src/state.rs create mode 100644 clientd-stateless/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 87b3447..6030ccc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -697,6 +697,44 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clientd-statless" +version = "0.3.3" +dependencies = [ + "anyhow", + "async-utility", + "axum", + "axum-macros", + "axum-otel-metrics", + "bitcoin 0.29.2", + "bitcoin_hashes 0.13.0", + "chrono", + "clap", + "dotenv", + "fedimint", + "fedimint-client", + "fedimint-core", + "fedimint-ln-client", + "fedimint-mint-client", + "fedimint-rocksdb", + "fedimint-wallet-client", + "futures-util", + "itertools 0.12.1", + "lazy_static", + "lightning-invoice", + "lnurl-rs", + "multimint 0.3.2", + "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 bba366d..1e28912 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["multimint", "fedimint-clientd"] +members = ["multimint", "fedimint-clientd", "clientd-stateless"] resolver = "2" version = "0.3.3" @@ -15,7 +15,13 @@ ci = ["github"] # The installers to generate for each app installers = ["shell"] # Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] +targets = [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "x86_64-pc-windows-msvc", +] # Publish jobs to run in CI pr-run-mode = "plan" # Whether to install an updater program diff --git a/clientd-stateless/Cargo.toml b/clientd-stateless/Cargo.toml new file mode 100644 index 0000000..c858268 --- /dev/null +++ b/clientd-stateless/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "clientd-statless" +version = "0.3.3" +edition = "2021" +description = "A stateless fedimint client daemon" +repository = "https://github.com/fedimint/fedimint-clientd" +keywords = ["fedimint", "bitcoin", "lightning", "ecash"] +license = "MIT" + +[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" +fedimint-client = "0.3.0" +fedimint-core = "0.3.0" +fedimint-wallet-client = "0.3.0" +fedimint-mint-client = "0.3.0" +fedimint-ln-client = "0.3.0" +fedimint-rocksdb = "0.3.0" +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.2" } +# multimint = { path = "../multimint" } +axum-otel-metrics = "0.8.0" diff --git a/clientd-stateless/src/error.rs b/clientd-stateless/src/error.rs new file mode 100644 index 0000000..7ecc428 --- /dev/null +++ b/clientd-stateless/src/error.rs @@ -0,0 +1,49 @@ +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 new file mode 100644 index 0000000..941bd38 --- /dev/null +++ b/clientd-stateless/src/main.rs @@ -0,0 +1,186 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use anyhow::Result; +use axum::http::Method; +use fedimint_core::api::InviteCode; +use router::handlers; +use tower_http::cors::{Any, CorsLayer}; +use tower_http::trace::TraceLayer; +use tracing::info; + +mod error; +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(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( + "/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), + ) + .route( + "/melt/quote/:method/:quote_id", + get(handlers::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)) +} diff --git a/clientd-stateless/src/router/handlers/check.rs b/clientd-stateless/src/router/handlers/check.rs new file mode 100644 index 0000000..406419e --- /dev/null +++ b/clientd-stateless/src/router/handlers/check.rs @@ -0,0 +1,34 @@ +use axum::extract::State; +use axum::Json; +use fedimint_core::Amount; +use 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/handlers/info.rs b/clientd-stateless/src/router/handlers/info.rs new file mode 100644 index 0000000..01452f0 --- /dev/null +++ b/clientd-stateless/src/router/handlers/info.rs @@ -0,0 +1,104 @@ +use std::collections::BTreeMap; + +use axum::extract::State; +use axum::Json; +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 client = state.get_cashu_client().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/handlers/keys.rs b/clientd-stateless/src/router/handlers/keys.rs new file mode 100644 index 0000000..eeb6c78 --- /dev/null +++ b/clientd-stateless/src/router/handlers/keys.rs @@ -0,0 +1,17 @@ +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/handlers/keysets.rs b/clientd-stateless/src/router/handlers/keysets.rs new file mode 100644 index 0000000..89b6e0d --- /dev/null +++ b/clientd-stateless/src/router/handlers/keysets.rs @@ -0,0 +1,10 @@ +use axum::extract::State; + +use crate::error::AppError; +use crate::state::AppState; + +#[axum_macros::debug_handler] +pub async fn handle_keysets(State(_state): State) -> Result<(), AppError> { + // TODO: Implement this function + Ok(()) +} diff --git a/clientd-stateless/src/router/handlers/melt/method.rs b/clientd-stateless/src/router/handlers/melt/method.rs new file mode 100644 index 0000000..5553c4d --- /dev/null +++ b/clientd-stateless/src/router/handlers/melt/method.rs @@ -0,0 +1,182 @@ +use std::str::FromStr; + +use anyhow::anyhow; +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::Json; +use fedimint_client::ClientHandleArc; +use fedimint_core::config::FederationId; +use fedimint_core::Amount; +use fedimint_ln_client::{LightningClientModule, OutgoingLightningPayment}; +use fedimint_wallet_client::{WalletClientModule, WithdrawState}; +use futures_util::StreamExt; +use lightning_invoice::Bolt11Invoice; +use serde::{Deserialize, Serialize}; +use tracing::{error, info}; + +use crate::error::AppError; +use crate::router::handlers::{Method, Unit}; +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/handlers/melt/mod.rs b/clientd-stateless/src/router/handlers/melt/mod.rs new file mode 100644 index 0000000..dd31219 --- /dev/null +++ b/clientd-stateless/src/router/handlers/melt/mod.rs @@ -0,0 +1,2 @@ +pub mod method; +pub mod quote; diff --git a/clientd-stateless/src/router/handlers/melt/quote.rs b/clientd-stateless/src/router/handlers/melt/quote.rs new file mode 100644 index 0000000..d5c2714 --- /dev/null +++ b/clientd-stateless/src/router/handlers/melt/quote.rs @@ -0,0 +1,16 @@ +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/handlers/mint/method.rs b/clientd-stateless/src/router/handlers/mint/method.rs new file mode 100644 index 0000000..3c7eb13 --- /dev/null +++ b/clientd-stateless/src/router/handlers/mint/method.rs @@ -0,0 +1,128 @@ +use std::time::Duration; + +use anyhow::anyhow; +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::Json; +use fedimint_client::ClientHandleArc; +use fedimint_core::config::FederationId; +use fedimint_core::time::now; +use fedimint_core::Amount; +use fedimint_ln_client::LightningClientModule; +use fedimint_wallet_client::WalletClientModule; +use lightning_invoice::{Bolt11InvoiceDescription, Description}; +use serde::{Deserialize, Serialize}; +use tracing::error; + +use crate::error::AppError; +use crate::router::handlers::{Method, Unit}; +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/handlers/mint/mod.rs b/clientd-stateless/src/router/handlers/mint/mod.rs new file mode 100644 index 0000000..dd31219 --- /dev/null +++ b/clientd-stateless/src/router/handlers/mint/mod.rs @@ -0,0 +1,2 @@ +pub mod method; +pub mod quote; diff --git a/clientd-stateless/src/router/handlers/mint/quote.rs b/clientd-stateless/src/router/handlers/mint/quote.rs new file mode 100644 index 0000000..d5c2714 --- /dev/null +++ b/clientd-stateless/src/router/handlers/mint/quote.rs @@ -0,0 +1,16 @@ +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/handlers/mod.rs b/clientd-stateless/src/router/handlers/mod.rs new file mode 100644 index 0000000..daff08c --- /dev/null +++ b/clientd-stateless/src/router/handlers/mod.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; + +pub mod check; +pub mod info; +pub mod keys; +pub mod keysets; +pub mod melt; +pub mod mint; +pub mod swap; + +#[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/handlers/swap.rs new file mode 100644 index 0000000..b9da850 --- /dev/null +++ b/clientd-stateless/src/router/handlers/swap.rs @@ -0,0 +1,52 @@ +use anyhow::anyhow; +use axum::extract::State; +use axum::http::StatusCode; +use axum::Json; +use fedimint_core::config::FederationId; +use fedimint_core::Amount; +use fedimint_mint_client::{MintClientModule, OOBNotes}; +use futures_util::StreamExt; +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 fedimint_mint_client::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/router/mod.rs b/clientd-stateless/src/router/mod.rs new file mode 100644 index 0000000..c3d4495 --- /dev/null +++ b/clientd-stateless/src/router/mod.rs @@ -0,0 +1 @@ +pub mod handlers; diff --git a/clientd-stateless/src/state.rs b/clientd-stateless/src/state.rs new file mode 100644 index 0000000..b9f91bb --- /dev/null +++ b/clientd-stateless/src/state.rs @@ -0,0 +1,70 @@ +use std::path::PathBuf; + +use anyhow::{anyhow, Result}; +use axum::http::StatusCode; +use fedimint_client::ClientHandleArc; +use fedimint_core::config::{FederationId, FederationIdPrefix}; +use multimint::MultiMint; + +use crate::error::AppError; +#[derive(Debug, Clone)] +pub struct AppState { + pub multimint: MultiMint, + pub cashu_mint: Option, +} + +impl AppState { + pub async fn new(fm_db_path: PathBuf) -> Result { + let clients = MultiMint::new(fm_db_path).await?; + clients.update_gateway_caches(true).await?; + Ok(Self { + multimint: clients, + cashu_mint: None, + }) + } + + // 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_cashu_client(&self) -> Result { + match self.cashu_mint { + Some(client) => match self.multimint.get(&client).await { + Some(client) => Ok(client), + None => Err(AppError::new( + StatusCode::BAD_REQUEST, + anyhow!("No cashu client found for federation id"), + )), + }, + None => Err(AppError::new( + StatusCode::BAD_REQUEST, + anyhow!("No cashu client set"), + )), + } + } + + 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 new file mode 100644 index 0000000..5321a72 --- /dev/null +++ b/clientd-stateless/src/utils.rs @@ -0,0 +1,11 @@ +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")), + } +} From 50ef2328a1875863cb67898387999a7f3de5f62b Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 27 Apr 2024 09:38:43 -0700 Subject: [PATCH 2/4] refactor: remove stateless from normal clientd --- fedimint-clientd/src/main.rs | 139 +++---------- .../handlers/{fedimint => }/admin/backup.rs | 0 .../handlers/{fedimint => }/admin/config.rs | 0 .../{fedimint => }/admin/discover_version.rs | 0 .../{fedimint => }/admin/federation_ids.rs | 0 .../handlers/{fedimint => }/admin/info.rs | 0 .../handlers/{fedimint => }/admin/join.rs | 0 .../{fedimint => }/admin/list_operations.rs | 0 .../handlers/{fedimint => }/admin/mod.rs | 0 .../handlers/{fedimint => }/admin/module.rs | 0 .../handlers/{fedimint => }/admin/restore.rs | 0 .../src/router/handlers/cashu/check.rs | 34 ---- .../src/router/handlers/cashu/info.rs | 104 ---------- .../src/router/handlers/cashu/keys.rs | 17 -- .../src/router/handlers/cashu/keysets.rs | 10 - .../src/router/handlers/cashu/melt/method.rs | 182 ------------------ .../src/router/handlers/cashu/melt/mod.rs | 2 - .../src/router/handlers/cashu/melt/quote.rs | 16 -- .../src/router/handlers/cashu/mint/method.rs | 128 ------------ .../src/router/handlers/cashu/mint/mod.rs | 2 - .../src/router/handlers/cashu/mint/quote.rs | 16 -- .../src/router/handlers/cashu/mod.rs | 23 --- .../src/router/handlers/cashu/swap.rs | 52 ----- .../src/router/handlers/fedimint/mod.rs | 4 - .../{fedimint => }/ln/await_invoice.rs | 0 .../ln/claim_external_receive_tweaked.rs | 0 .../handlers/{fedimint => }/ln/invoice.rs | 0 .../ln/invoice_external_pubkey_tweaked.rs | 0 .../{fedimint => }/ln/list_gateways.rs | 0 .../router/handlers/{fedimint => }/ln/mod.rs | 0 .../router/handlers/{fedimint => }/ln/pay.rs | 0 .../handlers/{fedimint => }/mint/combine.rs | 0 .../{fedimint => }/mint/decode_notes.rs | 0 .../{fedimint => }/mint/encode_notes.rs | 0 .../handlers/{fedimint => }/mint/mod.rs | 0 .../handlers/{fedimint => }/mint/reissue.rs | 0 .../handlers/{fedimint => }/mint/spend.rs | 0 .../handlers/{fedimint => }/mint/split.rs | 0 .../handlers/{fedimint => }/mint/validate.rs | 0 fedimint-clientd/src/router/handlers/mod.rs | 6 +- .../{fedimint => }/onchain/await_deposit.rs | 0 .../{fedimint => }/onchain/deposit_address.rs | 0 .../handlers/{fedimint => }/onchain/mod.rs | 0 .../{fedimint => }/onchain/withdraw.rs | 0 fedimint-clientd/src/router/ws.rs | 69 +++---- fedimint-clientd/src/state.rs | 22 +-- 46 files changed, 58 insertions(+), 768 deletions(-) rename fedimint-clientd/src/router/handlers/{fedimint => }/admin/backup.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/admin/config.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/admin/discover_version.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/admin/federation_ids.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/admin/info.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/admin/join.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/admin/list_operations.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/admin/mod.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/admin/module.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/admin/restore.rs (100%) delete mode 100644 fedimint-clientd/src/router/handlers/cashu/check.rs delete mode 100644 fedimint-clientd/src/router/handlers/cashu/info.rs delete mode 100644 fedimint-clientd/src/router/handlers/cashu/keys.rs delete mode 100644 fedimint-clientd/src/router/handlers/cashu/keysets.rs delete mode 100644 fedimint-clientd/src/router/handlers/cashu/melt/method.rs delete mode 100644 fedimint-clientd/src/router/handlers/cashu/melt/mod.rs delete mode 100644 fedimint-clientd/src/router/handlers/cashu/melt/quote.rs delete mode 100644 fedimint-clientd/src/router/handlers/cashu/mint/method.rs delete mode 100644 fedimint-clientd/src/router/handlers/cashu/mint/mod.rs delete mode 100644 fedimint-clientd/src/router/handlers/cashu/mint/quote.rs delete mode 100644 fedimint-clientd/src/router/handlers/cashu/mod.rs delete mode 100644 fedimint-clientd/src/router/handlers/cashu/swap.rs delete mode 100644 fedimint-clientd/src/router/handlers/fedimint/mod.rs rename fedimint-clientd/src/router/handlers/{fedimint => }/ln/await_invoice.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/ln/claim_external_receive_tweaked.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/ln/invoice.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/ln/invoice_external_pubkey_tweaked.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/ln/list_gateways.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/ln/mod.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/ln/pay.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/mint/combine.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/mint/decode_notes.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/mint/encode_notes.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/mint/mod.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/mint/reissue.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/mint/spend.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/mint/split.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/mint/validate.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/onchain/await_deposit.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/onchain/deposit_address.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/onchain/mod.rs (100%) rename fedimint-clientd/src/router/handlers/{fedimint => }/onchain/withdraw.rs (100%) diff --git a/fedimint-clientd/src/main.rs b/fedimint-clientd/src/main.rs index 18d348a..be57cb6 100644 --- a/fedimint-clientd/src/main.rs +++ b/fedimint-clientd/src/main.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use anyhow::Result; use axum::http::Method; use fedimint_core::api::InviteCode; +use router::handlers::{admin, ln, mint, onchain}; use router::ws::websocket_handler; use tower_http::cors::{Any, CorsLayer}; use tower_http::trace::TraceLayer; @@ -18,7 +19,6 @@ use axum::routing::{get, post}; use axum::Router; use axum_otel_metrics::HttpMetricsLayerBuilder; use clap::{Parser, Subcommand, ValueEnum}; -use router::handlers::*; use state::AppState; // use tower_http::cors::{Any, CorsLayer}; use tower_http::validate_request::ValidateRequestHeaderLayer; @@ -27,7 +27,6 @@ use tower_http::validate_request::ValidateRequestHeaderLayer; enum Mode { Rest, Ws, - Cashu, } impl FromStr for Mode { @@ -37,7 +36,6 @@ impl FromStr for Mode { match s { "rest" => Ok(Mode::Rest), "ws" => Ok(Mode::Ws), - "cashu" => Ok(Mode::Cashu), _ => Err(anyhow::anyhow!("Invalid mode")), } } @@ -103,9 +101,6 @@ async fn main() -> Result<()> { .register_new(invite_code, manual_secret) .await?; info!("Created client for federation id: {:?}", federation_id); - if cli.mode == Mode::Cashu { - state.cashu_mint = Some(federation_id); - } } Err(e) => { info!( @@ -128,10 +123,6 @@ async fn main() -> Result<()> { .route("/ws", get(websocket_handler)) .with_state(state) .layer(ValidateRequestHeaderLayer::bearer(&cli.password)), - Mode::Cashu => Router::new() - .nest("/v1", cashu_v1_rest()) - .with_state(state) - .layer(ValidateRequestHeaderLayer::bearer(&cli.password)), }; info!("Starting server in {:?} mode", cli.mode); @@ -213,72 +204,54 @@ async fn main() -> Result<()> { /// - `/fedimint/v2/onchain/withdraw`: Withdraw funds from the federation. fn fedimint_v2_rest() -> Router { let mint_router = Router::new() - .route( - "/decode-notes", - post(fedimint::mint::decode_notes::handle_rest), - ) - .route( - "/encode-notes", - post(fedimint::mint::encode_notes::handle_rest), - ) - .route("/reissue", post(fedimint::mint::reissue::handle_rest)) - .route("/spend", post(fedimint::mint::spend::handle_rest)) - .route("/validate", post(fedimint::mint::validate::handle_rest)) - .route("/split", post(fedimint::mint::split::handle_rest)) - .route("/combine", post(fedimint::mint::combine::handle_rest)); + .route("/decode-notes", post(mint::decode_notes::handle_rest)) + .route("/encode-notes", post(mint::encode_notes::handle_rest)) + .route("/reissue", post(mint::reissue::handle_rest)) + .route("/spend", post(mint::spend::handle_rest)) + .route("/validate", post(mint::validate::handle_rest)) + .route("/split", post(mint::split::handle_rest)) + .route("/combine", post(mint::combine::handle_rest)); let ln_router = Router::new() - .route("/invoice", post(fedimint::ln::invoice::handle_rest)) + .route("/invoice", post(ln::invoice::handle_rest)) .route( "/invoice-external-pubkey-tweaked", - post(fedimint::ln::invoice_external_pubkey_tweaked::handle_rest), - ) - .route( - "/await-invoice", - post(fedimint::ln::await_invoice::handle_rest), + post(ln::invoice_external_pubkey_tweaked::handle_rest), ) + .route("/await-invoice", post(ln::await_invoice::handle_rest)) .route( "/claim-external-receive-tweaked", - post(fedimint::ln::claim_external_receive_tweaked::handle_rest), + post(ln::claim_external_receive_tweaked::handle_rest), ) - .route("/pay", post(fedimint::ln::pay::handle_rest)) - .route( - "/list-gateways", - post(fedimint::ln::list_gateways::handle_rest), - ); + .route("/pay", post(ln::pay::handle_rest)) + .route("/list-gateways", post(ln::list_gateways::handle_rest)); let onchain_router = Router::new() .route( "/deposit-address", - post(fedimint::onchain::deposit_address::handle_rest), - ) - .route( - "/await-deposit", - post(fedimint::onchain::await_deposit::handle_rest), + post(onchain::deposit_address::handle_rest), ) - .route("/withdraw", post(fedimint::onchain::withdraw::handle_rest)); + .route("/await-deposit", post(onchain::await_deposit::handle_rest)) + .route("/withdraw", post(onchain::withdraw::handle_rest)); let admin_router = Router::new() - .route("/backup", post(fedimint::admin::backup::handle_rest)) + .route("/backup", post(admin::backup::handle_rest)) .route( "/discover-version", - post(fedimint::admin::discover_version::handle_rest), - ) - .route( - "/federation-ids", - get(fedimint::admin::federation_ids::handle_rest), + post(admin::discover_version::handle_rest), ) - .route("/info", get(fedimint::admin::info::handle_rest)) - .route("/join", post(fedimint::admin::join::handle_rest)) - .route("/restore", post(fedimint::admin::restore::handle_rest)) - // .route("/printsecret", get(fedimint::handle_printsecret)) TODO: should I expose this + .route("/federation-ids", get(admin::federation_ids::handle_rest)) + .route("/info", get(admin::info::handle_rest)) + .route("/join", post(admin::join::handle_rest)) + .route("/restore", post(admin::restore::handle_rest)) + // .route("/printsecret", get(handle_printsecret)) TODO: should I expose this // under admin? .route( "/list-operations", - post(fedimint::admin::list_operations::handle_rest), + post(admin::list_operations::handle_rest), ) - .route("/module", post(fedimint::admin::module::handle_rest)) - .route("/config", get(fedimint::admin::config::handle_rest)); + .route("/module", post(admin::module::handle_rest)) + .route("/config", get(admin::config::handle_rest)); Router::new() .nest("/admin", admin_router) @@ -286,61 +259,3 @@ fn fedimint_v2_rest() -> Router { .nest("/ln", ln_router) .nest("/onchain", onchain_router) } - -/// 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(cashu::keys::handle_keys)) - .route("/keys/:keyset_id", get(cashu::keys::handle_keys_keyset_id)) - .route("/keysets", get(cashu::keysets::handle_keysets)) - .route("/swap", post(cashu::swap::handle_swap)) - .route( - "/mint/quote/:method", - get(cashu::mint::quote::handle_method), - ) - .route( - "/mint/quote/:method/:quote_id", - get(cashu::mint::quote::handle_method_quote_id), - ) - .route("/mint/:method", post(cashu::mint::method::handle_method)) - .route( - "/melt/quote/:method", - get(cashu::melt::quote::handle_method), - ) - .route( - "/melt/quote/:method/:quote_id", - get(cashu::melt::quote::handle_method_quote_id), - ) - .route("/melt/:method", post(cashu::melt::method::handle_method)) - .route("/info", get(cashu::info::handle_info)) - .route("/check", post(cashu::check::handle_check)) -} diff --git a/fedimint-clientd/src/router/handlers/fedimint/admin/backup.rs b/fedimint-clientd/src/router/handlers/admin/backup.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/admin/backup.rs rename to fedimint-clientd/src/router/handlers/admin/backup.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/admin/config.rs b/fedimint-clientd/src/router/handlers/admin/config.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/admin/config.rs rename to fedimint-clientd/src/router/handlers/admin/config.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/admin/discover_version.rs b/fedimint-clientd/src/router/handlers/admin/discover_version.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/admin/discover_version.rs rename to fedimint-clientd/src/router/handlers/admin/discover_version.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/admin/federation_ids.rs b/fedimint-clientd/src/router/handlers/admin/federation_ids.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/admin/federation_ids.rs rename to fedimint-clientd/src/router/handlers/admin/federation_ids.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/admin/info.rs b/fedimint-clientd/src/router/handlers/admin/info.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/admin/info.rs rename to fedimint-clientd/src/router/handlers/admin/info.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/admin/join.rs b/fedimint-clientd/src/router/handlers/admin/join.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/admin/join.rs rename to fedimint-clientd/src/router/handlers/admin/join.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/admin/list_operations.rs b/fedimint-clientd/src/router/handlers/admin/list_operations.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/admin/list_operations.rs rename to fedimint-clientd/src/router/handlers/admin/list_operations.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/admin/mod.rs b/fedimint-clientd/src/router/handlers/admin/mod.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/admin/mod.rs rename to fedimint-clientd/src/router/handlers/admin/mod.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/admin/module.rs b/fedimint-clientd/src/router/handlers/admin/module.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/admin/module.rs rename to fedimint-clientd/src/router/handlers/admin/module.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/admin/restore.rs b/fedimint-clientd/src/router/handlers/admin/restore.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/admin/restore.rs rename to fedimint-clientd/src/router/handlers/admin/restore.rs diff --git a/fedimint-clientd/src/router/handlers/cashu/check.rs b/fedimint-clientd/src/router/handlers/cashu/check.rs deleted file mode 100644 index 406419e..0000000 --- a/fedimint-clientd/src/router/handlers/cashu/check.rs +++ /dev/null @@ -1,34 +0,0 @@ -use axum::extract::State; -use axum::Json; -use fedimint_core::Amount; -use 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/fedimint-clientd/src/router/handlers/cashu/info.rs b/fedimint-clientd/src/router/handlers/cashu/info.rs deleted file mode 100644 index 01452f0..0000000 --- a/fedimint-clientd/src/router/handlers/cashu/info.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::collections::BTreeMap; - -use axum::extract::State; -use axum::Json; -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 client = state.get_cashu_client().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/fedimint-clientd/src/router/handlers/cashu/keys.rs b/fedimint-clientd/src/router/handlers/cashu/keys.rs deleted file mode 100644 index eeb6c78..0000000 --- a/fedimint-clientd/src/router/handlers/cashu/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/fedimint-clientd/src/router/handlers/cashu/keysets.rs b/fedimint-clientd/src/router/handlers/cashu/keysets.rs deleted file mode 100644 index 89b6e0d..0000000 --- a/fedimint-clientd/src/router/handlers/cashu/keysets.rs +++ /dev/null @@ -1,10 +0,0 @@ -use axum::extract::State; - -use crate::error::AppError; -use crate::state::AppState; - -#[axum_macros::debug_handler] -pub async fn handle_keysets(State(_state): State) -> Result<(), AppError> { - // TODO: Implement this function - Ok(()) -} diff --git a/fedimint-clientd/src/router/handlers/cashu/melt/method.rs b/fedimint-clientd/src/router/handlers/cashu/melt/method.rs deleted file mode 100644 index 3da6347..0000000 --- a/fedimint-clientd/src/router/handlers/cashu/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 fedimint_client::ClientHandleArc; -use fedimint_core::config::FederationId; -use fedimint_core::Amount; -use fedimint_ln_client::{LightningClientModule, OutgoingLightningPayment}; -use fedimint_wallet_client::{WalletClientModule, WithdrawState}; -use futures_util::StreamExt; -use lightning_invoice::Bolt11Invoice; -use serde::{Deserialize, Serialize}; -use tracing::{error, info}; - -use crate::error::AppError; -use crate::router::handlers::cashu::{Method, Unit}; -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/fedimint-clientd/src/router/handlers/cashu/melt/mod.rs b/fedimint-clientd/src/router/handlers/cashu/melt/mod.rs deleted file mode 100644 index dd31219..0000000 --- a/fedimint-clientd/src/router/handlers/cashu/melt/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod method; -pub mod quote; diff --git a/fedimint-clientd/src/router/handlers/cashu/melt/quote.rs b/fedimint-clientd/src/router/handlers/cashu/melt/quote.rs deleted file mode 100644 index d5c2714..0000000 --- a/fedimint-clientd/src/router/handlers/cashu/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/fedimint-clientd/src/router/handlers/cashu/mint/method.rs b/fedimint-clientd/src/router/handlers/cashu/mint/method.rs deleted file mode 100644 index c7d4993..0000000 --- a/fedimint-clientd/src/router/handlers/cashu/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 fedimint_client::ClientHandleArc; -use fedimint_core::config::FederationId; -use fedimint_core::time::now; -use fedimint_core::Amount; -use fedimint_ln_client::LightningClientModule; -use fedimint_wallet_client::WalletClientModule; -use lightning_invoice::{Bolt11InvoiceDescription, Description}; -use serde::{Deserialize, Serialize}; -use tracing::error; - -use crate::error::AppError; -use crate::router::handlers::cashu::{Method, Unit}; -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/fedimint-clientd/src/router/handlers/cashu/mint/mod.rs b/fedimint-clientd/src/router/handlers/cashu/mint/mod.rs deleted file mode 100644 index dd31219..0000000 --- a/fedimint-clientd/src/router/handlers/cashu/mint/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod method; -pub mod quote; diff --git a/fedimint-clientd/src/router/handlers/cashu/mint/quote.rs b/fedimint-clientd/src/router/handlers/cashu/mint/quote.rs deleted file mode 100644 index d5c2714..0000000 --- a/fedimint-clientd/src/router/handlers/cashu/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/fedimint-clientd/src/router/handlers/cashu/mod.rs b/fedimint-clientd/src/router/handlers/cashu/mod.rs deleted file mode 100644 index daff08c..0000000 --- a/fedimint-clientd/src/router/handlers/cashu/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -use serde::{Deserialize, Serialize}; - -pub mod check; -pub mod info; -pub mod keys; -pub mod keysets; -pub mod melt; -pub mod mint; -pub mod swap; - -#[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/fedimint-clientd/src/router/handlers/cashu/swap.rs b/fedimint-clientd/src/router/handlers/cashu/swap.rs deleted file mode 100644 index b9da850..0000000 --- a/fedimint-clientd/src/router/handlers/cashu/swap.rs +++ /dev/null @@ -1,52 +0,0 @@ -use anyhow::anyhow; -use axum::extract::State; -use axum::http::StatusCode; -use axum::Json; -use fedimint_core::config::FederationId; -use fedimint_core::Amount; -use fedimint_mint_client::{MintClientModule, OOBNotes}; -use futures_util::StreamExt; -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 fedimint_mint_client::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/fedimint-clientd/src/router/handlers/fedimint/mod.rs b/fedimint-clientd/src/router/handlers/fedimint/mod.rs deleted file mode 100644 index ba61bce..0000000 --- a/fedimint-clientd/src/router/handlers/fedimint/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod admin; -pub mod ln; -pub mod mint; -pub mod onchain; diff --git a/fedimint-clientd/src/router/handlers/fedimint/ln/await_invoice.rs b/fedimint-clientd/src/router/handlers/ln/await_invoice.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/ln/await_invoice.rs rename to fedimint-clientd/src/router/handlers/ln/await_invoice.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/ln/claim_external_receive_tweaked.rs b/fedimint-clientd/src/router/handlers/ln/claim_external_receive_tweaked.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/ln/claim_external_receive_tweaked.rs rename to fedimint-clientd/src/router/handlers/ln/claim_external_receive_tweaked.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/ln/invoice.rs b/fedimint-clientd/src/router/handlers/ln/invoice.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/ln/invoice.rs rename to fedimint-clientd/src/router/handlers/ln/invoice.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/ln/invoice_external_pubkey_tweaked.rs b/fedimint-clientd/src/router/handlers/ln/invoice_external_pubkey_tweaked.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/ln/invoice_external_pubkey_tweaked.rs rename to fedimint-clientd/src/router/handlers/ln/invoice_external_pubkey_tweaked.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/ln/list_gateways.rs b/fedimint-clientd/src/router/handlers/ln/list_gateways.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/ln/list_gateways.rs rename to fedimint-clientd/src/router/handlers/ln/list_gateways.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/ln/mod.rs b/fedimint-clientd/src/router/handlers/ln/mod.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/ln/mod.rs rename to fedimint-clientd/src/router/handlers/ln/mod.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/ln/pay.rs b/fedimint-clientd/src/router/handlers/ln/pay.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/ln/pay.rs rename to fedimint-clientd/src/router/handlers/ln/pay.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/mint/combine.rs b/fedimint-clientd/src/router/handlers/mint/combine.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/mint/combine.rs rename to fedimint-clientd/src/router/handlers/mint/combine.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/mint/decode_notes.rs b/fedimint-clientd/src/router/handlers/mint/decode_notes.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/mint/decode_notes.rs rename to fedimint-clientd/src/router/handlers/mint/decode_notes.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/mint/encode_notes.rs b/fedimint-clientd/src/router/handlers/mint/encode_notes.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/mint/encode_notes.rs rename to fedimint-clientd/src/router/handlers/mint/encode_notes.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/mint/mod.rs b/fedimint-clientd/src/router/handlers/mint/mod.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/mint/mod.rs rename to fedimint-clientd/src/router/handlers/mint/mod.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/mint/reissue.rs b/fedimint-clientd/src/router/handlers/mint/reissue.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/mint/reissue.rs rename to fedimint-clientd/src/router/handlers/mint/reissue.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/mint/spend.rs b/fedimint-clientd/src/router/handlers/mint/spend.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/mint/spend.rs rename to fedimint-clientd/src/router/handlers/mint/spend.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/mint/split.rs b/fedimint-clientd/src/router/handlers/mint/split.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/mint/split.rs rename to fedimint-clientd/src/router/handlers/mint/split.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/mint/validate.rs b/fedimint-clientd/src/router/handlers/mint/validate.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/mint/validate.rs rename to fedimint-clientd/src/router/handlers/mint/validate.rs diff --git a/fedimint-clientd/src/router/handlers/mod.rs b/fedimint-clientd/src/router/handlers/mod.rs index d8b0f2f..ba61bce 100644 --- a/fedimint-clientd/src/router/handlers/mod.rs +++ b/fedimint-clientd/src/router/handlers/mod.rs @@ -1,2 +1,4 @@ -pub mod cashu; -pub mod fedimint; +pub mod admin; +pub mod ln; +pub mod mint; +pub mod onchain; diff --git a/fedimint-clientd/src/router/handlers/fedimint/onchain/await_deposit.rs b/fedimint-clientd/src/router/handlers/onchain/await_deposit.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/onchain/await_deposit.rs rename to fedimint-clientd/src/router/handlers/onchain/await_deposit.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/onchain/deposit_address.rs b/fedimint-clientd/src/router/handlers/onchain/deposit_address.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/onchain/deposit_address.rs rename to fedimint-clientd/src/router/handlers/onchain/deposit_address.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/onchain/mod.rs b/fedimint-clientd/src/router/handlers/onchain/mod.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/onchain/mod.rs rename to fedimint-clientd/src/router/handlers/onchain/mod.rs diff --git a/fedimint-clientd/src/router/handlers/fedimint/onchain/withdraw.rs b/fedimint-clientd/src/router/handlers/onchain/withdraw.rs similarity index 100% rename from fedimint-clientd/src/router/handlers/fedimint/onchain/withdraw.rs rename to fedimint-clientd/src/router/handlers/onchain/withdraw.rs diff --git a/fedimint-clientd/src/router/ws.rs b/fedimint-clientd/src/router/ws.rs index c972a2c..e1da903 100644 --- a/fedimint-clientd/src/router/ws.rs +++ b/fedimint-clientd/src/router/ws.rs @@ -163,85 +163,68 @@ async fn send_err_invalid_req( async fn match_method(req: JsonRpcRequest, state: AppState) -> Result { match req.method { JsonRpcMethod::AdminBackup => { - handlers::fedimint::admin::backup::handle_ws(state.clone(), req.params).await - } - JsonRpcMethod::AdminConfig => { - handlers::fedimint::admin::config::handle_ws(state.clone()).await + handlers::admin::backup::handle_ws(state.clone(), req.params).await } + JsonRpcMethod::AdminConfig => handlers::admin::config::handle_ws(state.clone()).await, JsonRpcMethod::AdminDiscoverVersion => { - handlers::fedimint::admin::discover_version::handle_ws(state.clone(), req.params).await + handlers::admin::discover_version::handle_ws(state.clone(), req.params).await } JsonRpcMethod::AdminFederationIds => { - handlers::fedimint::admin::federation_ids::handle_ws(state.clone(), req.params).await + handlers::admin::federation_ids::handle_ws(state.clone(), req.params).await } JsonRpcMethod::AdminInfo => { - handlers::fedimint::admin::info::handle_ws(state.clone(), req.params).await + handlers::admin::info::handle_ws(state.clone(), req.params).await } JsonRpcMethod::AdminJoin => { - handlers::fedimint::admin::join::handle_ws(state.clone(), req.params).await + handlers::admin::join::handle_ws(state.clone(), req.params).await } JsonRpcMethod::AdminModule => { - handlers::fedimint::admin::module::handle_ws(state.clone(), req.params).await + handlers::admin::module::handle_ws(state.clone(), req.params).await } JsonRpcMethod::AdminRestore => { - handlers::fedimint::admin::restore::handle_ws(state.clone(), req.params).await + handlers::admin::restore::handle_ws(state.clone(), req.params).await } JsonRpcMethod::AdminListOperations => { - handlers::fedimint::admin::list_operations::handle_ws(state.clone(), req.params).await - } - JsonRpcMethod::MintDecodeNotes => { - handlers::fedimint::mint::decode_notes::handle_ws(req.params).await - } - JsonRpcMethod::MintEncodeNotes => { - handlers::fedimint::mint::encode_notes::handle_ws(req.params).await + handlers::admin::list_operations::handle_ws(state.clone(), req.params).await } + JsonRpcMethod::MintDecodeNotes => handlers::mint::decode_notes::handle_ws(req.params).await, + JsonRpcMethod::MintEncodeNotes => handlers::mint::encode_notes::handle_ws(req.params).await, JsonRpcMethod::MintReissue => { - handlers::fedimint::mint::reissue::handle_ws(state.clone(), req.params).await + handlers::mint::reissue::handle_ws(state.clone(), req.params).await } JsonRpcMethod::MintSpend => { - handlers::fedimint::mint::spend::handle_ws(state.clone(), req.params).await + handlers::mint::spend::handle_ws(state.clone(), req.params).await } JsonRpcMethod::MintValidate => { - handlers::fedimint::mint::validate::handle_ws(state.clone(), req.params).await - } - JsonRpcMethod::MintSplit => handlers::fedimint::mint::split::handle_ws(req.params).await, - JsonRpcMethod::MintCombine => { - handlers::fedimint::mint::combine::handle_ws(req.params).await + handlers::mint::validate::handle_ws(state.clone(), req.params).await } + JsonRpcMethod::MintSplit => handlers::mint::split::handle_ws(req.params).await, + JsonRpcMethod::MintCombine => handlers::mint::combine::handle_ws(req.params).await, JsonRpcMethod::LnInvoice => { - handlers::fedimint::ln::invoice::handle_ws(state.clone(), req.params).await + handlers::ln::invoice::handle_ws(state.clone(), req.params).await } JsonRpcMethod::LnInvoiceExternalPubkeyTweaked => { - handlers::fedimint::ln::invoice_external_pubkey_tweaked::handle_ws( - state.clone(), - req.params, - ) - .await + handlers::ln::invoice_external_pubkey_tweaked::handle_ws(state.clone(), req.params) + .await } JsonRpcMethod::LnAwaitInvoice => { - handlers::fedimint::ln::await_invoice::handle_ws(state.clone(), req.params).await + handlers::ln::await_invoice::handle_ws(state.clone(), req.params).await } JsonRpcMethod::LnClaimExternalReceiveTweaked => { - handlers::fedimint::ln::claim_external_receive_tweaked::handle_ws( - state.clone(), - req.params, - ) - .await - } - JsonRpcMethod::LnPay => { - handlers::fedimint::ln::pay::handle_ws(state.clone(), req.params).await + handlers::ln::claim_external_receive_tweaked::handle_ws(state.clone(), req.params).await } + JsonRpcMethod::LnPay => handlers::ln::pay::handle_ws(state.clone(), req.params).await, JsonRpcMethod::LnListGateways => { - handlers::fedimint::ln::list_gateways::handle_ws(state.clone(), req.params).await + handlers::ln::list_gateways::handle_ws(state.clone(), req.params).await } JsonRpcMethod::WalletDepositAddress => { - handlers::fedimint::onchain::deposit_address::handle_ws(state.clone(), req.params).await + handlers::onchain::deposit_address::handle_ws(state.clone(), req.params).await } JsonRpcMethod::WalletAwaitDeposit => { - handlers::fedimint::onchain::await_deposit::handle_ws(state.clone(), req.params).await + handlers::onchain::await_deposit::handle_ws(state.clone(), req.params).await } JsonRpcMethod::WalletWithdraw => { - handlers::fedimint::onchain::withdraw::handle_ws(state.clone(), req.params).await + handlers::onchain::withdraw::handle_ws(state.clone(), req.params).await } } } diff --git a/fedimint-clientd/src/state.rs b/fedimint-clientd/src/state.rs index b9f91bb..7db3af5 100644 --- a/fedimint-clientd/src/state.rs +++ b/fedimint-clientd/src/state.rs @@ -10,17 +10,13 @@ use crate::error::AppError; #[derive(Debug, Clone)] pub struct AppState { pub multimint: MultiMint, - pub cashu_mint: Option, } impl AppState { pub async fn new(fm_db_path: PathBuf) -> Result { let clients = MultiMint::new(fm_db_path).await?; clients.update_gateway_caches(true).await?; - Ok(Self { - multimint: clients, - cashu_mint: None, - }) + Ok(Self { multimint: clients }) } // Helper function to get a specific client from the state or default @@ -37,22 +33,6 @@ impl AppState { } } - pub async fn get_cashu_client(&self) -> Result { - match self.cashu_mint { - Some(client) => match self.multimint.get(&client).await { - Some(client) => Ok(client), - None => Err(AppError::new( - StatusCode::BAD_REQUEST, - anyhow!("No cashu client found for federation id"), - )), - }, - None => Err(AppError::new( - StatusCode::BAD_REQUEST, - anyhow!("No cashu client set"), - )), - } - } - pub async fn get_client_by_prefix( &self, federation_id_prefix: &FederationIdPrefix, From 1eeb28461fbe6509c0ba173dadaee24900623c98 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 27 Apr 2024 09:42:33 -0700 Subject: [PATCH 3/4] fix: broken async py --- fedimint-clientd/src/router/handlers/ln/pay.rs | 2 +- fedimint-clientd/src/utils.rs | 2 +- wrappers/fedimint-py/test_async.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fedimint-clientd/src/router/handlers/ln/pay.rs b/fedimint-clientd/src/router/handlers/ln/pay.rs index bc2e6d8..27b1a1a 100644 --- a/fedimint-clientd/src/router/handlers/ln/pay.rs +++ b/fedimint-clientd/src/router/handlers/ln/pay.rs @@ -13,7 +13,7 @@ use serde_json::{json, Value}; use tracing::{error, info}; use crate::error::AppError; -use crate::router::handlers::fedimint::ln::{get_invoice, wait_for_ln_payment}; +use crate::router::handlers::ln::{get_invoice, wait_for_ln_payment}; use crate::state::AppState; #[derive(Debug, Deserialize)] diff --git a/fedimint-clientd/src/utils.rs b/fedimint-clientd/src/utils.rs index 5321a72..2873a68 100644 --- a/fedimint-clientd/src/utils.rs +++ b/fedimint-clientd/src/utils.rs @@ -3,7 +3,7 @@ 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 { +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")), diff --git a/wrappers/fedimint-py/test_async.py b/wrappers/fedimint-py/test_async.py index e85747c..75e5f1f 100644 --- a/wrappers/fedimint-py/test_async.py +++ b/wrappers/fedimint-py/test_async.py @@ -107,7 +107,7 @@ async def main(): ) # Pay the invoice - fedimint_client.lightning.pay(data["invoice"], None, None) + await fedimint_client.lightning.pay(data["invoice"], None, None) print("Paid locked invoice!") # `/v2/ln/claim-external-pubkey-tweaked` From 4bf43c06eca9dff3d1c42f6ca34d111dc00def36 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 27 Apr 2024 10:55:03 -0700 Subject: [PATCH 4/4] feat: start stateless token translation --- Cargo.lock | 3 + clientd-stateless/Cargo.toml | 3 + clientd-stateless/examples/cashu_encoding.rs | 1 + clientd-stateless/src/router/handlers/info.rs | 9 +- clientd-stateless/src/router/handlers/mod.rs | 148 ++++++++++++++++++ clientd-stateless/src/state.rs | 22 +-- 6 files changed, 164 insertions(+), 22 deletions(-) create mode 100644 clientd-stateless/examples/cashu_encoding.rs diff --git a/Cargo.lock b/Cargo.lock index 6030ccc..5e1bc41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -706,6 +706,7 @@ dependencies = [ "axum", "axum-macros", "axum-otel-metrics", + "base64 0.22.0", "bitcoin 0.29.2", "bitcoin_hashes 0.13.0", "chrono", @@ -717,8 +718,10 @@ dependencies = [ "fedimint-ln-client", "fedimint-mint-client", "fedimint-rocksdb", + "fedimint-tbs", "fedimint-wallet-client", "futures-util", + "hex", "itertools 0.12.1", "lazy_static", "lightning-invoice", diff --git a/clientd-stateless/Cargo.toml b/clientd-stateless/Cargo.toml index c858268..16fb465 100644 --- a/clientd-stateless/Cargo.toml +++ b/clientd-stateless/Cargo.toml @@ -44,3 +44,6 @@ clap = { version = "3", features = ["derive", "env"] } multimint = { version = "0.3.2" } # 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/examples/cashu_encoding.rs b/clientd-stateless/examples/cashu_encoding.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/clientd-stateless/examples/cashu_encoding.rs @@ -0,0 +1 @@ + diff --git a/clientd-stateless/src/router/handlers/info.rs b/clientd-stateless/src/router/handlers/info.rs index 01452f0..d43ca91 100644 --- a/clientd-stateless/src/router/handlers/info.rs +++ b/clientd-stateless/src/router/handlers/info.rs @@ -1,7 +1,10 @@ use std::collections::BTreeMap; +use std::env; +use std::str::FromStr; use axum::extract::State; use axum::Json; +use fedimint_core::config::FederationId; use serde::Serialize; use crate::error::AppError; @@ -42,7 +45,11 @@ pub struct CashuNUT06InfoResponse { pub async fn handle_info( State(state): State, ) -> Result, AppError> { - let client = state.get_cashu_client().await?; + 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(); diff --git a/clientd-stateless/src/router/handlers/mod.rs b/clientd-stateless/src/router/handlers/mod.rs index daff08c..f4f3512 100644 --- a/clientd-stateless/src/router/handlers/mod.rs +++ b/clientd-stateless/src/router/handlers/mod.rs @@ -1,4 +1,20 @@ +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; @@ -8,6 +24,138 @@ 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 { diff --git a/clientd-stateless/src/state.rs b/clientd-stateless/src/state.rs index b9f91bb..7db3af5 100644 --- a/clientd-stateless/src/state.rs +++ b/clientd-stateless/src/state.rs @@ -10,17 +10,13 @@ use crate::error::AppError; #[derive(Debug, Clone)] pub struct AppState { pub multimint: MultiMint, - pub cashu_mint: Option, } impl AppState { pub async fn new(fm_db_path: PathBuf) -> Result { let clients = MultiMint::new(fm_db_path).await?; clients.update_gateway_caches(true).await?; - Ok(Self { - multimint: clients, - cashu_mint: None, - }) + Ok(Self { multimint: clients }) } // Helper function to get a specific client from the state or default @@ -37,22 +33,6 @@ impl AppState { } } - pub async fn get_cashu_client(&self) -> Result { - match self.cashu_mint { - Some(client) => match self.multimint.get(&client).await { - Some(client) => Ok(client), - None => Err(AppError::new( - StatusCode::BAD_REQUEST, - anyhow!("No cashu client found for federation id"), - )), - }, - None => Err(AppError::new( - StatusCode::BAD_REQUEST, - anyhow!("No cashu client set"), - )), - } - } - pub async fn get_client_by_prefix( &self, federation_id_prefix: &FederationIdPrefix,