From 81acc200db23a80cf279dc77db26ac044f3020ce Mon Sep 17 00:00:00 2001 From: amackillop Date: Thu, 2 Apr 2026 15:48:41 -0700 Subject: [PATCH 1/6] Move existing modules into daemon/ and mdk/ directories Split the crate into library + binary targets so we can extract a public MdkClient facade. Binary-only modules (api, store, webhook, event_loop, expiry, logger, secret, time, types, config TOML parsing) move under src/daemon/. The oRPC client moves to src/mdk/mdk_api/. Infrastructure types (NetworkInfra, ChainSource, LspInfra) split out of config.rs into src/mdk/config.rs. New src/lib.rs re-exports mdk::config and mdk::mdk_api at the crate root to avoid mdk::mdk:: stutter. Cargo.toml adds [lib] name = "mdk" and [[bin]] name = "mdkd" targets. Pure file moves and import rewrites. No logic changes. All 49 tests pass. --- Cargo.toml | 8 ++ src/{ => daemon}/api/auth.rs | 2 +- src/{ => daemon}/api/balance.rs | 4 +- src/{ => daemon}/api/channels.rs | 4 +- src/{ => daemon}/api/decode.rs | 6 +- src/{ => daemon}/api/error.rs | 2 +- src/{ => daemon}/api/info.rs | 4 +- src/{ => daemon}/api/invoices.rs | 19 +-- src/{ => daemon}/api/mod.rs | 11 +- src/{ => daemon}/api/onchain.rs | 8 +- src/{ => daemon}/api/websocket.rs | 2 +- src/{ => daemon}/config.rs | 126 -------------------- src/{ => daemon}/event_loop.rs | 13 ++- src/{ => daemon}/expiry.rs | 8 +- src/{ => daemon}/logger.rs | 0 src/daemon/mod.rs | 10 ++ src/{ => daemon}/secret.rs | 0 src/{ => daemon}/store/invoice_metadata.rs | 0 src/{ => daemon}/store/mod.rs | 0 src/{ => daemon}/time.rs | 0 src/{ => daemon}/types.rs | 0 src/{ => daemon}/webhook/dispatcher.rs | 2 +- src/{ => daemon}/webhook/mod.rs | 0 src/lib.rs | 4 + src/main.rs | 31 ++--- src/mdk/config.rs | 129 +++++++++++++++++++++ src/mdk/{ => mdk_api}/client.rs | 0 src/mdk/mdk_api/mod.rs | 2 + src/mdk/{ => mdk_api}/types.rs | 0 src/mdk/mod.rs | 4 +- 30 files changed, 210 insertions(+), 189 deletions(-) rename src/{ => daemon}/api/auth.rs (99%) rename src/{ => daemon}/api/balance.rs (93%) rename src/{ => daemon}/api/channels.rs (91%) rename src/{ => daemon}/api/decode.rs (98%) rename src/{ => daemon}/api/error.rs (95%) rename src/{ => daemon}/api/info.rs (88%) rename src/{ => daemon}/api/invoices.rs (97%) rename src/{ => daemon}/api/mod.rs (97%) rename src/{ => daemon}/api/onchain.rs (85%) rename src/{ => daemon}/api/websocket.rs (99%) rename src/{ => daemon}/config.rs (52%) rename src/{ => daemon}/event_loop.rs (94%) rename src/{ => daemon}/expiry.rs (89%) rename src/{ => daemon}/logger.rs (100%) create mode 100644 src/daemon/mod.rs rename src/{ => daemon}/secret.rs (100%) rename src/{ => daemon}/store/invoice_metadata.rs (100%) rename src/{ => daemon}/store/mod.rs (100%) rename src/{ => daemon}/time.rs (100%) rename src/{ => daemon}/types.rs (100%) rename src/{ => daemon}/webhook/dispatcher.rs (98%) rename src/{ => daemon}/webhook/mod.rs (100%) create mode 100644 src/lib.rs create mode 100644 src/mdk/config.rs rename src/mdk/{ => mdk_api}/client.rs (100%) create mode 100644 src/mdk/mdk_api/mod.rs rename src/mdk/{ => mdk_api}/types.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 7814f92..bcf5bd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,14 @@ name = "mdkd" version = "0.0.1" edition = "2021" +[lib] +name = "mdk" +path = "src/lib.rs" + +[[bin]] +name = "mdkd" +path = "src/main.rs" + [features] demo = [] diff --git a/src/api/auth.rs b/src/daemon/api/auth.rs similarity index 99% rename from src/api/auth.rs rename to src/daemon/api/auth.rs index 6a83cf2..c51fa02 100644 --- a/src/api/auth.rs +++ b/src/daemon/api/auth.rs @@ -7,7 +7,7 @@ use axum::Json; use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; -use crate::types::ApiError; +use crate::daemon::types::ApiError; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AccessLevel { diff --git a/src/api/balance.rs b/src/daemon/api/balance.rs similarity index 93% rename from src/api/balance.rs rename to src/daemon/api/balance.rs index 4fa2a61..3349460 100644 --- a/src/api/balance.rs +++ b/src/daemon/api/balance.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use axum::Json; use ldk_node::Node; -use crate::api::error::AppError; -use crate::types::GetBalanceResponse; +use crate::daemon::api::error::AppError; +use crate::daemon::types::GetBalanceResponse; /// Returns the node's Lightning and on-chain balances. /// diff --git a/src/api/channels.rs b/src/daemon/api/channels.rs similarity index 91% rename from src/api/channels.rs rename to src/daemon/api/channels.rs index 7dc6741..9fda079 100644 --- a/src/api/channels.rs +++ b/src/daemon/api/channels.rs @@ -6,8 +6,8 @@ use hex::FromHex; use ldk_node::lightning::ln::types::ChannelId; use ldk_node::Node; -use crate::api::error::AppError; -use crate::types::{ChannelInfo, CloseChannelRequest}; +use crate::daemon::api::error::AppError; +use crate::daemon::types::{ChannelInfo, CloseChannelRequest}; pub async fn handle_list_channels(node: Arc) -> Result>, AppError> { let channels = node.list_channels().iter().map(ChannelInfo::from).collect(); diff --git a/src/api/decode.rs b/src/daemon/api/decode.rs similarity index 98% rename from src/api/decode.rs rename to src/daemon/api/decode.rs index 28c57a3..dc7520a 100644 --- a/src/api/decode.rs +++ b/src/daemon/api/decode.rs @@ -4,8 +4,8 @@ use axum::Json; use ldk_node::lightning::offers::offer::{Amount, Offer}; use ldk_node::lightning_invoice::Bolt11Invoice; -use crate::api::error::AppError; -use crate::types::{ +use crate::daemon::api::error::AppError; +use crate::daemon::types::{ DecodeInvoiceRequest, DecodeInvoiceResponse, DecodeOfferRequest, DecodeOfferResponse, RoutingHint, RoutingHintHop, }; @@ -100,7 +100,7 @@ mod tests { use ldk_node::bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use ldk_node::lightning::offers::offer::OfferBuilder; - use crate::types::{DecodeInvoiceRequest, DecodeOfferRequest}; + use crate::daemon::types::{DecodeInvoiceRequest, DecodeOfferRequest}; // Signet invoice with LSPS4 JIT route hint (single hop), "1 cup coffee". // { diff --git a/src/api/error.rs b/src/daemon/api/error.rs similarity index 95% rename from src/api/error.rs rename to src/daemon/api/error.rs index 3338f4c..a02b5c4 100644 --- a/src/api/error.rs +++ b/src/daemon/api/error.rs @@ -2,7 +2,7 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; -use crate::types::ApiError; +use crate::daemon::types::ApiError; #[derive(Debug)] pub enum AppError { diff --git a/src/api/info.rs b/src/daemon/api/info.rs similarity index 88% rename from src/api/info.rs rename to src/daemon/api/info.rs index 30e297d..c9d33ea 100644 --- a/src/api/info.rs +++ b/src/daemon/api/info.rs @@ -4,8 +4,8 @@ use axum::Json; use ldk_node::bitcoin::Network; use ldk_node::Node; -use crate::api::error::AppError; -use crate::types::{ChannelInfo, GetInfoResponse}; +use crate::daemon::api::error::AppError; +use crate::daemon::types::{ChannelInfo, GetInfoResponse}; const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/src/api/invoices.rs b/src/daemon/api/invoices.rs similarity index 97% rename from src/api/invoices.rs rename to src/daemon/api/invoices.rs index ea57c56..0991183 100644 --- a/src/api/invoices.rs +++ b/src/daemon/api/invoices.rs @@ -13,11 +13,12 @@ use ldk_node::payment::{PaymentDetails, PaymentDirection, PaymentKind, PaymentSt use ldk_node::Node; use log::{error, info}; -use crate::api::error::AppError; -use crate::mdk::client::MdkApiClient; -use crate::mdk::types::{CheckoutCustomer, CreateCheckoutRequest, RegisterInvoiceRequest}; -use crate::store::invoice_metadata::{InvoiceMetadata, InvoiceMetadataStore}; -use crate::types::{ +use mdk::mdk_api::client::MdkApiClient; +use mdk::mdk_api::types::{CheckoutCustomer, CreateCheckoutRequest, RegisterInvoiceRequest}; + +use crate::daemon::api::error::AppError; +use crate::daemon::store::invoice_metadata::{InvoiceMetadata, InvoiceMetadataStore}; +use crate::daemon::types::{ CreateInvoiceRequest, CreateInvoiceResponse, IncomingPaymentResponse, ListOutgoingPaymentsRequest, ListPaymentsRequest, OutgoingPaymentResponse, }; @@ -78,7 +79,7 @@ pub async fn handle_create_invoice( description: req.description.clone(), invoice: Some(invoice_str.clone()), amount_sat, - created_at: crate::time::seconds_since_epoch(), + created_at: crate::daemon::time::seconds_since_epoch(), expires_at, }; @@ -221,7 +222,7 @@ pub async fn handle_list_incoming_payments( metadata_store: Arc, params: &ListPaymentsRequest, ) -> Result>, AppError> { - let now = crate::time::seconds_since_epoch(); + let now = crate::daemon::time::seconds_since_epoch(); let from = params.from.unwrap_or(0); let to = params.to.unwrap_or(now); let limit = params.limit.unwrap_or(20); @@ -265,7 +266,7 @@ pub async fn handle_list_outgoing_payments( metadata_store: Arc, params: &ListOutgoingPaymentsRequest, ) -> Result>, AppError> { - let now = crate::time::seconds_since_epoch(); + let now = crate::daemon::time::seconds_since_epoch(); let from = params.from.unwrap_or(0); let to = params.to.unwrap_or(now); let limit = params.limit.unwrap_or(20) as usize; @@ -410,7 +411,7 @@ fn enrich_metadata( None => (false, None, 0, None), }; - let now = crate::time::seconds_since_epoch(); + let now = crate::daemon::time::seconds_since_epoch(); let is_expired = !is_paid && metadata.expires_at > 0 && metadata.expires_at <= now; let fees = if is_paid { requested_sat.unwrap_or(0).saturating_sub(received_sat) diff --git a/src/api/mod.rs b/src/daemon/api/mod.rs similarity index 97% rename from src/api/mod.rs rename to src/daemon/api/mod.rs index 5ba17db..f32549b 100644 --- a/src/api/mod.rs +++ b/src/daemon/api/mod.rs @@ -22,10 +22,11 @@ use utoipa_scalar::{Scalar, Servable}; pub use auth::HttpAuth; -use crate::api::error::AppError; -use crate::mdk::client::MdkApiClient; -use crate::store::invoice_metadata::InvoiceMetadataStore; -use crate::types::{ +use mdk::mdk_api::client::MdkApiClient; + +use crate::daemon::api::error::AppError; +use crate::daemon::store::invoice_metadata::InvoiceMetadataStore; +use crate::daemon::types::{ ApiError, ChannelInfo, CloseChannelRequest, CreateInvoiceRequest, CreateInvoiceResponse, DecodeInvoiceRequest, DecodeInvoiceResponse, DecodeOfferRequest, DecodeOfferResponse, GetBalanceResponse, GetInfoResponse, IncomingPaymentResponse, ListOutgoingPaymentsRequest, @@ -107,7 +108,7 @@ pub fn router(state: AppState) -> Router { #[cfg(feature = "demo")] let router = { - const DEMO_HTML: &str = include_str!("../../wallet.html"); + const DEMO_HTML: &str = include_str!("../../../wallet.html"); router.route( "/", axum::routing::get(|| async { axum::response::Html(DEMO_HTML) }), diff --git a/src/api/onchain.rs b/src/daemon/api/onchain.rs similarity index 85% rename from src/api/onchain.rs rename to src/daemon/api/onchain.rs index 0cc0b1a..60edcb2 100644 --- a/src/api/onchain.rs +++ b/src/daemon/api/onchain.rs @@ -4,9 +4,9 @@ use ldk_node::bitcoin::{Address, FeeRate}; use ldk_node::Node; use log::error; -use crate::api::error::AppError; -use crate::store::invoice_metadata::{InvoiceMetadataStore, OutgoingSendRecord}; -use crate::types::SendToAddressRequest; +use crate::daemon::api::error::AppError; +use crate::daemon::store::invoice_metadata::{InvoiceMetadataStore, OutgoingSendRecord}; +use crate::daemon::types::SendToAddressRequest; pub async fn handle_send_to_address( node: Arc, @@ -41,7 +41,7 @@ pub async fn handle_send_to_address( address: req.address.clone(), amount_sat: req.amount_sat, fee_sat: None, - created_at: crate::time::seconds_since_epoch(), + created_at: crate::daemon::time::seconds_since_epoch(), }; if let Err(e) = metadata_store.insert_outgoing_send(&record) { error!("Failed to store outgoing send: {e}"); diff --git a/src/api/websocket.rs b/src/daemon/api/websocket.rs similarity index 99% rename from src/api/websocket.rs rename to src/daemon/api/websocket.rs index 4395b54..000705c 100644 --- a/src/api/websocket.rs +++ b/src/daemon/api/websocket.rs @@ -6,7 +6,7 @@ use axum::http::{HeaderMap, StatusCode}; use axum::response::Response; use tokio::sync::broadcast; -use crate::api::auth::HttpAuth; +use crate::daemon::api::auth::HttpAuth; #[derive(Clone)] pub struct WsState { diff --git a/src/config.rs b/src/daemon/config.rs similarity index 52% rename from src/config.rs rename to src/daemon/config.rs index 1e77b15..1043ab1 100644 --- a/src/config.rs +++ b/src/daemon/config.rs @@ -140,132 +140,6 @@ pub fn get_default_data_dir() -> Option { } } -pub struct LspInfra { - pub chain_source: ChainSource, - pub lsp_node_id: &'static str, - pub lsp_address: &'static str, - pub mdk_api_base_url: &'static str, - pub vss_url: &'static str, -} - -impl LspInfra { - pub fn for_network(network: Network) -> Option { - match network { - Network::Bitcoin => Some(LspInfra { - chain_source: ChainSource::Esplora("https://esplora.moneydevkit.com/api"), - lsp_node_id: "02a63339cc6b913b6330bd61b2f469af8785a6011a6305bb102298a8e76697473b", - lsp_address: "lsp.moneydevkit.com:9735", - mdk_api_base_url: "https://moneydevkit.com/rpc", - vss_url: "https://vss.moneydevkit.com/vss", - }), - Network::Signet => Some(LspInfra { - chain_source: ChainSource::Esplora("https://mutinynet.com/api"), - lsp_node_id: "03fd9a377576df94cc7e458471c43c400630655083dee89df66c6ad38d1b7acffd", - lsp_address: "lsp.staging.moneydevkit.com:9735", - mdk_api_base_url: "https://staging.moneydevkit.com/rpc", - vss_url: "https://vss.staging.moneydevkit.com/vss", - }), - _ => None, - } - } -} - -pub enum ChainSource { - Esplora(&'static str), - Bitcoind { - rpc_host: String, - rpc_port: u16, - rpc_user: String, - rpc_password: String, - }, -} - -pub enum NetworkInfra { - Production(LspInfra), - Regtest { - chain_source: ChainSource, - lsp_node_id: String, - lsp_address: String, - mdk_api_base_url: String, - vss_url: String, - }, -} - -impl NetworkInfra { - pub fn resolve(network: Network) -> io::Result { - match LspInfra::for_network(network) { - Some(infra) => Ok(NetworkInfra::Production(infra)), - None => { - let rpc_port_str = env_required("MDK_BITCOIND_RPC_PORT")?; - let rpc_port: u16 = rpc_port_str.parse().map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidInput, - format!("MDK_BITCOIND_RPC_PORT is not a valid port: {rpc_port_str}"), - ) - })?; - Ok(NetworkInfra::Regtest { - chain_source: ChainSource::Bitcoind { - rpc_host: env_required("MDK_BITCOIND_RPC_HOST")?, - rpc_port, - rpc_user: env_required("MDK_BITCOIND_RPC_USER")?, - rpc_password: env_required("MDK_BITCOIND_RPC_PASSWORD")?, - }, - lsp_node_id: env_required("MDK_LSP_NODE_ID")?, - lsp_address: env_required("MDK_LSP_ADDRESS")?, - mdk_api_base_url: env_required("MDK_API_BASE_URL")?, - vss_url: env_required("MDK_VSS_URL")?, - }) - } - } - } - - pub fn chain_source(&self) -> &ChainSource { - match self { - NetworkInfra::Production(lsp_infra) => &lsp_infra.chain_source, - NetworkInfra::Regtest { chain_source, .. } => chain_source, - } - } - - pub fn lsp_node_id(&self) -> &str { - match self { - NetworkInfra::Production(lsp_infra) => lsp_infra.lsp_node_id, - NetworkInfra::Regtest { lsp_node_id, .. } => lsp_node_id, - } - } - - pub fn lsp_address(&self) -> &str { - match self { - NetworkInfra::Production(lsp_infra) => lsp_infra.lsp_address, - NetworkInfra::Regtest { lsp_address, .. } => lsp_address, - } - } - - pub fn mdk_api_base_url(&self) -> &str { - match self { - NetworkInfra::Production(lsp_infra) => lsp_infra.mdk_api_base_url, - NetworkInfra::Regtest { - mdk_api_base_url, .. - } => mdk_api_base_url, - } - } - - pub fn vss_url(&self) -> &str { - match self { - NetworkInfra::Production(lsp_infra) => lsp_infra.vss_url, - NetworkInfra::Regtest { vss_url, .. } => vss_url, - } - } -} - -fn env_required(name: &str) -> io::Result { - std::env::var(name).map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidInput, - format!("{name} environment variable is required for regtest"), - ) - }) -} - fn missing(field: &str) -> io::Error { io::Error::new( io::ErrorKind::InvalidInput, diff --git a/src/event_loop.rs b/src/daemon/event_loop.rs similarity index 94% rename from src/event_loop.rs rename to src/daemon/event_loop.rs index f632880..79f232e 100644 --- a/src/event_loop.rs +++ b/src/daemon/event_loop.rs @@ -4,12 +4,13 @@ use ldk_node::{Event, Node}; use log::{error, info}; use tokio::sync::broadcast; -use crate::mdk::client::MdkApiClient; -use crate::mdk::types::{PaymentEntry, PaymentReceivedRequest}; -use crate::store::invoice_metadata::InvoiceMetadataStore; -use crate::time; -use crate::types::WebhookEvent; -use crate::webhook::dispatcher::spawn_webhook_delivery; +use mdk::mdk_api::client::MdkApiClient; +use mdk::mdk_api::types::{PaymentEntry, PaymentReceivedRequest}; + +use crate::daemon::store::invoice_metadata::InvoiceMetadataStore; +use crate::daemon::time; +use crate::daemon::types::WebhookEvent; +use crate::daemon::webhook::dispatcher::spawn_webhook_delivery; pub async fn run_event_loop( node: Arc, diff --git a/src/expiry.rs b/src/daemon/expiry.rs similarity index 89% rename from src/expiry.rs rename to src/daemon/expiry.rs index c3f637a..bf0131d 100644 --- a/src/expiry.rs +++ b/src/daemon/expiry.rs @@ -3,10 +3,10 @@ use std::time::Duration; use log::{error, info}; -use crate::store::invoice_metadata::InvoiceMetadataStore; -use crate::time; -use crate::types::WebhookEvent; -use crate::webhook::dispatcher::spawn_webhook_delivery; +use crate::daemon::store::invoice_metadata::InvoiceMetadataStore; +use crate::daemon::time; +use crate::daemon::types::WebhookEvent; +use crate::daemon::webhook::dispatcher::spawn_webhook_delivery; const POLL_INTERVAL: Duration = Duration::from_secs(30); diff --git a/src/logger.rs b/src/daemon/logger.rs similarity index 100% rename from src/logger.rs rename to src/daemon/logger.rs diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs new file mode 100644 index 0000000..6339737 --- /dev/null +++ b/src/daemon/mod.rs @@ -0,0 +1,10 @@ +pub mod api; +pub mod config; +pub mod event_loop; +pub mod expiry; +pub mod logger; +pub mod secret; +pub mod store; +pub mod time; +pub mod types; +pub mod webhook; diff --git a/src/secret.rs b/src/daemon/secret.rs similarity index 100% rename from src/secret.rs rename to src/daemon/secret.rs diff --git a/src/store/invoice_metadata.rs b/src/daemon/store/invoice_metadata.rs similarity index 100% rename from src/store/invoice_metadata.rs rename to src/daemon/store/invoice_metadata.rs diff --git a/src/store/mod.rs b/src/daemon/store/mod.rs similarity index 100% rename from src/store/mod.rs rename to src/daemon/store/mod.rs diff --git a/src/time.rs b/src/daemon/time.rs similarity index 100% rename from src/time.rs rename to src/daemon/time.rs diff --git a/src/types.rs b/src/daemon/types.rs similarity index 100% rename from src/types.rs rename to src/daemon/types.rs diff --git a/src/webhook/dispatcher.rs b/src/daemon/webhook/dispatcher.rs similarity index 98% rename from src/webhook/dispatcher.rs rename to src/daemon/webhook/dispatcher.rs index b7587ad..0bc777d 100644 --- a/src/webhook/dispatcher.rs +++ b/src/daemon/webhook/dispatcher.rs @@ -5,7 +5,7 @@ use hmac::{Hmac, Mac}; use log::{error, info}; use sha2::Sha256; -use crate::types::WebhookEvent; +use crate::daemon::types::WebhookEvent; type HmacSha256 = Hmac; diff --git a/src/webhook/mod.rs b/src/daemon/webhook/mod.rs similarity index 100% rename from src/webhook/mod.rs rename to src/daemon/webhook/mod.rs diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4b22b58 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +mod mdk; + +pub use mdk::config; +pub use mdk::mdk_api; diff --git a/src/main.rs b/src/main.rs index 5eb7690..71106e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,4 @@ -mod api; -mod config; -mod event_loop; -mod expiry; -mod logger; -mod mdk; -mod secret; -mod store; -mod time; -mod types; -mod webhook; +mod daemon; use std::collections::HashMap; use std::net::ToSocketAddrs; @@ -26,14 +16,15 @@ use ldk_node::config::Config as LdkNodeConfig; use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::Builder; use log::{error, info}; +use mdk::config::{ChainSource, NetworkInfra}; +use mdk::mdk_api::client::MdkApiClient; use reqwest::{Client, Proxy}; use tokio::signal::unix::SignalKind; use tokio::sync::broadcast; -use crate::api::{AppState, HttpAuth}; -use crate::config::{get_default_data_dir, load_config, ChainSource, NetworkInfra}; -use crate::mdk::client::MdkApiClient; -use crate::store::invoice_metadata::InvoiceMetadataStore; +use daemon::api::{AppState, HttpAuth}; +use daemon::config::{get_default_data_dir, load_config}; +use daemon::store::invoice_metadata::InvoiceMetadataStore; #[derive(Parser)] #[command(version, about = "mdkd - MDK daemon")] @@ -81,7 +72,7 @@ fn main() { // Resolve secrets: FD flags take precedence, env vars as fallback. let resolve = |name, fd| { - secret::try_resolve(name, fd).unwrap_or_else(|e| { + daemon::secret::try_resolve(name, fd).unwrap_or_else(|e| { eprintln!("{e}"); std::process::exit(1); }) @@ -124,7 +115,7 @@ fn main() { std::process::exit(1); } - logger::init(config_file.log_level); + daemon::logger::init(config_file.log_level); // Optional SOCKS5 proxy for all outbound traffic. let socks_proxy_url = args.socks_proxy; @@ -322,7 +313,7 @@ fn main() { event_tx: event_tx.clone(), }; - let app = api::router(app_state); + let app = daemon::api::router(app_state); let listener = match tokio::net::TcpListener::bind(bind_addr).await { Ok(l) => l, @@ -339,7 +330,7 @@ fn main() { let expiry_client = http_client.clone(); tokio::spawn(async move { - expiry::run_expiry_monitor(expiry_metadata, expiry_secret, expiry_client).await; + daemon::expiry::run_expiry_monitor(expiry_metadata, expiry_secret, expiry_client).await; }); let event_node = Arc::clone(&node); @@ -349,7 +340,7 @@ fn main() { let event_mdk_client = mdk_client.clone(); tokio::spawn(async move { - event_loop::run_event_loop( + daemon::event_loop::run_event_loop( event_node, event_metadata, event_secret, diff --git a/src/mdk/config.rs b/src/mdk/config.rs new file mode 100644 index 0000000..47565d4 --- /dev/null +++ b/src/mdk/config.rs @@ -0,0 +1,129 @@ +use std::io; + +use ldk_node::bitcoin::Network; + +pub struct LspInfra { + pub chain_source: ChainSource, + pub lsp_node_id: &'static str, + pub lsp_address: &'static str, + pub mdk_api_base_url: &'static str, + pub vss_url: &'static str, +} + +impl LspInfra { + pub fn for_network(network: Network) -> Option { + match network { + Network::Bitcoin => Some(LspInfra { + chain_source: ChainSource::Esplora("https://esplora.moneydevkit.com/api"), + lsp_node_id: "02a63339cc6b913b6330bd61b2f469af8785a6011a6305bb102298a8e76697473b", + lsp_address: "lsp.moneydevkit.com:9735", + mdk_api_base_url: "https://moneydevkit.com/rpc", + vss_url: "https://vss.moneydevkit.com/vss", + }), + Network::Signet => Some(LspInfra { + chain_source: ChainSource::Esplora("https://mutinynet.com/api"), + lsp_node_id: "03fd9a377576df94cc7e458471c43c400630655083dee89df66c6ad38d1b7acffd", + lsp_address: "lsp.staging.moneydevkit.com:9735", + mdk_api_base_url: "https://staging.moneydevkit.com/rpc", + vss_url: "https://vss.staging.moneydevkit.com/vss", + }), + _ => None, + } + } +} + +pub enum ChainSource { + Esplora(&'static str), + Bitcoind { + rpc_host: String, + rpc_port: u16, + rpc_user: String, + rpc_password: String, + }, +} + +pub enum NetworkInfra { + Production(LspInfra), + Regtest { + chain_source: ChainSource, + lsp_node_id: String, + lsp_address: String, + mdk_api_base_url: String, + vss_url: String, + }, +} + +impl NetworkInfra { + pub fn resolve(network: Network) -> io::Result { + match LspInfra::for_network(network) { + Some(infra) => Ok(NetworkInfra::Production(infra)), + None => { + let rpc_port_str = env_required("MDK_BITCOIND_RPC_PORT")?; + let rpc_port: u16 = rpc_port_str.parse().map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("MDK_BITCOIND_RPC_PORT is not a valid port: {rpc_port_str}"), + ) + })?; + Ok(NetworkInfra::Regtest { + chain_source: ChainSource::Bitcoind { + rpc_host: env_required("MDK_BITCOIND_RPC_HOST")?, + rpc_port, + rpc_user: env_required("MDK_BITCOIND_RPC_USER")?, + rpc_password: env_required("MDK_BITCOIND_RPC_PASSWORD")?, + }, + lsp_node_id: env_required("MDK_LSP_NODE_ID")?, + lsp_address: env_required("MDK_LSP_ADDRESS")?, + mdk_api_base_url: env_required("MDK_API_BASE_URL")?, + vss_url: env_required("MDK_VSS_URL")?, + }) + } + } + } + + pub fn chain_source(&self) -> &ChainSource { + match self { + NetworkInfra::Production(lsp_infra) => &lsp_infra.chain_source, + NetworkInfra::Regtest { chain_source, .. } => chain_source, + } + } + + pub fn lsp_node_id(&self) -> &str { + match self { + NetworkInfra::Production(lsp_infra) => lsp_infra.lsp_node_id, + NetworkInfra::Regtest { lsp_node_id, .. } => lsp_node_id, + } + } + + pub fn lsp_address(&self) -> &str { + match self { + NetworkInfra::Production(lsp_infra) => lsp_infra.lsp_address, + NetworkInfra::Regtest { lsp_address, .. } => lsp_address, + } + } + + pub fn mdk_api_base_url(&self) -> &str { + match self { + NetworkInfra::Production(lsp_infra) => lsp_infra.mdk_api_base_url, + NetworkInfra::Regtest { + mdk_api_base_url, .. + } => mdk_api_base_url, + } + } + + pub fn vss_url(&self) -> &str { + match self { + NetworkInfra::Production(lsp_infra) => lsp_infra.vss_url, + NetworkInfra::Regtest { vss_url, .. } => vss_url, + } + } +} + +fn env_required(name: &str) -> io::Result { + std::env::var(name).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("{name} environment variable is required for regtest"), + ) + }) +} diff --git a/src/mdk/client.rs b/src/mdk/mdk_api/client.rs similarity index 100% rename from src/mdk/client.rs rename to src/mdk/mdk_api/client.rs diff --git a/src/mdk/mdk_api/mod.rs b/src/mdk/mdk_api/mod.rs new file mode 100644 index 0000000..9251ae9 --- /dev/null +++ b/src/mdk/mdk_api/mod.rs @@ -0,0 +1,2 @@ +pub mod client; +pub mod types; diff --git a/src/mdk/types.rs b/src/mdk/mdk_api/types.rs similarity index 100% rename from src/mdk/types.rs rename to src/mdk/mdk_api/types.rs diff --git a/src/mdk/mod.rs b/src/mdk/mod.rs index 9251ae9..aa2232d 100644 --- a/src/mdk/mod.rs +++ b/src/mdk/mod.rs @@ -1,2 +1,2 @@ -pub mod client; -pub mod types; +pub mod config; +pub mod mdk_api; From 17a6e5d7d3637bf91c7e588d468cc2ad27998c82 Mon Sep 17 00:00:00 2001 From: amackillop Date: Mon, 6 Apr 2026 12:13:57 -0700 Subject: [PATCH 2/6] Add MdkError, domain types, and node builder Three new library modules, no daemon changes. mdk::error has MdkError with From impls for NodeError, BuildError, and MdkApiError so callers deal with one error type. mdk::types has the domain types MdkClient will expose: CreateCheckoutParams, CheckoutResult, Balance, Channel, NodeInfo, MdkEvent, PaymentResult. mdk::node pulls the LDK builder sequence into build_node(NodeConfig) -> Result, MdkError>. NodeConfig takes network and storage_dir_path directly since the caller already has both. Also moves derive_vss_identifier here. Collapses LspInfra + NetworkInfra into one flat struct with owned Strings. The split only existed so hardcoded networks could use &'static str while regtest used String. Not worth five accessor methods. One struct, one constructor, direct field access. Testnet/Testnet4 now return an error instead of panicking. --- src/lib.rs | 3 ++ src/main.rs | 16 +++--- src/mdk/config.rs | 111 +++++++++++----------------------------- src/mdk/error.rs | 64 +++++++++++++++++++++++ src/mdk/mod.rs | 3 ++ src/mdk/node.rs | 127 ++++++++++++++++++++++++++++++++++++++++++++++ src/mdk/types.rs | 112 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 347 insertions(+), 89 deletions(-) create mode 100644 src/mdk/error.rs create mode 100644 src/mdk/node.rs create mode 100644 src/mdk/types.rs diff --git a/src/lib.rs b/src/lib.rs index 4b22b58..5e189a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,7 @@ mod mdk; pub use mdk::config; +pub use mdk::error; pub use mdk::mdk_api; +pub use mdk::node; +pub use mdk::types; diff --git a/src/main.rs b/src/main.rs index 71106e0..adec3e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -163,9 +163,9 @@ fn main() { } } - match infra.chain_source() { + match &infra.chain_source { ChainSource::Esplora(server_url) => { - builder.set_chain_source_esplora(server_url.to_string(), None); + builder.set_chain_source_esplora(server_url.clone(), None); } ChainSource::Bitcoind { rpc_host, @@ -186,16 +186,16 @@ fn main() { builder.set_pathfinding_scores_source(url); } - let lsp_pubkey = PublicKey::from_str(infra.lsp_node_id()).unwrap_or_else(|e| { + let lsp_pubkey = PublicKey::from_str(&infra.lsp_node_id).unwrap_or_else(|e| { error!("Bad lsp_node_id: {e}"); std::process::exit(1); }); - let lsp_addr = SocketAddress::from_str(infra.lsp_address()).unwrap_or_else(|e| { + let lsp_addr = SocketAddress::from_str(&infra.lsp_address).unwrap_or_else(|e| { error!("Bad lsp_address: {e}"); std::process::exit(1); }); builder.set_liquidity_source_lsps4(lsp_pubkey, lsp_addr); - info!("LSPS4 liquidity source: {}", infra.lsp_node_id()); + info!("LSPS4 liquidity source: {}", infra.lsp_node_id); let runtime = match tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -219,12 +219,12 @@ fn main() { let store_id = derive_vss_identifier(&mnemonic); info!( "VSS store: {} (store_id={}...)", - infra.vss_url(), + infra.vss_url, &store_id[..16] ); let node = match builder.build_with_vss_store_and_fixed_headers( - infra.vss_url().to_string(), + infra.vss_url.clone(), store_id, HashMap::new(), ) { @@ -259,7 +259,7 @@ fn main() { }) }; - let base_url = infra.mdk_api_base_url().to_string(); + let base_url = infra.mdk_api_base_url; info!("MDK platform integration enabled ({})", base_url); let mdk_client = Arc::new(MdkApiClient::new( http_client.clone(), diff --git a/src/mdk/config.rs b/src/mdk/config.rs index 47565d4..0e5467b 100644 --- a/src/mdk/config.rs +++ b/src/mdk/config.rs @@ -2,38 +2,8 @@ use std::io; use ldk_node::bitcoin::Network; -pub struct LspInfra { - pub chain_source: ChainSource, - pub lsp_node_id: &'static str, - pub lsp_address: &'static str, - pub mdk_api_base_url: &'static str, - pub vss_url: &'static str, -} - -impl LspInfra { - pub fn for_network(network: Network) -> Option { - match network { - Network::Bitcoin => Some(LspInfra { - chain_source: ChainSource::Esplora("https://esplora.moneydevkit.com/api"), - lsp_node_id: "02a63339cc6b913b6330bd61b2f469af8785a6011a6305bb102298a8e76697473b", - lsp_address: "lsp.moneydevkit.com:9735", - mdk_api_base_url: "https://moneydevkit.com/rpc", - vss_url: "https://vss.moneydevkit.com/vss", - }), - Network::Signet => Some(LspInfra { - chain_source: ChainSource::Esplora("https://mutinynet.com/api"), - lsp_node_id: "03fd9a377576df94cc7e458471c43c400630655083dee89df66c6ad38d1b7acffd", - lsp_address: "lsp.staging.moneydevkit.com:9735", - mdk_api_base_url: "https://staging.moneydevkit.com/rpc", - vss_url: "https://vss.staging.moneydevkit.com/vss", - }), - _ => None, - } - } -} - pub enum ChainSource { - Esplora(&'static str), + Esplora(String), Bitcoind { rpc_host: String, rpc_port: u16, @@ -42,22 +12,38 @@ pub enum ChainSource { }, } -pub enum NetworkInfra { - Production(LspInfra), - Regtest { - chain_source: ChainSource, - lsp_node_id: String, - lsp_address: String, - mdk_api_base_url: String, - vss_url: String, - }, +pub struct NetworkInfra { + pub chain_source: ChainSource, + pub lsp_node_id: String, + pub lsp_address: String, + pub mdk_api_base_url: String, + pub vss_url: String, } impl NetworkInfra { pub fn resolve(network: Network) -> io::Result { - match LspInfra::for_network(network) { - Some(infra) => Ok(NetworkInfra::Production(infra)), - None => { + match network { + Network::Bitcoin => Ok(Self { + chain_source: ChainSource::Esplora("https://esplora.moneydevkit.com/api".into()), + lsp_node_id: "02a63339cc6b913b6330bd61b2f469af8785a6011a6305bb102298a8e76697473b" + .into(), + lsp_address: "lsp.moneydevkit.com:9735".into(), + mdk_api_base_url: "https://moneydevkit.com/rpc".into(), + vss_url: "https://vss.moneydevkit.com/vss".into(), + }), + Network::Signet => Ok(Self { + chain_source: ChainSource::Esplora("https://mutinynet.com/api".into()), + lsp_node_id: "03fd9a377576df94cc7e458471c43c400630655083dee89df66c6ad38d1b7acffd" + .into(), + lsp_address: "lsp.staging.moneydevkit.com:9735".into(), + mdk_api_base_url: "https://staging.moneydevkit.com/rpc".into(), + vss_url: "https://vss.staging.moneydevkit.com/vss".into(), + }), + Network::Testnet | Network::Testnet4 => Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("unsupported network: {network}"), + )), + _ => { let rpc_port_str = env_required("MDK_BITCOIND_RPC_PORT")?; let rpc_port: u16 = rpc_port_str.parse().map_err(|_| { io::Error::new( @@ -65,7 +51,7 @@ impl NetworkInfra { format!("MDK_BITCOIND_RPC_PORT is not a valid port: {rpc_port_str}"), ) })?; - Ok(NetworkInfra::Regtest { + Ok(Self { chain_source: ChainSource::Bitcoind { rpc_host: env_required("MDK_BITCOIND_RPC_HOST")?, rpc_port, @@ -80,43 +66,6 @@ impl NetworkInfra { } } } - - pub fn chain_source(&self) -> &ChainSource { - match self { - NetworkInfra::Production(lsp_infra) => &lsp_infra.chain_source, - NetworkInfra::Regtest { chain_source, .. } => chain_source, - } - } - - pub fn lsp_node_id(&self) -> &str { - match self { - NetworkInfra::Production(lsp_infra) => lsp_infra.lsp_node_id, - NetworkInfra::Regtest { lsp_node_id, .. } => lsp_node_id, - } - } - - pub fn lsp_address(&self) -> &str { - match self { - NetworkInfra::Production(lsp_infra) => lsp_infra.lsp_address, - NetworkInfra::Regtest { lsp_address, .. } => lsp_address, - } - } - - pub fn mdk_api_base_url(&self) -> &str { - match self { - NetworkInfra::Production(lsp_infra) => lsp_infra.mdk_api_base_url, - NetworkInfra::Regtest { - mdk_api_base_url, .. - } => mdk_api_base_url, - } - } - - pub fn vss_url(&self) -> &str { - match self { - NetworkInfra::Production(lsp_infra) => lsp_infra.vss_url, - NetworkInfra::Regtest { vss_url, .. } => vss_url, - } - } } fn env_required(name: &str) -> io::Result { diff --git a/src/mdk/error.rs b/src/mdk/error.rs new file mode 100644 index 0000000..ae72fb9 --- /dev/null +++ b/src/mdk/error.rs @@ -0,0 +1,64 @@ +use std::fmt; + +use crate::mdk::mdk_api::types::MdkApiError; + +#[derive(Debug)] +pub enum MdkError { + InvalidInput(String), + Node(String), + Platform { + code: String, + message: String, + status: u16, + }, + Network(String), + NotFound(String), +} + +impl fmt::Display for MdkError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MdkError::InvalidInput(msg) => write!(f, "invalid input: {msg}"), + MdkError::Node(msg) => write!(f, "node error: {msg}"), + MdkError::Platform { + code, + message, + status, + } => write!(f, "platform API error ({status}): [{code}] {message}"), + MdkError::Network(msg) => write!(f, "network error: {msg}"), + MdkError::NotFound(msg) => write!(f, "not found: {msg}"), + } + } +} + +impl std::error::Error for MdkError {} + +impl From for MdkError { + fn from(e: ldk_node::NodeError) -> Self { + MdkError::Node(e.to_string()) + } +} + +impl From for MdkError { + fn from(e: ldk_node::BuildError) -> Self { + MdkError::Node(e.to_string()) + } +} + +impl From for MdkError { + fn from(e: MdkApiError) -> Self { + match e { + MdkApiError::Network(inner) => MdkError::Network(inner.to_string()), + MdkApiError::Api { + code, + message, + status, + } => MdkError::Platform { + code, + message, + status, + }, + MdkApiError::Deserialize(msg) => MdkError::Network(msg), + } + } +} diff --git a/src/mdk/mod.rs b/src/mdk/mod.rs index aa2232d..2d45805 100644 --- a/src/mdk/mod.rs +++ b/src/mdk/mod.rs @@ -1,2 +1,5 @@ pub mod config; +pub mod error; pub mod mdk_api; +pub mod node; +pub mod types; diff --git a/src/mdk/node.rs b/src/mdk/node.rs new file mode 100644 index 0000000..26603f5 --- /dev/null +++ b/src/mdk/node.rs @@ -0,0 +1,127 @@ +use std::collections::HashMap; +use std::net::ToSocketAddrs; +use std::str::FromStr; +use std::sync::Arc; + +use ldk_node::bip39::Mnemonic; +use ldk_node::bitcoin::hashes::sha256; +use ldk_node::bitcoin::hashes::Hash; +use ldk_node::bitcoin::secp256k1::PublicKey; +use ldk_node::bitcoin::Network; +use ldk_node::config::Config as LdkNodeConfig; +use ldk_node::lightning::ln::msgs::SocketAddress; +use ldk_node::{Builder, Node}; +use log::info; + +use crate::mdk::config::{ChainSource, NetworkInfra}; +use crate::mdk::error::MdkError; + +pub struct NodeConfig { + pub network: Network, + pub storage_dir_path: String, + pub listening_addresses: Option>, + pub announcement_addresses: Option>, + pub alias: Option, + pub socks_proxy: Option, + pub pathfinding_scores_source_url: Option, + pub mnemonic: String, + pub infra: NetworkInfra, + pub runtime: tokio::runtime::Handle, +} + +pub fn build_node(config: NodeConfig) -> Result, MdkError> { + let ldk_config = LdkNodeConfig { + storage_dir_path: config.storage_dir_path.clone(), + listening_addresses: config.listening_addresses, + announcement_addresses: config.announcement_addresses, + network: config.network, + ..Default::default() + }; + + let mut builder = Builder::from_config(ldk_config); + builder.set_log_facade_logger(); + + if let Some(ref proxy_url) = config.socks_proxy { + let addr = resolve_socks_proxy(proxy_url)?; + builder.set_socks5_proxy(addr); + info!("SOCKS5 proxy enabled: {}", proxy_url); + } + + if let Some(alias) = config.alias { + builder + .set_node_alias(alias.to_string()) + .map_err(|e| MdkError::InvalidInput(format!("invalid node alias: {e}")))?; + } + + let infra = config.infra; + + match &infra.chain_source { + ChainSource::Esplora(server_url) => { + builder.set_chain_source_esplora(server_url.clone(), None); + } + ChainSource::Bitcoind { + rpc_host, + rpc_port, + rpc_user, + rpc_password, + } => { + builder.set_chain_source_bitcoind_rpc( + rpc_host.clone(), + *rpc_port, + rpc_user.clone(), + rpc_password.clone(), + ); + } + } + + if let Some(url) = config.pathfinding_scores_source_url { + builder.set_pathfinding_scores_source(url); + } + + let lsp_pubkey = PublicKey::from_str(&infra.lsp_node_id) + .map_err(|e| MdkError::InvalidInput(format!("bad lsp_node_id: {e}")))?; + let lsp_addr = SocketAddress::from_str(&infra.lsp_address) + .map_err(|e| MdkError::InvalidInput(format!("bad lsp_address: {e}")))?; + builder.set_liquidity_source_lsps4(lsp_pubkey, lsp_addr); + info!("LSPS4 liquidity source: {}", infra.lsp_node_id); + + builder.set_runtime(config.runtime); + + let mnemonic = Mnemonic::parse(&config.mnemonic) + .map_err(|e| MdkError::InvalidInput(format!("invalid mnemonic: {e}")))?; + builder.set_entropy_bip39_mnemonic(mnemonic.clone(), None); + + let store_id = derive_vss_identifier(&mnemonic); + info!( + "VSS store: {} (store_id={}...)", + infra.vss_url, + &store_id[..16] + ); + + let node = + builder.build_with_vss_store_and_fixed_headers(infra.vss_url, store_id, HashMap::new())?; + + Ok(Arc::new(node)) +} + +fn resolve_socks_proxy(raw: &str) -> Result { + let host_port = raw + .strip_prefix("socks5://") + .or_else(|| raw.strip_prefix("socks5h://")) + .ok_or_else(|| { + MdkError::InvalidInput( + "SOCKS5 proxy url must start with socks5:// or socks5h://".into(), + ) + })?; + + host_port + .to_socket_addrs() + .ok() + .and_then(|mut addrs| addrs.next()) + .ok_or_else(|| MdkError::InvalidInput(format!("cannot resolve SOCKS5 proxy: {host_port}"))) +} + +pub fn derive_vss_identifier(mnemonic: &Mnemonic) -> String { + let phrase = mnemonic.to_string(); + sha256::Hash::hash(phrase.as_bytes()).to_string() +} diff --git a/src/mdk/types.rs b/src/mdk/types.rs new file mode 100644 index 0000000..01b509b --- /dev/null +++ b/src/mdk/types.rs @@ -0,0 +1,112 @@ +use ldk_node::ChannelDetails; + +/// Parameters for creating a checkout (invoice + platform registration). +pub struct CreateCheckoutParams { + pub amount_sat: Option, + pub description: InvoiceDescription, + pub expiry_seconds: Option, + pub product: Option, + pub currency: Option, + pub success_url: Option, + pub metadata: Option, + pub customer: Option, +} + +pub enum InvoiceDescription { + Direct(String), + Hash([u8; 32]), +} + +pub struct Customer { + pub name: Option, + pub email: Option, + pub external_id: Option, +} + +pub struct CheckoutResult { + pub checkout_id: String, + pub invoice: String, + pub payment_hash: String, + pub amount_sat: Option, + pub expires_at: Option, +} + +pub struct Balance { + pub lightning_sats: u64, + pub onchain_sats: u64, +} + +pub struct Channel { + pub state: ChannelState, + pub channel_id: String, + pub balance_sats: u64, + pub inbound_liquidity_sats: u64, + pub capacity_sats: u64, + pub funding_tx_id: Option, +} + +pub enum ChannelState { + Online, + Offline, + Opening, +} + +impl From<&ChannelDetails> for Channel { + fn from(ch: &ChannelDetails) -> Self { + let state = match (ch.is_channel_ready, ch.is_usable) { + (true, true) => ChannelState::Online, + (true, false) => ChannelState::Offline, + (false, _) => ChannelState::Opening, + }; + + Self { + state, + channel_id: ch.channel_id.to_string(), + balance_sats: ch.outbound_capacity_msat / 1000, + inbound_liquidity_sats: ch.inbound_capacity_msat / 1000, + capacity_sats: ch.channel_value_sats, + funding_tx_id: ch.funding_txo.map(|txo| txo.txid.to_string()), + } + } +} + +pub struct NodeInfo { + pub node_id: String, + pub network: String, + pub block_height: u32, + pub channels: Vec, +} + +#[derive(Debug, Clone)] +pub enum MdkEvent { + PaymentReceived { + payment_hash: String, + amount_sats: u64, + }, + PaymentSuccessful { + payment_id: String, + payment_hash: Option, + fee_paid_sats: Option, + }, + PaymentFailed { + payment_id: String, + reason: Option, + }, + ChannelPending { + channel_id: String, + counterparty_node_id: String, + }, + ChannelReady { + channel_id: String, + counterparty_node_id: String, + }, + PaymentForwarded { + fee_earned_sats: Option, + }, +} + +pub struct PaymentResult { + pub payment_id: String, + pub payment_hash: Option, + pub fee_paid_sats: Option, +} From 0ee57e84790cf62d7e23d6db052aadaed492a4a8 Mon Sep 17 00:00:00 2001 From: amackillop Date: Mon, 6 Apr 2026 12:29:44 -0700 Subject: [PATCH 3/6] Add MdkClient facade with create_checkout MdkClient wraps Arc + Arc and owns the checkout flow that was spread across daemon/api/invoices.rs. create_checkout does the full sequence: create a platform checkout, mint a JIT invoice via LSPS4, register the invoice back with the platform, return a CheckoutResult. The daemon still uses its old code path. Rewiring happens in a later commit. --- src/lib.rs | 1 + src/mdk/client.rs | 141 ++++++++++++++++++++++++++++++++++++++++++++++ src/mdk/mod.rs | 1 + 3 files changed, 143 insertions(+) create mode 100644 src/mdk/client.rs diff --git a/src/lib.rs b/src/lib.rs index 5e189a4..d9ae4aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ mod mdk; +pub use mdk::client; pub use mdk::config; pub use mdk::error; pub use mdk::mdk_api; diff --git a/src/mdk/client.rs b/src/mdk/client.rs new file mode 100644 index 0000000..708c442 --- /dev/null +++ b/src/mdk/client.rs @@ -0,0 +1,141 @@ +use std::sync::Arc; + +use chrono::{DateTime, SecondsFormat}; +use ldk_node::bitcoin::hashes::sha256; +use ldk_node::bitcoin::hashes::Hash as _; +use ldk_node::lightning_invoice::{Bolt11InvoiceDescription, Description, Sha256}; +use ldk_node::Node; +use log::{error, info}; + +use crate::mdk::error::MdkError; +use crate::mdk::mdk_api::client::MdkApiClient; +use crate::mdk::mdk_api::types::{CheckoutCustomer, CreateCheckoutRequest, RegisterInvoiceRequest}; +use crate::mdk::types::{CheckoutResult, CreateCheckoutParams, InvoiceDescription}; + +const DEFAULT_EXPIRY_SECS: u32 = 3600; +const MAX_DESCRIPTION_LEN: usize = 128; + +pub struct MdkClient { + node: Arc, + api: Arc, +} + +impl MdkClient { + pub fn new(node: Arc, api: Arc) -> Self { + Self { node, api } + } + + pub fn node(&self) -> &Node { + &self.node + } + + pub async fn create_checkout( + &self, + params: CreateCheckoutParams, + ) -> Result { + let description = to_bolt11_description(¶ms.description)?; + let expiry_secs = params.expiry_seconds.unwrap_or(DEFAULT_EXPIRY_SECS); + + let customer = params.customer.map(|c| CheckoutCustomer { + name: c.name, + email: c.email, + external_id: c.external_id, + }); + + let checkout_req = CreateCheckoutRequest { + node_id: self.node.node_id().to_string(), + amount: params.amount_sat, + currency: params.currency.or_else(|| Some("SAT".into())), + products: params.product.map(|p| vec![p]), + success_url: params.success_url, + metadata: params.metadata, + customer, + }; + + let checkout = self.api.create_checkout(&checkout_req).await.map_err(|e| { + error!("MDK checkout/create failed: {e}"); + MdkError::from(e) + })?; + + info!( + "Created checkout {} (status: {})", + checkout.id, checkout.status + ); + + let amount_msat = match checkout.invoice_amount_sats { + Some(sats) => Some(sats * 1000), + None => params.amount_sat.map(|s| s * 1000), + }; + + let invoice = self + .node + .bolt11_payment() + .receive_via_lsps4_jit_channel(amount_msat, &description, expiry_secs) + .map_err(|e| MdkError::Node(format!("failed to create JIT invoice: {e}")))?; + + let scid = extract_scid(&invoice); + let payment_hash = invoice.payment_hash().to_string(); + let expires_at = invoice.expires_at().map(|d| d.as_secs()); + let expires_at_iso = expires_at + .and_then(|secs| { + DateTime::from_timestamp(secs as i64, 0) + .map(|dt| dt.to_rfc3339_opts(SecondsFormat::Secs, true)) + }) + .unwrap_or_default(); + + let register_req = RegisterInvoiceRequest { + node_id: self.node.node_id().to_string(), + scid, + checkout_id: checkout.id.clone(), + invoice: invoice.to_string(), + payment_hash: payment_hash.clone(), + invoice_expires_at: expires_at_iso, + }; + + self.api + .register_invoice(®ister_req) + .await + .map_err(|e| { + error!("MDK checkout/registerInvoice failed: {e}"); + MdkError::from(e) + })?; + + let amount_sat = invoice.amount_milli_satoshis().map(|m| m / 1000); + + Ok(CheckoutResult { + checkout_id: checkout.id, + invoice: invoice.to_string(), + payment_hash, + amount_sat, + expires_at, + }) + } +} + +fn to_bolt11_description(desc: &InvoiceDescription) -> Result { + match desc { + InvoiceDescription::Direct(text) => { + if text.len() > MAX_DESCRIPTION_LEN { + return Err(MdkError::InvalidInput(format!( + "description too long (max {MAX_DESCRIPTION_LEN} characters)" + ))); + } + let d = Description::new(text.clone()) + .map_err(|e| MdkError::InvalidInput(format!("invalid description: {e}")))?; + Ok(Bolt11InvoiceDescription::Direct(d)) + } + InvoiceDescription::Hash(bytes) => Ok(Bolt11InvoiceDescription::Hash(Sha256( + sha256::Hash::from_byte_array(*bytes), + ))), + } +} + +fn extract_scid(invoice: &ldk_node::lightning_invoice::Bolt11Invoice) -> String { + invoice + .route_hints() + .iter() + .flat_map(|hint| &hint.0) + .next() + .map(|hop| hop.short_channel_id.to_string()) + .unwrap_or_default() +} diff --git a/src/mdk/mod.rs b/src/mdk/mod.rs index 2d45805..a8981b8 100644 --- a/src/mdk/mod.rs +++ b/src/mdk/mod.rs @@ -1,3 +1,4 @@ +pub mod client; pub mod config; pub mod error; pub mod mdk_api; From 93626d382cab224155c259b883ed42d53dfe6ae8 Mon Sep 17 00:00:00 2001 From: amackillop Date: Mon, 6 Apr 2026 13:05:52 -0700 Subject: [PATCH 4/6] Add event loop and binding-friendly event API MdkClient now owns its event loop internally. start() spawns it, stop() cancels via CancellationToken, and run_event_loop is private. Callers no longer manage the task themselves. Two event delivery mechanisms: - EventHandler callback (Arc) passed at construction. Fires before the broadcast send. This is what language bindings (NAPI, etc.) will use since tokio channels don't cross FFI boundaries. - broadcast::Receiver via subscribe() for Rust consumers who prefer async channels. The previous design required callers to spawn the loop and subscribe separately, which is a footgun for bindings and has event ordering issues (subscribe after loop start = missed events). The callback runs inside the loop so it sees every event from the start. Logs use payment_hash for traceability since hashes are what appear in invoices and platform records. --- Cargo.lock | 14 ++++ Cargo.toml | 1 + src/mdk/client.rs | 192 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 200 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47b8126..c777733 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1717,6 +1717,7 @@ dependencies = [ "tempfile", "tokio", "tokio-tungstenite 0.26.2", + "tokio-util", "toml", "tower", "utoipa", @@ -2698,6 +2699,19 @@ dependencies = [ "tungstenite 0.28.0", ] +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" diff --git a/Cargo.toml b/Cargo.toml index bcf5bd7..daac812 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ utoipa = { version = "5.4", features = ["axum_extras"] } utoipa-axum = "0.2" utoipa-scalar = { version = "0.3", features = ["axum"] } tokio = { version = "1", features = ["full"] } +tokio-util = "0.7" serde = { version = "1", features = ["derive"] } serde_json = "1" reqwest = { version = "0.12", features = ["json", "rustls-tls", "socks"], default-features = false } diff --git a/src/mdk/client.rs b/src/mdk/client.rs index 708c442..bc50d44 100644 --- a/src/mdk/client.rs +++ b/src/mdk/client.rs @@ -3,32 +3,203 @@ use std::sync::Arc; use chrono::{DateTime, SecondsFormat}; use ldk_node::bitcoin::hashes::sha256; use ldk_node::bitcoin::hashes::Hash as _; -use ldk_node::lightning_invoice::{Bolt11InvoiceDescription, Description, Sha256}; -use ldk_node::Node; +use ldk_node::lightning::ln::channelmanager::PaymentId; +use ldk_node::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description, Sha256}; +use ldk_node::{Event, Node}; use log::{error, info}; +use tokio::sync::broadcast; +use tokio_util::sync::CancellationToken; use crate::mdk::error::MdkError; use crate::mdk::mdk_api::client::MdkApiClient; -use crate::mdk::mdk_api::types::{CheckoutCustomer, CreateCheckoutRequest, RegisterInvoiceRequest}; -use crate::mdk::types::{CheckoutResult, CreateCheckoutParams, InvoiceDescription}; +use crate::mdk::mdk_api::types::{ + CheckoutCustomer, CreateCheckoutRequest, PaymentEntry, PaymentReceivedRequest, + RegisterInvoiceRequest, +}; +use crate::mdk::types::{CheckoutResult, CreateCheckoutParams, InvoiceDescription, MdkEvent}; const DEFAULT_EXPIRY_SECS: u32 = 3600; const MAX_DESCRIPTION_LEN: usize = 128; +/// Callback invoked for each translated MdkEvent. +/// Fires before the broadcast channel send, so handlers see events +/// even when no broadcast subscriber exists. +pub type EventHandler = Arc; + pub struct MdkClient { node: Arc, api: Arc, + event_tx: broadcast::Sender, + event_handler: Option, + shutdown: CancellationToken, } impl MdkClient { - pub fn new(node: Arc, api: Arc) -> Self { - Self { node, api } + pub fn new( + node: Arc, + api: Arc, + event_handler: Option, + ) -> Self { + let (event_tx, _) = broadcast::channel(256); + Self { + node, + api, + event_tx, + event_handler, + shutdown: CancellationToken::new(), + } + } + + /// Spawn the internal event loop. Call once after construction. + /// The loop translates LDK events into MdkEvents, notifies the + /// platform on payment receipt, invokes the event handler callback, + /// and broadcasts to all subscribers. + pub fn start(self: &Arc) { + let this = Arc::clone(self); + tokio::spawn(async move { + this.run_event_loop().await; + }); + } + + /// Cancel the event loop. Idempotent. + pub fn stop(&self) { + self.shutdown.cancel(); } pub fn node(&self) -> &Node { &self.node } + pub fn subscribe(&self) -> broadcast::Receiver { + self.event_tx.subscribe() + } + + async fn run_event_loop(&self) { + loop { + tokio::select! { + _ = self.shutdown.cancelled() => break, + event = self.node.next_event_async() => { + let mdk_event = self.handle_ldk_event(&event).await; + + if let Err(e) = self.node.event_handled() { + error!("Failed to mark event as handled: {e}"); + } + + if let Some(ev) = mdk_event { + if let Some(handler) = &self.event_handler { + handler(ev.clone()); + } + let _ = self.event_tx.send(ev); + } + } + } + } + info!("Event loop stopped"); + } + + async fn handle_ldk_event(&self, event: &Event) -> Option { + match event { + Event::PaymentReceived { + payment_hash, + amount_msat, + .. + } => { + let hash = payment_hash.to_string(); + let amount_sats = amount_msat / 1000; + info!("PAYMENT_RECEIVED: hash {hash}, amount_sats {amount_sats}"); + + self.notify_payment_received(&hash, amount_sats).await; + + Some(MdkEvent::PaymentReceived { + payment_hash: hash, + amount_sats, + }) + } + Event::PaymentSuccessful { + payment_id, + payment_hash, + fee_paid_msat, + .. + } => { + info!("PAYMENT_SUCCESSFUL: hash {payment_hash}"); + Some(MdkEvent::PaymentSuccessful { + payment_id: format_payment_id(payment_id), + payment_hash: Some(payment_hash.to_string()), + fee_paid_sats: fee_paid_msat.map(|m| m / 1000), + }) + } + Event::PaymentFailed { + payment_id, + payment_hash, + reason, + .. + } => { + info!("PAYMENT_FAILED: hash {payment_hash:?}, reason: {reason:?}"); + Some(MdkEvent::PaymentFailed { + payment_id: format_payment_id(payment_id), + reason: reason.map(|r| format!("{r:?}")), + }) + } + Event::ChannelPending { + channel_id, + counterparty_node_id, + .. + } => { + info!("CHANNEL_PENDING: {channel_id} from {counterparty_node_id}"); + Some(MdkEvent::ChannelPending { + channel_id: channel_id.to_string(), + counterparty_node_id: counterparty_node_id.to_string(), + }) + } + Event::ChannelReady { + channel_id, + counterparty_node_id, + .. + } => { + info!("CHANNEL_READY: {channel_id} from {counterparty_node_id:?}"); + Some(MdkEvent::ChannelReady { + channel_id: channel_id.to_string(), + counterparty_node_id: counterparty_node_id + .map(|pk| pk.to_string()) + .unwrap_or_default(), + }) + } + Event::PaymentForwarded { + total_fee_earned_msat, + outbound_amount_forwarded_msat, + prev_channel_id, + next_channel_id, + .. + } => { + info!( + "PAYMENT_FORWARDED: outbound_msat {}, fee_msat {}, in: {prev_channel_id}, out: {next_channel_id}", + outbound_amount_forwarded_msat.unwrap_or(0), + total_fee_earned_msat.unwrap_or(0), + ); + Some(MdkEvent::PaymentForwarded { + fee_earned_sats: total_fee_earned_msat.map(|m| m / 1000), + }) + } + _ => None, + } + } + + async fn notify_payment_received(&self, payment_hash: &str, amount_sats: u64) { + let req = PaymentReceivedRequest { + payments: vec![PaymentEntry { + payment_hash: payment_hash.to_string(), + amount_sats, + sandbox: false, + }], + }; + match self.api.payment_received(&req).await { + Ok(_) => { + info!("Notified moneydevkit.com of payment {payment_hash} ({amount_sats} sats)") + } + Err(e) => error!("Failed to notify moneydevkit.com for payment {payment_hash}: {e}"), + } + } + pub async fn create_checkout( &self, params: CreateCheckoutParams, @@ -130,7 +301,7 @@ fn to_bolt11_description(desc: &InvoiceDescription) -> Result String { +fn extract_scid(invoice: &Bolt11Invoice) -> String { invoice .route_hints() .iter() @@ -139,3 +310,10 @@ fn extract_scid(invoice: &ldk_node::lightning_invoice::Bolt11Invoice) -> String .map(|hop| hop.short_channel_id.to_string()) .unwrap_or_default() } + +fn format_payment_id(id: &Option) -> String { + match id { + Some(pid) => pid.0.iter().map(|b| format!("{b:02x}")).collect(), + None => "unknown".into(), + } +} From 7830f0136431bc3db7184246e42d19b6cb45fecb Mon Sep 17 00:00:00 2001 From: amackillop Date: Mon, 6 Apr 2026 14:40:03 -0700 Subject: [PATCH 5/6] Make MdkClient own its full lifecycle MdkClient::new() now takes NodeConfig + access_token instead of pre-built Arc + Arc. It builds the LDK node, HTTP client, and API client internally. start() starts the node and spawns the event loop. stop() tears both down. Node construction is a library concern, not something the caller should deal with. The previous API forced callers to build_node(), build an HTTP client, wire up MdkApiClient, then pass all three in. Bindings consumers (NAPI, Python) would have to replicate this boilerplate. The tokio runtime is now optional: pass Some(handle) to share an existing runtime (Rust callers like mdkd), or None to let the library spin up its own (language bindings). This removes the "must be called from a tokio runtime context" constraint on start(), since the library uses self.handle.spawn() instead of bare tokio::spawn(). The runtime field is removed from NodeConfig. It was never node configuration; build_node() takes it as a separate parameter now. --- src/mdk/client.rs | 83 +++++++++++++++++++++++++++++++++++++++-------- src/mdk/node.rs | 8 +++-- 2 files changed, 75 insertions(+), 16 deletions(-) diff --git a/src/mdk/client.rs b/src/mdk/client.rs index bc50d44..ac7da01 100644 --- a/src/mdk/client.rs +++ b/src/mdk/client.rs @@ -7,6 +7,8 @@ use ldk_node::lightning::ln::channelmanager::PaymentId; use ldk_node::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description, Sha256}; use ldk_node::{Event, Node}; use log::{error, info}; +use reqwest::{Client, Proxy}; +use tokio::runtime::Handle; use tokio::sync::broadcast; use tokio_util::sync::CancellationToken; @@ -16,6 +18,7 @@ use crate::mdk::mdk_api::types::{ CheckoutCustomer, CreateCheckoutRequest, PaymentEntry, PaymentReceivedRequest, RegisterInvoiceRequest, }; +use crate::mdk::node::{build_node, NodeConfig}; use crate::mdk::types::{CheckoutResult, CreateCheckoutParams, InvoiceDescription, MdkEvent}; const DEFAULT_EXPIRY_SECS: u32 = 3600; @@ -32,44 +35,86 @@ pub struct MdkClient { event_tx: broadcast::Sender, event_handler: Option, shutdown: CancellationToken, + handle: Handle, + /// Keeps the runtime alive when the library created it. + /// None when the caller provided a handle. + _runtime: Option>, } impl MdkClient { + /// Build the LDK node, HTTP client, and platform API client from config. + /// + /// `runtime` — pass `Some(handle)` to reuse an existing tokio runtime + /// (typical for Rust callers), or `None` to let the library create its own + /// (typical for language bindings). + /// + /// Does not start the node or event loop — call `start()` for that. pub fn new( - node: Arc, - api: Arc, + config: NodeConfig, + access_token: String, event_handler: Option, - ) -> Self { + runtime: Option, + ) -> Result { + let (handle, owned_runtime) = match runtime { + Some(h) => (h, None), + None => { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .map_err(|e| MdkError::Node(format!("failed to create tokio runtime: {e}")))?; + let h = rt.handle().clone(); + (h, Some(Arc::new(rt))) + } + }; + + let api_base_url = config.infra.mdk_api_base_url.clone(); + let socks_proxy = config.socks_proxy.clone(); + + let node = build_node(config, handle.clone())?; + let http_client = build_http_client(socks_proxy.as_deref())?; + let api = Arc::new(MdkApiClient::new( + http_client.clone(), + api_base_url, + access_token, + )); + let (event_tx, _) = broadcast::channel(256); - Self { + Ok(Self { node, api, event_tx, event_handler, shutdown: CancellationToken::new(), - } + handle, + _runtime: owned_runtime, + }) } - /// Spawn the internal event loop. Call once after construction. - /// The loop translates LDK events into MdkEvents, notifies the - /// platform on payment receipt, invokes the event handler callback, - /// and broadcasts to all subscribers. - pub fn start(self: &Arc) { + /// Start the LDK node and spawn the internal event loop. + pub fn start(self: &Arc) -> Result<(), MdkError> { + self.node.start()?; let this = Arc::clone(self); - tokio::spawn(async move { + self.handle.spawn(async move { this.run_event_loop().await; }); + Ok(()) } - /// Cancel the event loop. Idempotent. - pub fn stop(&self) { + /// Cancel the event loop and stop the LDK node. + pub fn stop(&self) -> Result<(), MdkError> { self.shutdown.cancel(); + self.node.stop()?; + Ok(()) } pub fn node(&self) -> &Node { &self.node } + pub fn node_arc(&self) -> Arc { + Arc::clone(&self.node) + } + pub fn subscribe(&self) -> broadcast::Receiver { self.event_tx.subscribe() } @@ -283,6 +328,18 @@ impl MdkClient { } } +fn build_http_client(socks_proxy: Option<&str>) -> Result { + let mut builder = Client::builder(); + if let Some(proxy_url) = socks_proxy { + let proxy = Proxy::all(proxy_url) + .map_err(|e| MdkError::InvalidInput(format!("invalid SOCKS5 proxy for HTTP: {e}")))?; + builder = builder.proxy(proxy); + } + builder + .build() + .map_err(|e| MdkError::Network(format!("failed to build HTTP client: {e}"))) +} + fn to_bolt11_description(desc: &InvoiceDescription) -> Result { match desc { InvoiceDescription::Direct(text) => { diff --git a/src/mdk/node.rs b/src/mdk/node.rs index 26603f5..246f5d3 100644 --- a/src/mdk/node.rs +++ b/src/mdk/node.rs @@ -26,10 +26,12 @@ pub struct NodeConfig { pub pathfinding_scores_source_url: Option, pub mnemonic: String, pub infra: NetworkInfra, - pub runtime: tokio::runtime::Handle, } -pub fn build_node(config: NodeConfig) -> Result, MdkError> { +pub fn build_node( + config: NodeConfig, + runtime: tokio::runtime::Handle, +) -> Result, MdkError> { let ldk_config = LdkNodeConfig { storage_dir_path: config.storage_dir_path.clone(), listening_addresses: config.listening_addresses, @@ -85,7 +87,7 @@ pub fn build_node(config: NodeConfig) -> Result, MdkError> { builder.set_liquidity_source_lsps4(lsp_pubkey, lsp_addr); info!("LSPS4 liquidity source: {}", infra.lsp_node_id); - builder.set_runtime(config.runtime); + builder.set_runtime(runtime); let mnemonic = Mnemonic::parse(&config.mnemonic) .map_err(|e| MdkError::InvalidInput(format!("invalid mnemonic: {e}")))?; From e99d9420493969db489b949535468deac64c89bc Mon Sep 17 00:00:00 2001 From: amackillop Date: Mon, 6 Apr 2026 14:50:26 -0700 Subject: [PATCH 6/6] Rewire daemon to use MdkClient facade main.rs drops ~100 lines of manual node builder code. It constructs a NodeConfig, passes it to MdkClient::new(), and calls start(). The daemon event loop subscribes to MdkClient's broadcast channel instead of consuming raw LDK events. It only handles MdkEvent::PaymentReceived for daemon concerns (SQLite, webhooks, WebSocket). Platform notification moved to the library. handle_create_invoice delegates the checkout flow to mdk_client.create_checkout() and just stores metadata. Deleted create_with_checkout, create_jit_invoice, and extract_scid from the daemon. From for AppError maps library errors to HTTP status codes. The daemon builds its own reqwest::Client for webhooks since that's not a library concern. --- src/daemon/api/error.rs | 14 +++ src/daemon/api/invoices.rs | 179 ++++++++------------------------ src/daemon/api/mod.rs | 6 +- src/daemon/event_loop.rs | 186 ++++++++++++--------------------- src/main.rs | 203 ++++++++++--------------------------- 5 files changed, 176 insertions(+), 412 deletions(-) diff --git a/src/daemon/api/error.rs b/src/daemon/api/error.rs index a02b5c4..905c06e 100644 --- a/src/daemon/api/error.rs +++ b/src/daemon/api/error.rs @@ -2,6 +2,8 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; +use mdk::error::MdkError; + use crate::daemon::types::ApiError; #[derive(Debug)] @@ -11,6 +13,18 @@ pub enum AppError { Internal(String), } +impl From for AppError { + fn from(e: MdkError) -> Self { + match e { + MdkError::InvalidInput(msg) => AppError::BadRequest(msg), + MdkError::NotFound(msg) => AppError::NotFound(msg), + MdkError::Node(msg) => AppError::Internal(msg), + MdkError::Platform { message, .. } => AppError::Internal(message), + MdkError::Network(msg) => AppError::Internal(msg), + } + } +} + impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, code, message) = match self { diff --git a/src/daemon/api/invoices.rs b/src/daemon/api/invoices.rs index 0991183..d5b84c6 100644 --- a/src/daemon/api/invoices.rs +++ b/src/daemon/api/invoices.rs @@ -3,18 +3,14 @@ use std::sync::Arc; use axum::extract::Path; use axum::Json; -use chrono::{DateTime, SecondsFormat}; use hex::FromHex; -use ldk_node::bitcoin::hashes::sha256; -use ldk_node::bitcoin::hashes::Hash as _; use ldk_node::lightning::ln::channelmanager::PaymentId; -use ldk_node::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description, Sha256}; use ldk_node::payment::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; use ldk_node::Node; -use log::{error, info}; +use log::error; -use mdk::mdk_api::client::MdkApiClient; -use mdk::mdk_api::types::{CheckoutCustomer, CreateCheckoutRequest, RegisterInvoiceRequest}; +use mdk::client::MdkClient; +use mdk::types::{CreateCheckoutParams, Customer, InvoiceDescription}; use crate::daemon::api::error::AppError; use crate::daemon::store::invoice_metadata::{InvoiceMetadata, InvoiceMetadataStore}; @@ -27,9 +23,7 @@ use crate::daemon::types::{ /// Not a spec limit. Use `descriptionHash` for longer descriptions. const MAX_DESCRIPTION_LEN: usize = 128; -const DEFAULT_EXPIRY_SECS: u32 = 3600; - -fn parse_description(req: &CreateInvoiceRequest) -> Result { +fn parse_description(req: &CreateInvoiceRequest) -> Result { match (&req.description, &req.description_hash) { (Some(desc), None) => { if desc.len() > MAX_DESCRIPTION_LEN { @@ -37,16 +31,12 @@ fn parse_description(req: &CreateInvoiceRequest) -> Result { let bytes = <[u8; 32]>::from_hex(hash_hex) .map_err(|e| AppError::BadRequest(format!("Invalid descriptionHash: {e}")))?; - Ok(Bolt11InvoiceDescription::Hash(Sha256( - sha256::Hash::from_byte_array(bytes), - ))) + Ok(InvoiceDescription::Hash(bytes)) } _ => Err(AppError::BadRequest( "Must provide either description or descriptionHash".into(), @@ -55,56 +45,13 @@ fn parse_description(req: &CreateInvoiceRequest) -> Result, + mdk_client: Arc, metadata_store: Arc, - mdk_client: Arc, req: &CreateInvoiceRequest, ) -> Result, AppError> { let description = parse_description(req)?; - let expiry_secs = req.expiry_seconds.unwrap_or(DEFAULT_EXPIRY_SECS); - - let (invoice, checkout_id) = - create_with_checkout(&node, &mdk_client, &description, req, expiry_secs).await?; - - let payment_hash = invoice.payment_hash().to_string(); - let expires_at = invoice.expires_at().map(|d| d.as_secs()).unwrap_or(0); - let amount_sat = invoice.amount_milli_satoshis().map(|m| m / 1000); - - let invoice_str = invoice.to_string(); - let metadata = InvoiceMetadata { - payment_hash: payment_hash.clone(), - external_id: req.external_id.clone(), - webhook_url: req.webhook_url.clone(), - checkout_id, - description: req.description.clone(), - invoice: Some(invoice_str.clone()), - amount_sat, - created_at: crate::daemon::time::seconds_since_epoch(), - expires_at, - }; - - metadata_store - .insert(&metadata) - .map_err(|e| AppError::Internal(format!("Failed to store invoice metadata: {e}")))?; - - Ok(Json(CreateInvoiceResponse { - amount_sat, - payment_hash, - serialized: invoice_str, - checkout_id: metadata.checkout_id, - })) -} - -async fn create_with_checkout( - node: &Node, - client: &MdkApiClient, - description: &Bolt11InvoiceDescription, - req: &CreateInvoiceRequest, - expiry_secs: u32, -) -> Result<(Bolt11Invoice, String), AppError> { - let products = req.product.as_ref().map(|p| vec![p.clone()]); - let metadata: Option = req + let metadata_json: Option = req .metadata .as_ref() .map(|s| serde_json::from_str(s)) @@ -115,7 +62,7 @@ async fn create_with_checkout( || req.customer_email.is_some() || req.customer_external_id.is_some() { - Some(CheckoutCustomer { + Some(Customer { name: req.customer_name.clone(), email: req.customer_email.clone(), external_id: req.customer_external_id.clone(), @@ -124,80 +71,41 @@ async fn create_with_checkout( None }; - let checkout_req = CreateCheckoutRequest { - node_id: node.node_id().to_string(), - amount: req.amount_sat, - currency: req.currency.clone().or_else(|| Some("SAT".into())), - products, + let params = CreateCheckoutParams { + amount_sat: req.amount_sat, + description, + expiry_seconds: req.expiry_seconds, + product: req.product.clone(), + currency: req.currency.clone(), success_url: req.success_url.clone(), - metadata, + metadata: metadata_json, customer, }; - let checkout = client.create_checkout(&checkout_req).await.map_err(|e| { - error!("MDK checkout/create failed: {e}"); - AppError::Internal(format!("Failed to create checkout: {e}")) - })?; + let result = mdk_client.create_checkout(params).await?; - info!( - "Created checkout {} (status: {})", - checkout.id, checkout.status - ); - - // Use the amount from the checkout (authoritative for product-based checkouts). - let amount_msat = match checkout.invoice_amount_sats { - Some(sats) => Some(sats * 1000), - None => req.amount_sat.map(|s| s * 1000), - }; - - let invoice = create_jit_invoice(node, amount_msat, description, expiry_secs)?; - - let scid = extract_scid(&invoice); - let payment_hash = invoice.payment_hash().to_string(); - let expires_at_iso = invoice - .expires_at() - .and_then(|d| { - DateTime::from_timestamp(d.as_secs() as i64, 0) - .map(|dt| dt.to_rfc3339_opts(SecondsFormat::Secs, true)) - }) - .unwrap_or_default(); - - let register_req = RegisterInvoiceRequest { - node_id: node.node_id().to_string(), - scid, - checkout_id: checkout.id.clone(), - invoice: invoice.to_string(), - payment_hash, - invoice_expires_at: expires_at_iso, + let metadata = InvoiceMetadata { + payment_hash: result.payment_hash.clone(), + external_id: req.external_id.clone(), + webhook_url: req.webhook_url.clone(), + checkout_id: result.checkout_id.clone(), + description: req.description.clone(), + invoice: Some(result.invoice.clone()), + amount_sat: result.amount_sat, + created_at: crate::daemon::time::seconds_since_epoch(), + expires_at: result.expires_at.unwrap_or(0), }; - let _registered = client.register_invoice(®ister_req).await.map_err(|e| { - error!("MDK checkout/registerInvoice failed: {e}"); - AppError::Internal(format!("Failed to register invoice: {e}")) - })?; - - Ok((invoice, checkout.id)) -} - -fn create_jit_invoice( - node: &Node, - amount_msat: Option, - description: &Bolt11InvoiceDescription, - expiry_secs: u32, -) -> Result { - node.bolt11_payment() - .receive_via_lsps4_jit_channel(amount_msat, description, expiry_secs) - .map_err(|e| AppError::Internal(format!("Failed to create JIT invoice: {e}"))) -} + metadata_store + .insert(&metadata) + .map_err(|e| AppError::Internal(format!("Failed to store invoice metadata: {e}")))?; -fn extract_scid(invoice: &Bolt11Invoice) -> String { - invoice - .route_hints() - .iter() - .flat_map(|hint| &hint.0) - .next() - .map(|hop| hop.short_channel_id.to_string()) - .unwrap_or_default() + Ok(Json(CreateInvoiceResponse { + amount_sat: result.amount_sat, + payment_hash: result.payment_hash, + serialized: result.invoice, + checkout_id: result.checkout_id, + })) } pub async fn handle_get_incoming_payment( @@ -207,8 +115,8 @@ pub async fn handle_get_incoming_payment( ) -> Result, AppError> { let metadata = metadata_store .get_by_payment_hash(&payment_hash) - .map_err(|e| AppError::Internal(format!("Failed to query metadata: {}", e)))? - .ok_or_else(|| AppError::NotFound(format!("Invoice {} not found", payment_hash)))?; + .map_err(|e| AppError::Internal(format!("Failed to query metadata: {e}")))? + .ok_or_else(|| AppError::NotFound(format!("Invoice {payment_hash} not found")))?; let hash_bytes = <[u8; 32]>::from_hex(&payment_hash) .map_err(|_| AppError::BadRequest("Invalid payment hash hex".into()))?; @@ -327,11 +235,10 @@ pub async fn handle_get_outgoing_payment( .map_err(|_| AppError::BadRequest("Invalid payment id hex".into()))?; let details = node .payment(&PaymentId(id_bytes)) - .ok_or_else(|| AppError::NotFound(format!("Payment {} not found", payment_id)))?; + .ok_or_else(|| AppError::NotFound(format!("Payment {payment_id} not found")))?; if details.direction != PaymentDirection::Outbound { return Err(AppError::NotFound(format!( - "Payment {} not found", - payment_id + "Payment {payment_id} not found" ))); } Ok(Json(payment_to_outgoing(&details))) @@ -448,7 +355,7 @@ fn extract_preimage(kind: &PaymentKind) -> Option { | PaymentKind::Bolt11Jit { preimage, .. } | PaymentKind::Bolt12Offer { preimage, .. } | PaymentKind::Bolt12Refund { preimage, .. } - | PaymentKind::Spontaneous { preimage, .. } => preimage.map(|p| format!("{}", p)), + | PaymentKind::Spontaneous { preimage, .. } => preimage.map(|p| format!("{p}")), PaymentKind::Onchain { .. } => None, } } @@ -597,7 +504,7 @@ mod tests { let req = test_req(Some("coffee"), None); assert!(matches!( parse_description(&req), - Ok(Bolt11InvoiceDescription::Direct(_)) + Ok(InvoiceDescription::Direct(_)) )); } @@ -607,7 +514,7 @@ mod tests { let req = test_req(None, Some(&hash)); assert!(matches!( parse_description(&req), - Ok(Bolt11InvoiceDescription::Hash(_)) + Ok(InvoiceDescription::Hash(_)) )); } diff --git a/src/daemon/api/mod.rs b/src/daemon/api/mod.rs index f32549b..a04832b 100644 --- a/src/daemon/api/mod.rs +++ b/src/daemon/api/mod.rs @@ -22,7 +22,7 @@ use utoipa_scalar::{Scalar, Servable}; pub use auth::HttpAuth; -use mdk::mdk_api::client::MdkApiClient; +use mdk::client::MdkClient; use crate::daemon::api::error::AppError; use crate::daemon::store::invoice_metadata::InvoiceMetadataStore; @@ -38,7 +38,7 @@ pub struct AppState { pub node: Arc, pub metadata_store: Arc, pub http_auth: HttpAuth, - pub mdk_client: Arc, + pub mdk_client: Arc, pub event_tx: broadcast::Sender, } @@ -232,7 +232,7 @@ async fn create_invoice( State(state): State, Form(req): Form, ) -> Result, AppError> { - invoices::handle_create_invoice(state.node, state.metadata_store, state.mdk_client, &req).await + invoices::handle_create_invoice(state.mdk_client, state.metadata_store, &req).await } #[utoipa::path( diff --git a/src/daemon/event_loop.rs b/src/daemon/event_loop.rs index 79f232e..2557de1 100644 --- a/src/daemon/event_loop.rs +++ b/src/daemon/event_loop.rs @@ -1,11 +1,10 @@ use std::sync::Arc; -use ldk_node::{Event, Node}; -use log::{error, info}; +use log::{error, info, warn}; use tokio::sync::broadcast; -use mdk::mdk_api::client::MdkApiClient; -use mdk::mdk_api::types::{PaymentEntry, PaymentReceivedRequest}; +use mdk::client::MdkClient; +use mdk::types::MdkEvent; use crate::daemon::store::invoice_metadata::InvoiceMetadataStore; use crate::daemon::time; @@ -13,135 +12,80 @@ use crate::daemon::types::WebhookEvent; use crate::daemon::webhook::dispatcher::spawn_webhook_delivery; pub async fn run_event_loop( - node: Arc, + mdk_client: Arc, metadata_store: Arc, webhook_secret: Vec, http_client: reqwest::Client, - mdk_client: Arc, event_tx: broadcast::Sender, ) { + let mut rx = mdk_client.subscribe(); loop { - let event = node.next_event_async().await; - match event { - Event::PaymentReceived { - payment_hash, - amount_msat, - .. - } => { - info!( - "PAYMENT_RECEIVED: hash {}, amount_msat {}", - payment_hash, amount_msat + match rx.recv().await { + Ok(event) => { + handle_event( + &event, + &metadata_store, + &webhook_secret, + &http_client, + &event_tx, ); + } + Err(broadcast::error::RecvError::Lagged(n)) => { + warn!("Daemon event handler lagged, missed {n} events"); + } + Err(broadcast::error::RecvError::Closed) => { + info!("MdkClient event channel closed, stopping daemon event loop"); + break; + } + } + } +} - if let Err(e) = node.event_handled() { - error!("Failed to mark event as handled: {e}"); - } +fn handle_event( + event: &MdkEvent, + metadata_store: &InvoiceMetadataStore, + webhook_secret: &[u8], + http_client: &reqwest::Client, + event_tx: &broadcast::Sender, +) { + let MdkEvent::PaymentReceived { + payment_hash, + amount_sats, + } = event + else { + return; + }; - // Trigger webhook and MDK notification if registered for this payment hash. - let hash_str = payment_hash.to_string(); - match metadata_store.get_by_payment_hash(&hash_str) { - Ok(Some(metadata)) => { - if let Err(e) = metadata_store.mark_paid(&hash_str) { - error!("Failed to mark payment paid: {e}"); - } + let metadata = match metadata_store.get_by_payment_hash(payment_hash) { + Ok(Some(m)) => m, + Ok(None) => return, + Err(e) => { + error!("Failed to look up invoice metadata: {e}"); + return; + } + }; - let event = WebhookEvent::PaymentReceived { - payment_hash: hash_str.clone(), - amount_msat, - external_id: metadata.external_id.clone(), - timestamp: time::seconds_since_epoch(), - }; + if let Err(e) = metadata_store.mark_paid(payment_hash) { + error!("Failed to mark payment paid: {e}"); + } - if let Ok(json) = serde_json::to_string(&event) { - let _ = event_tx.send(json); - } + let webhook_event = WebhookEvent::PaymentReceived { + payment_hash: payment_hash.clone(), + amount_msat: amount_sats * 1000, + external_id: metadata.external_id.clone(), + timestamp: time::seconds_since_epoch(), + }; - if let Some(webhook_url) = metadata.webhook_url { - spawn_webhook_delivery( - http_client.clone(), - webhook_url, - webhook_secret.clone(), - event, - ); - } + if let Ok(json) = serde_json::to_string(&webhook_event) { + let _ = event_tx.send(json); + } - let client = Arc::clone(&mdk_client); - let hash = hash_str.clone(); - let amount_sats = amount_msat / 1000; - tokio::spawn(async move { - let req = PaymentReceivedRequest { - payments: vec![PaymentEntry { - payment_hash: hash.clone(), - amount_sats, - sandbox: false, - }], - }; - if let Err(e) = client.payment_received(&req).await { - error!( - "Failed to notify moneydevkit.com for payment {}: {e}", - hash - ); - } else { - info!( - "Notified moneydevkit.com of payment {} ({} sats)", - hash, amount_sats - ); - } - }); - } - Ok(None) => {} - Err(e) => error!("Failed to look up invoice metadata: {e}"), - } - } - Event::PaymentForwarded { - prev_channel_id, - next_channel_id, - total_fee_earned_msat, - outbound_amount_forwarded_msat, - .. - } => { - info!( - "PAYMENT_FORWARDED: outbound_msat {}, fee_msat: {}, in: {}, out: {}", - outbound_amount_forwarded_msat.unwrap_or(0), - total_fee_earned_msat.unwrap_or(0), - prev_channel_id, - next_channel_id - ); - if let Err(e) = node.event_handled() { - error!("Failed to mark event as handled: {e}"); - } - } - Event::ChannelPending { - channel_id, - counterparty_node_id, - .. - } => { - info!( - "CHANNEL_PENDING: {} from {}", - channel_id, counterparty_node_id - ); - if let Err(e) = node.event_handled() { - error!("Failed to mark event as handled: {e}"); - } - } - Event::ChannelReady { - channel_id, - counterparty_node_id, - .. - } => { - info!( - "CHANNEL_READY: {} from {:?}", - channel_id, counterparty_node_id - ); - if let Err(e) = node.event_handled() { - error!("Failed to mark event as handled: {e}"); - } - } - _ => { - if let Err(e) = node.event_handled() { - error!("Failed to mark event as handled: {e}"); - } - } - } + if let Some(webhook_url) = metadata.webhook_url { + spawn_webhook_delivery( + http_client.clone(), + webhook_url, + webhook_secret.to_vec(), + webhook_event, + ); } } diff --git a/src/main.rs b/src/main.rs index adec3e2..755e25f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,14 @@ mod daemon; -use std::collections::HashMap; -use std::net::ToSocketAddrs; use std::path::PathBuf; -use std::str::FromStr; use std::sync::Arc; use clap::Parser; use hex::FromHex; -use ldk_node::bip39::Mnemonic; -use ldk_node::bitcoin::hashes::sha256; -use ldk_node::bitcoin::hashes::Hash; -use ldk_node::bitcoin::secp256k1::PublicKey; -use ldk_node::config::Config as LdkNodeConfig; -use ldk_node::lightning::ln::msgs::SocketAddress; -use ldk_node::Builder; use log::{error, info}; -use mdk::config::{ChainSource, NetworkInfra}; -use mdk::mdk_api::client::MdkApiClient; +use mdk::client::MdkClient; +use mdk::config::NetworkInfra; +use mdk::node::NodeConfig; use reqwest::{Client, Proxy}; use tokio::signal::unix::SignalKind; use tokio::sync::broadcast; @@ -117,86 +108,6 @@ fn main() { daemon::logger::init(config_file.log_level); - // Optional SOCKS5 proxy for all outbound traffic. - let socks_proxy_url = args.socks_proxy; - let socks_proxy_addr = socks_proxy_url.as_ref().map(|raw| { - let host_port = raw - .strip_prefix("socks5://") - .or_else(|| raw.strip_prefix("socks5h://")) - .unwrap_or_else(|| { - eprintln!("SOCKS5 proxy url must start with socks5:// or socks5h://"); - std::process::exit(1); - }); - host_port - .to_socket_addrs() - .ok() - .and_then(|mut addrs| addrs.next()) - .unwrap_or_else(|| { - eprintln!("cannot resolve SOCKS5 proxy {}", host_port); - std::process::exit(1); - }) - }); - - if let Some(ref url) = socks_proxy_url { - info!("SOCKS5 proxy enabled: {}", url); - } - - let ldk_node_config = LdkNodeConfig { - storage_dir_path: network_dir.to_str().unwrap().to_string(), - listening_addresses: config_file.listening_addrs, - announcement_addresses: config_file.announcement_addrs, - network: config_file.network, - ..Default::default() - }; - - let mut builder = Builder::from_config(ldk_node_config); - builder.set_log_facade_logger(); - - if let Some(addr) = socks_proxy_addr { - builder.set_socks5_proxy(addr); - } - - if let Some(alias) = config_file.alias { - if let Err(e) = builder.set_node_alias(alias.to_string()) { - error!("Failed to set node alias: {e}"); - std::process::exit(1); - } - } - - match &infra.chain_source { - ChainSource::Esplora(server_url) => { - builder.set_chain_source_esplora(server_url.clone(), None); - } - ChainSource::Bitcoind { - rpc_host, - rpc_port, - rpc_user, - rpc_password, - } => { - builder.set_chain_source_bitcoind_rpc( - rpc_host.clone(), - *rpc_port, - rpc_user.clone(), - rpc_password.clone(), - ); - } - } - - if let Some(url) = config_file.pathfinding_scores_source_url { - builder.set_pathfinding_scores_source(url); - } - - let lsp_pubkey = PublicKey::from_str(&infra.lsp_node_id).unwrap_or_else(|e| { - error!("Bad lsp_node_id: {e}"); - std::process::exit(1); - }); - let lsp_addr = SocketAddress::from_str(&infra.lsp_address).unwrap_or_else(|e| { - error!("Bad lsp_address: {e}"); - std::process::exit(1); - }); - builder.set_liquidity_source_lsps4(lsp_pubkey, lsp_addr); - info!("LSPS4 liquidity source: {}", infra.lsp_node_id); - let runtime = match tokio::runtime::Builder::new_multi_thread() .enable_all() .build() @@ -208,45 +119,24 @@ fn main() { } }; - builder.set_runtime(runtime.handle().clone()); + let socks_proxy = args.socks_proxy; - let mnemonic = Mnemonic::parse(&mnemonic_phrase).unwrap_or_else(|e| { - error!("Invalid MDK_MNEMONIC: {e}"); - std::process::exit(1); - }); - builder.set_entropy_bip39_mnemonic(mnemonic.clone(), None); - - let store_id = derive_vss_identifier(&mnemonic); - info!( - "VSS store: {} (store_id={}...)", - infra.vss_url, - &store_id[..16] - ); - - let node = match builder.build_with_vss_store_and_fixed_headers( - infra.vss_url.clone(), - store_id, - HashMap::new(), - ) { - Ok(node) => Arc::new(node), - Err(e) => { - error!("Failed to build LDK Node: {e}"); - std::process::exit(1); - } - }; - - let db_path = network_dir.join("mdkd.sqlite"); - let metadata_store = match InvoiceMetadataStore::new(&db_path) { - Ok(store) => Arc::new(store), - Err(e) => { - error!("Failed to create InvoiceMetadataStore: {e}"); - std::process::exit(1); - } + let node_config = NodeConfig { + network: config_file.network, + storage_dir_path: network_dir.to_str().unwrap().to_string(), + listening_addresses: config_file.listening_addrs, + announcement_addresses: config_file.announcement_addrs, + alias: config_file.alias.map(|a| a.to_string()), + socks_proxy: socks_proxy.clone(), + pathfinding_scores_source_url: config_file.pathfinding_scores_source_url, + mnemonic: mnemonic_phrase, + infra, }; + // Separate HTTP client for daemon concerns (webhooks, expiry monitor). let http_client = { let mut b = Client::builder(); - if let Some(ref proxy_url) = socks_proxy_url { + if let Some(ref proxy_url) = socks_proxy { let proxy = Proxy::all(proxy_url).unwrap_or_else(|e| { error!("Invalid SOCKS5 proxy for reqwest: {e}"); std::process::exit(1); @@ -259,22 +149,29 @@ fn main() { }) }; - let base_url = infra.mdk_api_base_url; - info!("MDK platform integration enabled ({})", base_url); - let mdk_client = Arc::new(MdkApiClient::new( - http_client.clone(), - base_url, - mdk_access_token, - )); + let db_path = network_dir.join("mdkd.sqlite"); + let metadata_store = match InvoiceMetadataStore::new(&db_path) { + Ok(store) => Arc::new(store), + Err(e) => { + error!("Failed to create InvoiceMetadataStore: {e}"); + std::process::exit(1); + } + }; - info!("Starting up..."); - match node.start() { - Ok(()) => {} + let mdk_client = match MdkClient::new( + node_config, + mdk_access_token, + None, + Some(runtime.handle().clone()), + ) { + Ok(client) => Arc::new(client), Err(e) => { - error!("Failed to start LDK Node: {e}"); + error!("Failed to build MdkClient: {e}"); std::process::exit(1); } - } + }; + + let node = mdk_client.node(); let addrs = node .config() @@ -300,17 +197,24 @@ fn main() { } }; - let (event_tx, _) = broadcast::channel::(128); + if let Err(e) = mdk_client.start() { + error!("Failed to start MdkClient: {e}"); + std::process::exit(1); + } + + info!("Starting up..."); + + let (ws_tx, _) = broadcast::channel::(128); let app_state = AppState { - node: Arc::clone(&node), + node: mdk_client.node_arc(), metadata_store: Arc::clone(&metadata_store), http_auth: HttpAuth { full_password: full_password.clone(), read_only_password: read_only_password.clone(), }, mdk_client: mdk_client.clone(), - event_tx: event_tx.clone(), + event_tx: ws_tx.clone(), }; let app = daemon::api::router(app_state); @@ -333,20 +237,18 @@ fn main() { daemon::expiry::run_expiry_monitor(expiry_metadata, expiry_secret, expiry_client).await; }); - let event_node = Arc::clone(&node); + let event_mdk = Arc::clone(&mdk_client); let event_metadata = Arc::clone(&metadata_store); let event_secret = webhook_secret; let event_client = http_client.clone(); - let event_mdk_client = mdk_client.clone(); tokio::spawn(async move { daemon::event_loop::run_event_loop( - event_node, + event_mdk, event_metadata, event_secret, event_client, - event_mdk_client, - event_tx, + ws_tx, ) .await; }); @@ -367,11 +269,8 @@ fn main() { } }); - node.stop().expect("Shutdown should always succeed."); + if let Err(e) = mdk_client.stop() { + error!("Error during shutdown: {e}"); + } info!("Shutdown complete."); } - -fn derive_vss_identifier(mnemonic: &Mnemonic) -> String { - let mnemonic_phrase = mnemonic.to_string(); - sha256::Hash::hash(mnemonic_phrase.as_bytes()).to_string() -}