From f244977afea42a65b8054284dad3828251180a75 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Fri, 24 May 2024 21:20:00 -0700 Subject: [PATCH 01/50] feat: init fedimint-nwc --- Cargo.lock | 4 ++++ Cargo.toml | 2 +- fedimint-nwc/Cargo.toml | 11 +++++++++++ fedimint-nwc/src/main.rs | 3 +++ 4 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 fedimint-nwc/Cargo.toml create mode 100644 fedimint-nwc/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index e74afd6..cb495ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1280,6 +1280,10 @@ dependencies = [ "tracing", ] +[[package]] +name = "fedimint-nwc" +version = "0.3.5" + [[package]] name = "fedimint-rocksdb" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index d29ed0c..71cc265 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["multimint", "fedimint-clientd"] +members = ["multimint", "fedimint-clientd", "fedimint-nwc"] resolver = "2" [workspace.package] diff --git a/fedimint-nwc/Cargo.toml b/fedimint-nwc/Cargo.toml new file mode 100644 index 0000000..4824404 --- /dev/null +++ b/fedimint-nwc/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "fedimint-nwc" +version.workspace = true +edition.workspace = true +repository.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +authors.workspace = true + +[dependencies] diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/fedimint-nwc/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From 8099f768613c8261024dbe0f2be98f5875336999 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Fri, 24 May 2024 21:29:07 -0700 Subject: [PATCH 02/50] feat: setup main and state --- Cargo.lock | 9 +++++ fedimint-nwc/Cargo.toml | 8 ++++ fedimint-nwc/src/main.rs | 78 ++++++++++++++++++++++++++++++++++++++- fedimint-nwc/src/state.rs | 17 +++++++++ 4 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 fedimint-nwc/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index cb495ef..e8273c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1283,6 +1283,15 @@ dependencies = [ [[package]] name = "fedimint-nwc" version = "0.3.5" +dependencies = [ + "anyhow", + "clap", + "dotenv", + "multimint 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio", + "tracing", + "tracing-subscriber", +] [[package]] name = "fedimint-rocksdb" diff --git a/fedimint-nwc/Cargo.toml b/fedimint-nwc/Cargo.toml index 4824404..e16489c 100644 --- a/fedimint-nwc/Cargo.toml +++ b/fedimint-nwc/Cargo.toml @@ -9,3 +9,11 @@ readme.workspace = true authors.workspace = true [dependencies] +anyhow = "1.0.75" +clap = { version = "3", features = ["derive", "env"] } +dotenv = "0.15.0" +multimint = { version = "0.3.6" } +# multimint = { path = "../multimint" } +tokio = { version = "1.34.0", features = ["full"] } +tracing = "0.1.40" +tracing-subscriber = "0.3.18" diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index e7a11a9..309f592 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -1,3 +1,77 @@ -fn main() { - println!("Hello, world!"); +use std::path::PathBuf; +use std::str::FromStr; + +use anyhow::Result; +use multimint::fedimint_core::api::InviteCode; +use tracing::info; + +pub mod state; + +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, + + /// 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, +} + +#[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.")); + } + + Ok(()) } diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs new file mode 100644 index 0000000..e1227f3 --- /dev/null +++ b/fedimint-nwc/src/state.rs @@ -0,0 +1,17 @@ +use std::path::PathBuf; + +use anyhow::Result; +use multimint::MultiMint; + +#[derive(Debug, Clone)] +pub struct AppState { + pub multimint: MultiMint, +} + +impl AppState { + pub async fn new(fm_db_path: PathBuf) -> Result { + let clients = MultiMint::new(fm_db_path).await?; + clients.update_gateway_caches().await?; + Ok(Self { multimint: clients }) + } +} From d5eb3bec20473382c11b83ae04a8fe151d06ffc0 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 12:00:55 -0700 Subject: [PATCH 03/50] feat: nip47 server and user keys --- Cargo.lock | 563 +++++++++++++++++++++++++++++++++++-- fedimint-nwc/Cargo.toml | 6 +- fedimint-nwc/src/config.rs | 35 +++ fedimint-nwc/src/main.rs | 34 +-- fedimint-nwc/src/nwc.rs | 12 + fedimint-nwc/src/state.rs | 82 +++++- 6 files changed, 681 insertions(+), 51 deletions(-) create mode 100644 fedimint-nwc/src/config.rs create mode 100644 fedimint-nwc/src/nwc.rs diff --git a/Cargo.lock b/Cargo.lock index e8273c1..2065cd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.4" @@ -76,6 +86,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.82" @@ -181,6 +240,44 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "async-wsocket" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c38341e6ee670913fb9dc3aba40c22d616261da4dc0928326d3168ebf576fb0" +dependencies = [ + "async-utility", + "futures-util", + "thiserror", + "tokio", + "tokio-rustls 0.25.0", + "tokio-socks", + "tokio-tungstenite", + "url", + "wasm-ws", + "webpki-roots 0.26.1", +] + +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + +[[package]] +name = "atomic-destructor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4653a42bf04120a1d4e92452e006b4e3af4ab4afff8fb4af0f1bbb98418adf3e" +dependencies = [ + "tracing", +] + [[package]] name = "atty" version = "0.2.14" @@ -262,7 +359,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.60", @@ -353,6 +450,12 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bech32" +version = "0.10.0-beta" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" + [[package]] name = "beef" version = "0.5.2" @@ -391,13 +494,24 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "bip39" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f" +dependencies = [ + "bitcoin_hashes 0.11.0", + "serde", + "unicode-normalization", +] + [[package]] name = "bitcoin" version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0694ea59225b0c5f3cb405ff3f670e4828358ed26aec49dc352f730f0cb1a8a3" dependencies = [ - "bech32", + "bech32 0.9.1", "bitcoin_hashes 0.11.0", "core2", "hashbrown 0.8.2", @@ -411,7 +525,7 @@ version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1945a5048598e4189e239d3f809b19bdad4845c4b2ba400d304d2dcf26d2c462" dependencies = [ - "bech32", + "bech32 0.9.1", "bitcoin-private", "bitcoin_hashes 0.12.0", "hex_lit", @@ -419,11 +533,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoin" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c85783c2fe40083ea54a33aa2f0ba58831d90fcd190f5bdc47e74e84d2a96ae" +dependencies = [ + "bech32 0.10.0-beta", + "bitcoin-internals", + "bitcoin_hashes 0.13.0", + "hex-conservative", + "hex_lit", + "secp256k1 0.28.2", + "serde", +] + [[package]] name = "bitcoin-internals" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" +dependencies = [ + "serde", +] [[package]] name = "bitcoin-private" @@ -459,6 +591,7 @@ checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" dependencies = [ "bitcoin-internals", "hex-conservative", + "serde", ] [[package]] @@ -635,6 +768,30 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.38" @@ -657,6 +814,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -678,28 +836,62 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", "bitflags 1.3.2", - "clap_derive", - "clap_lex", + "clap_derive 3.2.25", + "clap_lex 0.2.4", "indexmap 1.9.3", "once_cell", - "strsim", + "strsim 0.10.0", "termcolor", "textwrap", ] +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", + "clap_derive 4.5.4", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex 0.7.0", + "strsim 0.11.1", +] + [[package]] name = "clap_derive" version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro-error", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "clap_lex" version = "0.2.4" @@ -709,6 +901,18 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + [[package]] name = "concurrent-queue" version = "2.4.0" @@ -780,6 +984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -803,7 +1008,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 2.0.60", ] @@ -1032,7 +1237,7 @@ dependencies = [ "bitcoin 0.29.2", "bitcoin_hashes 0.13.0", "chrono", - "clap", + "clap 3.2.25", "dotenv", "fedimint", "futures-util", @@ -1064,7 +1269,7 @@ dependencies = [ "async-trait", "backon", "backtrace", - "bech32", + "bech32 0.9.1", "bincode", "bitcoin 0.29.2", "bitcoin 0.30.2", @@ -1285,9 +1490,13 @@ name = "fedimint-nwc" version = "0.3.5" dependencies = [ "anyhow", - "clap", + "clap 4.5.4", "dotenv", "multimint 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "nostr", + "nostr-sdk", + "serde", + "serde_json", "tokio", "tracing", "tracing-subscriber", @@ -1514,7 +1723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" dependencies = [ "gloo-timers 0.2.6", - "send_wrapper", + "send_wrapper 0.4.0", ] [[package]] @@ -1690,6 +1899,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1732,6 +1947,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "http" version = "0.2.12" @@ -2040,12 +2264,30 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" @@ -2264,7 +2506,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb24878b0f4ef75f020976c886d9ad1503867802329cc963e0ab4623ea3b25c" dependencies = [ - "bech32", + "bech32 0.9.1", "bitcoin 0.29.2", "bitcoin_hashes 0.11.0", "lightning", @@ -2273,6 +2515,18 @@ dependencies = [ "serde", ] +[[package]] +name = "lnurl-pay" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c042191c2e3f27147decfad8182eea2c7dd1c6c1733562e25d3d401369669d" +dependencies = [ + "bech32 0.10.0-beta", + "reqwest 0.12.4", + "serde", + "serde_json", +] + [[package]] name = "lnurl-rs" version = "0.5.0" @@ -2282,7 +2536,7 @@ dependencies = [ "aes", "anyhow", "base64 0.22.0", - "bech32", + "bech32 0.9.1", "bitcoin 0.30.2", "cbc", "email_address", @@ -2458,6 +2712,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "negentropy" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e664971378a3987224f7a0e10059782035e89899ae403718ee07de85bec42afe" + [[package]] name = "nom" version = "7.1.3" @@ -2468,6 +2728,110 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nostr" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed4f065c7357937f9149fa6fb04c89c3041a8a01ca472b887baded59b65cfe2c" +dependencies = [ + "aes", + "base64 0.21.7", + "bip39", + "bitcoin 0.31.2", + "cbc", + "chacha20", + "chacha20poly1305", + "getrandom", + "instant", + "js-sys", + "negentropy", + "once_cell", + "reqwest 0.12.4", + "scrypt", + "serde", + "serde_json", + "tracing", + "unicode-normalization", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "nostr-database" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89506f743a5441695ab727794db41d8df1c1365ff96c25272985adf08f816b3" +dependencies = [ + "async-trait", + "lru", + "nostr", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "nostr-relay-pool" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751acc8bbb1329718d673470c7c3a18cddd33963dd91b97bccc92037113d254" +dependencies = [ + "async-utility", + "async-wsocket", + "atomic-destructor", + "nostr", + "nostr-database", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "nostr-sdk" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e65cd9f4f26f3f8e10253c518aff9e61a9204f600dfe4c3c241b0230471c67f" +dependencies = [ + "async-utility", + "lnurl-pay", + "nostr", + "nostr-database", + "nostr-relay-pool", + "nostr-signer", + "nostr-zapper", + "nwc", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "nostr-signer" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be1878e91a0b4a95cfd8142349b6124b037b287375d76db9638ccc4b4cdf271" +dependencies = [ + "async-utility", + "nostr", + "nostr-relay-pool", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "nostr-zapper" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5558bb031cff46e5b580847f26617d516ded4c0f8fd27fb568ec875bcd8fb99c" +dependencies = [ + "async-trait", + "nostr", + "thiserror", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2503,6 +2867,20 @@ dependencies = [ "libc", ] +[[package]] +name = "nwc" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd88cc13a04ae41037c182489893c2f421ba0c12a028564ec339882e7f96d61" +dependencies = [ + "async-utility", + "nostr", + "nostr-relay-pool", + "nostr-zapper", + "thiserror", + "tracing", +] + [[package]] name = "object" version = "0.32.2" @@ -2682,12 +3060,32 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -2726,6 +3124,17 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3045,6 +3454,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustls" version = "0.20.9" @@ -3141,12 +3559,33 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "sct" version = "0.7.1" @@ -3181,6 +3620,18 @@ dependencies = [ "serde", ] +[[package]] +name = "secp256k1" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +dependencies = [ + "bitcoin_hashes 0.13.0", + "rand", + "secp256k1-sys 0.9.2", + "serde", +] + [[package]] name = "secp256k1-sys" version = "0.6.1" @@ -3199,6 +3650,15 @@ dependencies = [ "cc", ] +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", +] + [[package]] name = "secp256k1-zkp" version = "0.7.0" @@ -3221,12 +3681,24 @@ dependencies = [ "secp256k1-sys 0.6.1", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "send_wrapper" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "serde" version = "1.0.198" @@ -3262,6 +3734,7 @@ version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -3313,6 +3786,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + [[package]] name = "sha3" version = "0.10.8" @@ -3405,6 +3889,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.26.2" @@ -3417,7 +3907,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", @@ -3686,8 +4176,12 @@ checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", + "rustls 0.22.4", + "rustls-pki-types", "tokio", + "tokio-rustls 0.25.0", "tungstenite", + "webpki-roots 0.26.1", ] [[package]] @@ -3850,6 +4344,8 @@ dependencies = [ "httparse", "log", "rand", + "rustls 0.22.4", + "rustls-pki-types", "sha1", "thiserror", "url", @@ -3876,13 +4372,23 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" @@ -3919,6 +4425,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "validator" version = "0.17.0" @@ -4048,6 +4560,23 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wasm-ws" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5b3a482e27ff54809c0848629d9033179705c5ea2f58e26cf45dc77c34c4984" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "pharos", + "send_wrapper 0.6.0", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.69" diff --git a/fedimint-nwc/Cargo.toml b/fedimint-nwc/Cargo.toml index e16489c..1bb3d5c 100644 --- a/fedimint-nwc/Cargo.toml +++ b/fedimint-nwc/Cargo.toml @@ -10,10 +10,14 @@ authors.workspace = true [dependencies] anyhow = "1.0.75" -clap = { version = "3", features = ["derive", "env"] } +clap = { version = "4.5.4", features = ["derive", "env"] } dotenv = "0.15.0" multimint = { version = "0.3.6" } # multimint = { path = "../multimint" } +nostr = { version = "0.31.2", features = ["nip47"] } +nostr-sdk = { version = "0.31.0", features = ["nip47"] } +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" diff --git a/fedimint-nwc/src/config.rs b/fedimint-nwc/src/config.rs new file mode 100644 index 0000000..ccebb07 --- /dev/null +++ b/fedimint-nwc/src/config.rs @@ -0,0 +1,35 @@ +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; + +#[derive(Subcommand)] +enum Commands { + Start, + Stop, +} + +#[derive(Parser)] +#[clap(version = "1.0", author = "Kody Low")] +pub struct Cli { + /// Federation invite code + #[clap(long, env = "FEDIMINT_CLIENTD_INVITE_CODE", required = false)] + pub invite_code: String, + /// Path to FM database + #[clap(long, env = "FEDIMINT_CLIENTD_DB_PATH", required = true)] + pub db_path: PathBuf, + /// Manual secret + #[clap(long, env = "FEDIMINT_CLIENTD_MANUAL_SECRET", required = false)] + pub manual_secret: Option, + /// Location of keys file + #[clap(default_value_t = String::from("keys.json"), long)] + pub keys_file: String, + #[clap(long)] + /// Nostr relay to use + pub relay: String, + /// Max invoice payment amount, in satoshis + #[clap(default_value_t = 100_000, long)] + pub max_amount: u64, + /// Max payment amount per day, in satoshis + #[clap(default_value_t = 100_000, long)] + pub daily_limit: u64, +} diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index 309f592..d8ac87c 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -1,41 +1,17 @@ -use std::path::PathBuf; use std::str::FromStr; use anyhow::Result; +use clap::Parser; +use config::Cli; use multimint::fedimint_core::api::InviteCode; use tracing::info; +pub mod config; +pub mod nwc; pub mod state; -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, - - /// 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, -} - #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt::init(); @@ -43,7 +19,7 @@ async fn main() -> Result<()> { let cli: Cli = Cli::parse(); - let mut state = AppState::new(cli.db_path).await?; + let mut state = AppState::new(cli.db_path, &cli.keys_file).await?; let manual_secret = match cli.manual_secret { Some(secret) => Some(secret), diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs new file mode 100644 index 0000000..71a5064 --- /dev/null +++ b/fedimint-nwc/src/nwc.rs @@ -0,0 +1,12 @@ +use nostr_sdk::nips::nip47::Method; + +pub const METHODS: [Method; 8] = [ + Method::GetInfo, + Method::MakeInvoice, + Method::GetBalance, + Method::LookupInvoice, + Method::PayInvoice, + Method::MultiPayInvoice, + Method::PayKeysend, + Method::MultiPayKeysend, +]; diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index e1227f3..83de0b6 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -1,17 +1,91 @@ -use std::path::PathBuf; +use std::fs::{create_dir_all, File}; +use std::io::{BufReader, Write}; +use std::path::{Path, PathBuf}; -use anyhow::Result; +use anyhow::{Context, Result}; use multimint::MultiMint; +use nostr_sdk::secp256k1::SecretKey; +use nostr_sdk::Keys; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone)] pub struct AppState { pub multimint: MultiMint, + pub user_keys: Keys, + pub server_keys: Keys, } impl AppState { - pub async fn new(fm_db_path: PathBuf) -> Result { + pub async fn new(fm_db_path: PathBuf, keys_file: &str) -> Result { let clients = MultiMint::new(fm_db_path).await?; clients.update_gateway_caches().await?; - Ok(Self { multimint: clients }) + + let keys = Nip47Keys::load_or_generate(keys_file)?; + + Ok(Self { + multimint: clients, + user_keys: keys.user_keys(), + server_keys: keys.server_keys(), + }) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +struct Nip47Keys { + server_key: SecretKey, + user_key: SecretKey, + #[serde(default)] + sent_info: bool, +} + +impl Nip47Keys { + fn generate() -> Result { + let server_keys = Keys::generate(); + let server_key = server_keys.secret_key()?; + + let user_keys = Keys::generate(); + let user_key = user_keys.secret_key()?; + + Ok(Nip47Keys { + server_key: **server_key, + user_key: **user_key, + sent_info: false, + }) + } + + fn load_or_generate(keys_file: &str) -> Result { + let path = Path::new(keys_file); + match File::open(path) { + Ok(file) => { + let reader = BufReader::new(file); + serde_json::from_reader(reader).context("Failed to parse JSON") + } + Err(_) => { + let keys = Self::generate()?; + Self::write_keys(&keys, path)?; + Ok(keys) + } + } + } + + fn write_keys(keys: &Nip47Keys, path: &Path) -> Result<()> { + let json_str = serde_json::to_string(keys).context("Failed to serialize data")?; + + if let Some(parent) = path.parent() { + create_dir_all(parent).context("Failed to create directory")?; + } + + let mut file = File::create(path).context("Failed to create file")?; + file.write_all(json_str.as_bytes()) + .context("Failed to write to file")?; + Ok(()) + } + + fn server_keys(&self) -> Keys { + Keys::new(self.server_key.into()) + } + + fn user_keys(&self) -> Keys { + Keys::new(self.user_key.into()) } } From ebef8a9d11a1e927d456005cb43dd50ebf1a9956 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 12:31:02 -0700 Subject: [PATCH 04/50] fix: better shutdown handling --- fedimint-nwc/src/main.rs | 76 +++++++++++++++++++++++---------------- fedimint-nwc/src/state.rs | 53 ++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 32 deletions(-) diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index d8ac87c..732b2c8 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -1,10 +1,9 @@ -use std::str::FromStr; - use anyhow::Result; use clap::Parser; use config::Cli; -use multimint::fedimint_core::api::InviteCode; -use tracing::info; +use tokio::signal::unix::SignalKind; +use tokio::sync::oneshot; +use tracing::{debug, info}; pub mod config; pub mod nwc; @@ -17,37 +16,52 @@ async fn main() -> Result<()> { tracing_subscriber::fmt::init(); dotenv::dotenv().ok(); - let cli: Cli = Cli::parse(); - - let mut state = AppState::new(cli.db_path, &cli.keys_file).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); + let cli = Cli::parse(); + let db_path = cli.db_path.clone(); + let keys_file = cli.keys_file.clone(); + let mut state = AppState::new(db_path, &keys_file).await?; + + let manual_secret = AppState::load_manual_secret(&cli).await; + let invite_code = cli.invite_code.clone(); + state.init_multimint(&invite_code, manual_secret).await?; + + if state.multimint.all().await.is_empty() { + return Err(anyhow::anyhow!( + "No multimint clients found, must have at least one client to start the server." + )); + } + + // Shutdown signal handler + let (tx, rx) = oneshot::channel::<()>(); + let signal_handler = tokio::spawn(handle_signals(tx)); + + // Start the event loop + + // Wait for shutdown signal + info!("Server is running. Press CTRL+C to exit."); + let _ = rx.await; + info!("Shutting down..."); + state.wait_for_active_requests().await; + let _ = signal_handler.await; + + Ok(()) +} + +async fn handle_signals(tx: oneshot::Sender<()>) -> Result<()> { + let signals = tokio::signal::unix::signal(SignalKind::terminate()) + .or_else(|_| tokio::signal::unix::signal(SignalKind::interrupt())); + + match signals { + Ok(mut stream) => { + while stream.recv().await.is_some() { + debug!("Received shutdown signal"); + } } Err(e) => { - info!( - "No federation invite code provided, skipping client creation: {}", - e - ); + return Err(anyhow::anyhow!("Failed to install signal handlers: {}", 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 _ = tx.send(()); Ok(()) } diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index 83de0b6..20402a4 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -1,18 +1,28 @@ +use std::collections::BTreeSet; use std::fs::{create_dir_all, File}; use std::io::{BufReader, Write}; use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; use anyhow::{Context, Result}; +use multimint::fedimint_core::api::InviteCode; use multimint::MultiMint; use nostr_sdk::secp256k1::SecretKey; -use nostr_sdk::Keys; +use nostr_sdk::{EventId, Keys}; use serde::{Deserialize, Serialize}; +use tokio::sync::Mutex; +use tracing::{debug, error, info}; + +use crate::config::Cli; #[derive(Debug, Clone)] pub struct AppState { pub multimint: MultiMint, pub user_keys: Keys, pub server_keys: Keys, + pub active_requests: Arc>>, } impl AppState { @@ -22,12 +32,53 @@ impl AppState { let keys = Nip47Keys::load_or_generate(keys_file)?; + let active_requests = Arc::new(Mutex::new(BTreeSet::new())); + Ok(Self { multimint: clients, user_keys: keys.user_keys(), server_keys: keys.server_keys(), + active_requests, }) } + + pub async fn load_manual_secret(cli: &Cli) -> Option { + cli.manual_secret + .clone() + .or_else(|| std::env::var("FEDIMINT_CLIENTD_MANUAL_SECRET").ok()) + } + + pub async fn init_multimint( + &mut self, + invite_code: &str, + manual_secret: Option, + ) -> Result<()> { + match InviteCode::from_str(invite_code) { + Ok(invite_code) => { + let federation_id = self + .multimint + .register_new(invite_code, manual_secret) + .await?; + info!("Created client for federation id: {:?}", federation_id); + Ok(()) + } + Err(e) => { + error!("Invalid federation invite code: {}", e); + Err(e.into()) + } + } + } + + pub async fn wait_for_active_requests(&self) { + let requests = self.active_requests.lock().await; + loop { + if requests.is_empty() { + break; + } + debug!("Waiting for {} requests to complete...", requests.len()); + tokio::time::sleep(Duration::from_secs(1)).await; + } + } } #[derive(Debug, Clone, Deserialize, Serialize)] From ff8bcf71cfb8585647f182e5ea07bdffdbd53d47 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 12:56:23 -0700 Subject: [PATCH 05/50] feat: event loop setup --- fedimint-nwc/src/main.rs | 37 ++++++++++++++++++++++- fedimint-nwc/src/nwc.rs | 7 +++++ fedimint-nwc/src/state.rs | 62 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index 732b2c8..f534c9b 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -1,6 +1,9 @@ +use std::time::Duration; + use anyhow::Result; use clap::Parser; use config::Cli; +use nostr_sdk::RelayPoolNotification; use tokio::signal::unix::SignalKind; use tokio::sync::oneshot; use tracing::{debug, info}; @@ -19,7 +22,9 @@ async fn main() -> Result<()> { let cli = Cli::parse(); let db_path = cli.db_path.clone(); let keys_file = cli.keys_file.clone(); - let mut state = AppState::new(db_path, &keys_file).await?; + let relay = cli.relay.clone(); + + let mut state = AppState::new(db_path, &keys_file, &relay).await?; let manual_secret = AppState::load_manual_secret(&cli).await; let invite_code = cli.invite_code.clone(); @@ -35,7 +40,11 @@ async fn main() -> Result<()> { let (tx, rx) = oneshot::channel::<()>(); let signal_handler = tokio::spawn(handle_signals(tx)); + // Broadcast info event + state.broadcast_info_event().await?; + // Start the event loop + event_loop(state.clone()).await?; // Wait for shutdown signal info!("Server is running. Press CTRL+C to exit."); @@ -65,3 +74,29 @@ async fn handle_signals(tx: oneshot::Sender<()>) -> Result<()> { let _ = tx.send(()); Ok(()) } + +async fn event_loop(state: AppState) -> Result<()> { + state.nostr_client.connect().await; + loop { + info!("Listening for events..."); + let (tx, _) = tokio::sync::watch::channel(()); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(60 * 15)).await; + let _ = tx.send(()); + }); + + let mut notifications = state.nostr_client.notifications(); + while let Ok(notification) = notifications.recv().await { + match notification { + RelayPoolNotification::Event { event, .. } => state.handle_event(*event).await, + RelayPoolNotification::Shutdown => { + info!("Relay pool shutdown"); + break; + } + _ => {} + } + } + + state.nostr_client.disconnect().await?; + } +} diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 71a5064..a231fd7 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -1,4 +1,7 @@ use nostr_sdk::nips::nip47::Method; +use nostr_sdk::Event; + +use crate::state::AppState; pub const METHODS: [Method; 8] = [ Method::GetInfo, @@ -10,3 +13,7 @@ pub const METHODS: [Method; 8] = [ Method::PayKeysend, Method::MultiPayKeysend, ]; + +pub async fn handle_nwc_request(_state: &AppState, _event: Event) -> Result<(), anyhow::Error> { + Ok(()) +} diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index 20402a4..d758ecd 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -10,27 +10,34 @@ use anyhow::{Context, Result}; use multimint::fedimint_core::api::InviteCode; use multimint::MultiMint; use nostr_sdk::secp256k1::SecretKey; -use nostr_sdk::{EventId, Keys}; +use nostr_sdk::{Client, Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, Timestamp}; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; use tracing::{debug, error, info}; use crate::config::Cli; +use crate::nwc::{handle_nwc_request, METHODS}; #[derive(Debug, Clone)] pub struct AppState { pub multimint: MultiMint, pub user_keys: Keys, pub server_keys: Keys, + pub sent_info: bool, + pub nostr_client: Client, pub active_requests: Arc>>, } impl AppState { - pub async fn new(fm_db_path: PathBuf, keys_file: &str) -> Result { + pub async fn new(fm_db_path: PathBuf, keys_file: &str, relay: &str) -> Result { let clients = MultiMint::new(fm_db_path).await?; clients.update_gateway_caches().await?; let keys = Nip47Keys::load_or_generate(keys_file)?; + let nostr_client = Client::new(&keys.server_keys()); + nostr_client.add_relay(relay).await?; + let subscription = setup_subscription(&keys); + nostr_client.subscribe(vec![subscription], None).await; let active_requests = Arc::new(Mutex::new(BTreeSet::new())); @@ -38,6 +45,8 @@ impl AppState { multimint: clients, user_keys: keys.user_keys(), server_keys: keys.server_keys(), + sent_info: keys.sent_info, + nostr_client, active_requests, }) } @@ -79,6 +88,47 @@ impl AppState { tokio::time::sleep(Duration::from_secs(1)).await; } } + + pub async fn broadcast_info_event(&mut self) -> Result<()> { + let content = METHODS + .iter() + .map(ToString::to_string) + .collect::>() + .join(" "); + let info = + EventBuilder::new(Kind::WalletConnectInfo, content, []).to_event(&self.server_keys)?; + let res = self + .nostr_client + .send_event(info) + .await + .map_err(|e| anyhow::anyhow!("Failed to send event: {}", e)); + + if res.is_ok() { + self.sent_info = true; + } + + Ok(()) + } + + pub async fn handle_event(&self, event: Event) { + if event.kind == Kind::WalletConnectRequest && event.verify().is_ok() { + debug!("Received event!"); + let event_id = event.id; + self.active_requests.lock().await.insert(event_id); + + match tokio::time::timeout(Duration::from_secs(60), handle_nwc_request(&self, event)) + .await + { + Ok(Ok(_)) => {} + Ok(Err(e)) => error!("Error processing request: {e}"), + Err(e) => error!("Timeout error: {e}"), + } + + self.active_requests.lock().await.remove(&event_id); + } else { + error!("Invalid event: {}", event.as_json()); + } + } } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -140,3 +190,11 @@ impl Nip47Keys { Keys::new(self.user_key.into()) } } + +fn setup_subscription(keys: &Nip47Keys) -> Filter { + Filter::new() + .kinds(vec![Kind::WalletConnectRequest]) + .author(keys.user_keys().public_key()) + .pubkey(keys.server_keys().public_key()) + .since(Timestamp::now()) +} From 42ba5637bdc8f489a9c8ed041bbd84c6f76d4898 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 12:59:38 -0700 Subject: [PATCH 06/50] fix: config handling --- fedimint-nwc/src/config.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fedimint-nwc/src/config.rs b/fedimint-nwc/src/config.rs index ccebb07..c89674c 100644 --- a/fedimint-nwc/src/config.rs +++ b/fedimint-nwc/src/config.rs @@ -21,15 +21,15 @@ pub struct Cli { #[clap(long, env = "FEDIMINT_CLIENTD_MANUAL_SECRET", required = false)] pub manual_secret: Option, /// Location of keys file - #[clap(default_value_t = String::from("keys.json"), long)] + #[clap(long, env = "FEDIMINT_NWC_KEYS_FILE", default_value_t = String::from("keys.json"))] pub keys_file: String, - #[clap(long)] /// Nostr relay to use + #[clap(long, env = "FEDIMINT_NWC_RELAY", default_value_t = String::from("wss://relay.damus.io"))] pub relay: String, /// Max invoice payment amount, in satoshis - #[clap(default_value_t = 100_000, long)] + #[clap(long, env = "FEDIMINT_NWC_MAX_AMOUNT", default_value_t = 100_000)] pub max_amount: u64, /// Max payment amount per day, in satoshis - #[clap(default_value_t = 100_000, long)] + #[clap(long, env = "FEDIMINT_NWC_DAILY_LIMIT", default_value_t = 100_000)] pub daily_limit: u64, } From 6aa17801075733b32d0683cc6443dca00c0ace64 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 13:02:26 -0700 Subject: [PATCH 07/50] fix: info logging plus ignore keys --- .gitignore | 2 ++ fedimint-nwc/src/main.rs | 1 + fedimint-nwc/src/state.rs | 5 ++++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8d72668..65c55d7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ result /vendor federations.txt +keys.json +fedimint-nwc/keys.json diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index f534c9b..981b85d 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -39,6 +39,7 @@ async fn main() -> Result<()> { // Shutdown signal handler let (tx, rx) = oneshot::channel::<()>(); let signal_handler = tokio::spawn(handle_signals(tx)); + info!("Shutdown signal handler started..."); // Broadcast info event state.broadcast_info_event().await?; diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index d758ecd..b0d347b 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -105,6 +105,9 @@ impl AppState { if res.is_ok() { self.sent_info = true; + info!("Sent info event..."); + } else { + error!("Failed to send info event: {}", res.err().unwrap()); } Ok(()) @@ -112,7 +115,7 @@ impl AppState { pub async fn handle_event(&self, event: Event) { if event.kind == Kind::WalletConnectRequest && event.verify().is_ok() { - debug!("Received event!"); + info!("Received event: {}", event.as_json()); let event_id = event.id; self.active_requests.lock().await.insert(event_id); From 4242ce40e7610266bee5cc5ced96a95428a3f92d Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 13:17:00 -0700 Subject: [PATCH 08/50] feat: relay pool + better logging --- fedimint-nwc/src/config.rs | 4 ++-- fedimint-nwc/src/main.rs | 4 ++-- fedimint-nwc/src/state.rs | 16 ++++++++++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/fedimint-nwc/src/config.rs b/fedimint-nwc/src/config.rs index c89674c..84baf61 100644 --- a/fedimint-nwc/src/config.rs +++ b/fedimint-nwc/src/config.rs @@ -24,8 +24,8 @@ pub struct Cli { #[clap(long, env = "FEDIMINT_NWC_KEYS_FILE", default_value_t = String::from("keys.json"))] pub keys_file: String, /// Nostr relay to use - #[clap(long, env = "FEDIMINT_NWC_RELAY", default_value_t = String::from("wss://relay.damus.io"))] - pub relay: String, + #[clap(long, env = "FEDIMINT_NWC_RELAYS", default_value_t = String::from("wss://relay.damus.io"))] + pub relays: String, /// Max invoice payment amount, in satoshis #[clap(long, env = "FEDIMINT_NWC_MAX_AMOUNT", default_value_t = 100_000)] pub max_amount: u64, diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index 981b85d..b75738c 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -22,9 +22,9 @@ async fn main() -> Result<()> { let cli = Cli::parse(); let db_path = cli.db_path.clone(); let keys_file = cli.keys_file.clone(); - let relay = cli.relay.clone(); + let relays = cli.relays.clone(); - let mut state = AppState::new(db_path, &keys_file, &relay).await?; + let mut state = AppState::new(db_path, &keys_file, &relays).await?; let manual_secret = AppState::load_manual_secret(&cli).await; let invite_code = cli.invite_code.clone(); diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index b0d347b..8114b1b 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -29,13 +29,25 @@ pub struct AppState { } impl AppState { - pub async fn new(fm_db_path: PathBuf, keys_file: &str, relay: &str) -> Result { + pub async fn new(fm_db_path: PathBuf, keys_file: &str, relays: &str) -> Result { let clients = MultiMint::new(fm_db_path).await?; clients.update_gateway_caches().await?; + info!("Setting up nostr client..."); let keys = Nip47Keys::load_or_generate(keys_file)?; + let lines = relays.split(',').collect::>(); + let relays = lines + .iter() + .map(|line| line.trim()) + .filter(|line| !line.is_empty()) + .map(|line| line.to_string()) + .collect::>(); let nostr_client = Client::new(&keys.server_keys()); - nostr_client.add_relay(relay).await?; + info!("Adding relays..."); + for relay in relays { + nostr_client.add_relay(relay).await?; + } + info!("Setting NWC subscription..."); let subscription = setup_subscription(&keys); nostr_client.subscribe(vec![subscription], None).await; From 6a68d7bf43a64b1aa6db35caea473e9a36532101 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 13:20:46 -0700 Subject: [PATCH 09/50] refactor: state setup --- fedimint-nwc/src/state.rs | 46 +++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index 8114b1b..a47438d 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -30,11 +30,39 @@ pub struct AppState { impl AppState { pub async fn new(fm_db_path: PathBuf, keys_file: &str, relays: &str) -> Result { + let clients = Self::init_multimint_clients(fm_db_path).await?; + let keys = Nip47Keys::load_or_generate(keys_file)?; + let nostr_client = Self::setup_nostr_client(&keys, relays).await?; + + let active_requests = Arc::new(Mutex::new(BTreeSet::new())); + + Ok(Self { + multimint: clients, + user_keys: keys.user_keys(), + server_keys: keys.server_keys(), + sent_info: keys.sent_info, + nostr_client, + active_requests, + }) + } + + async fn init_multimint_clients(fm_db_path: PathBuf) -> Result { let clients = MultiMint::new(fm_db_path).await?; clients.update_gateway_caches().await?; + Ok(clients) + } + async fn setup_nostr_client(keys: &Nip47Keys, relays: &str) -> Result { info!("Setting up nostr client..."); - let keys = Nip47Keys::load_or_generate(keys_file)?; + let nostr_client = Client::new(&keys.server_keys()); + Self::add_relays(&nostr_client, relays).await?; + info!("Setting NWC subscription..."); + let subscription = setup_subscription(keys); + nostr_client.subscribe(vec![subscription], None).await; + Ok(nostr_client) + } + + async fn add_relays(nostr_client: &Client, relays: &str) -> Result<()> { let lines = relays.split(',').collect::>(); let relays = lines .iter() @@ -42,25 +70,11 @@ impl AppState { .filter(|line| !line.is_empty()) .map(|line| line.to_string()) .collect::>(); - let nostr_client = Client::new(&keys.server_keys()); info!("Adding relays..."); for relay in relays { nostr_client.add_relay(relay).await?; } - info!("Setting NWC subscription..."); - let subscription = setup_subscription(&keys); - nostr_client.subscribe(vec![subscription], None).await; - - let active_requests = Arc::new(Mutex::new(BTreeSet::new())); - - Ok(Self { - multimint: clients, - user_keys: keys.user_keys(), - server_keys: keys.server_keys(), - sent_info: keys.sent_info, - nostr_client, - active_requests, - }) + Ok(()) } pub async fn load_manual_secret(cli: &Cli) -> Option { From dbf5173d335284699e01a91e1e5da1403834ffe3 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 13:25:14 -0700 Subject: [PATCH 10/50] refactor: init app state --- fedimint-nwc/src/main.rs | 22 +++------------------- fedimint-nwc/src/state.rs | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index b75738c..2b454c2 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -2,7 +2,6 @@ use std::time::Duration; use anyhow::Result; use clap::Parser; -use config::Cli; use nostr_sdk::RelayPoolNotification; use tokio::signal::unix::SignalKind; use tokio::sync::oneshot; @@ -14,36 +13,21 @@ pub mod state; use state::AppState; +use crate::config::Cli; + #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt::init(); dotenv::dotenv().ok(); let cli = Cli::parse(); - let db_path = cli.db_path.clone(); - let keys_file = cli.keys_file.clone(); - let relays = cli.relays.clone(); - - let mut state = AppState::new(db_path, &keys_file, &relays).await?; - - let manual_secret = AppState::load_manual_secret(&cli).await; - let invite_code = cli.invite_code.clone(); - state.init_multimint(&invite_code, manual_secret).await?; - - if state.multimint.all().await.is_empty() { - return Err(anyhow::anyhow!( - "No multimint clients found, must have at least one client to start the server." - )); - } + let state = state::init(cli).await?; // Shutdown signal handler let (tx, rx) = oneshot::channel::<()>(); let signal_handler = tokio::spawn(handle_signals(tx)); info!("Shutdown signal handler started..."); - // Broadcast info event - state.broadcast_info_event().await?; - // Start the event loop event_loop(state.clone()).await?; diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index a47438d..9b747ba 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -18,6 +18,29 @@ use tracing::{debug, error, info}; use crate::config::Cli; use crate::nwc::{handle_nwc_request, METHODS}; +pub async fn init(cli: Cli) -> Result { + let db_path = cli.db_path.clone(); + let keys_file = cli.keys_file.clone(); + let relays = cli.relays.clone(); + + let mut state = AppState::new(db_path, &keys_file, &relays).await?; + + let manual_secret = AppState::load_manual_secret(&cli).await; + let invite_code = cli.invite_code.clone(); + state.init_multimint(&invite_code, manual_secret).await?; + + if state.multimint.all().await.is_empty() { + return Err(anyhow::anyhow!( + "No multimint clients found, must have at least one client to start the server." + )); + } + + // Broadcast info event on startup + state.broadcast_info_event().await?; + + Ok(state) +} + #[derive(Debug, Clone)] pub struct AppState { pub multimint: MultiMint, From b4140cfadc2261cc8c50518713fabfe4a19f38d5 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 13:26:15 -0700 Subject: [PATCH 11/50] refactor: move --- fedimint-nwc/src/state.rs | 43 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index 9b747ba..c663c27 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -28,7 +28,6 @@ pub async fn init(cli: Cli) -> Result { let manual_secret = AppState::load_manual_secret(&cli).await; let invite_code = cli.invite_code.clone(); state.init_multimint(&invite_code, manual_secret).await?; - if state.multimint.all().await.is_empty() { return Err(anyhow::anyhow!( "No multimint clients found, must have at least one client to start the server." @@ -69,6 +68,27 @@ impl AppState { }) } + pub async fn init_multimint( + &mut self, + invite_code: &str, + manual_secret: Option, + ) -> Result<()> { + match InviteCode::from_str(invite_code) { + Ok(invite_code) => { + let federation_id = self + .multimint + .register_new(invite_code, manual_secret) + .await?; + info!("Created client for federation id: {:?}", federation_id); + Ok(()) + } + Err(e) => { + error!("Invalid federation invite code: {}", e); + Err(e.into()) + } + } + } + async fn init_multimint_clients(fm_db_path: PathBuf) -> Result { let clients = MultiMint::new(fm_db_path).await?; clients.update_gateway_caches().await?; @@ -106,27 +126,6 @@ impl AppState { .or_else(|| std::env::var("FEDIMINT_CLIENTD_MANUAL_SECRET").ok()) } - pub async fn init_multimint( - &mut self, - invite_code: &str, - manual_secret: Option, - ) -> Result<()> { - match InviteCode::from_str(invite_code) { - Ok(invite_code) => { - let federation_id = self - .multimint - .register_new(invite_code, manual_secret) - .await?; - info!("Created client for federation id: {:?}", federation_id); - Ok(()) - } - Err(e) => { - error!("Invalid federation invite code: {}", e); - Err(e.into()) - } - } - } - pub async fn wait_for_active_requests(&self) { let requests = self.active_requests.lock().await; loop { From 636860f20be04a74488370ade20f5c383ee4f9f9 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 13:43:27 -0700 Subject: [PATCH 12/50] refactor: managers and services --- fedimint-nwc/src/main.rs | 10 +- fedimint-nwc/src/managers/key.rs | 64 ++++++++ fedimint-nwc/src/managers/mod.rs | 3 + fedimint-nwc/src/nwc.rs | 2 +- fedimint-nwc/src/services/mod.rs | 5 + fedimint-nwc/src/services/multimint.rs | 40 +++++ fedimint-nwc/src/services/nostr.rs | 62 +++++++ fedimint-nwc/src/state.rs | 215 +++---------------------- 8 files changed, 202 insertions(+), 199 deletions(-) create mode 100644 fedimint-nwc/src/managers/key.rs create mode 100644 fedimint-nwc/src/managers/mod.rs create mode 100644 fedimint-nwc/src/services/mod.rs create mode 100644 fedimint-nwc/src/services/multimint.rs create mode 100644 fedimint-nwc/src/services/nostr.rs diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index 2b454c2..40dfbd1 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -8,7 +8,9 @@ use tokio::sync::oneshot; use tracing::{debug, info}; pub mod config; +pub mod managers; pub mod nwc; +pub mod services; pub mod state; use state::AppState; @@ -21,7 +23,7 @@ async fn main() -> Result<()> { dotenv::dotenv().ok(); let cli = Cli::parse(); - let state = state::init(cli).await?; + let state = AppState::new(cli).await?; // Shutdown signal handler let (tx, rx) = oneshot::channel::<()>(); @@ -61,7 +63,7 @@ async fn handle_signals(tx: oneshot::Sender<()>) -> Result<()> { } async fn event_loop(state: AppState) -> Result<()> { - state.nostr_client.connect().await; + state.nostr_service.connect().await; loop { info!("Listening for events..."); let (tx, _) = tokio::sync::watch::channel(()); @@ -70,7 +72,7 @@ async fn event_loop(state: AppState) -> Result<()> { let _ = tx.send(()); }); - let mut notifications = state.nostr_client.notifications(); + let mut notifications = state.nostr_service.notifications(); while let Ok(notification) = notifications.recv().await { match notification { RelayPoolNotification::Event { event, .. } => state.handle_event(*event).await, @@ -82,6 +84,6 @@ async fn event_loop(state: AppState) -> Result<()> { } } - state.nostr_client.disconnect().await?; + state.nostr_service.disconnect().await?; } } diff --git a/fedimint-nwc/src/managers/key.rs b/fedimint-nwc/src/managers/key.rs new file mode 100644 index 0000000..6945edb --- /dev/null +++ b/fedimint-nwc/src/managers/key.rs @@ -0,0 +1,64 @@ +use std::fs::{create_dir_all, File}; +use std::io::{BufReader, Write}; +use std::path::Path; + +use anyhow::{Context, Result}; +use nostr_sdk::secp256k1::SecretKey; +use nostr_sdk::Keys; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct KeyManager { + server_key: SecretKey, + user_key: SecretKey, + #[serde(default)] + pub sent_info: bool, +} + +impl KeyManager { + pub fn new(keys_file: &str) -> Result { + let path = Path::new(keys_file); + match File::open(path) { + Ok(file) => { + let reader = BufReader::new(file); + serde_json::from_reader(reader).context("Failed to parse JSON") + } + Err(_) => { + let keys = Self::generate()?; + Self::write_keys(&keys, path)?; + Ok(keys) + } + } + } + + fn generate() -> Result { + let server_keys = Keys::generate(); + let server_key = server_keys.secret_key()?; + let user_keys = Keys::generate(); + let user_key = user_keys.secret_key()?; + Ok(Self { + server_key: **server_key, + user_key: **user_key, + sent_info: false, + }) + } + + fn write_keys(keys: &Self, path: &Path) -> Result<()> { + let json_str = serde_json::to_string(keys).context("Failed to serialize data")?; + if let Some(parent) = path.parent() { + create_dir_all(parent).context("Failed to create directory")?; + } + let mut file = File::create(path).context("Failed to create file")?; + file.write_all(json_str.as_bytes()) + .context("Failed to write to file")?; + Ok(()) + } + + pub fn server_keys(&self) -> Keys { + Keys::new(self.server_key.into()) + } + + pub fn user_keys(&self) -> Keys { + Keys::new(self.user_key.into()) + } +} diff --git a/fedimint-nwc/src/managers/mod.rs b/fedimint-nwc/src/managers/mod.rs new file mode 100644 index 0000000..5f2e9f2 --- /dev/null +++ b/fedimint-nwc/src/managers/mod.rs @@ -0,0 +1,3 @@ +pub mod key; + +pub use key::KeyManager; diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index a231fd7..d8a95a8 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -1,4 +1,4 @@ -use nostr_sdk::nips::nip47::Method; +use nostr::nips::nip47::Method; use nostr_sdk::Event; use crate::state::AppState; diff --git a/fedimint-nwc/src/services/mod.rs b/fedimint-nwc/src/services/mod.rs new file mode 100644 index 0000000..ed3a576 --- /dev/null +++ b/fedimint-nwc/src/services/mod.rs @@ -0,0 +1,5 @@ +pub mod multimint; +pub mod nostr; + +pub use multimint::MultiMintService; +pub use nostr::NostrService; diff --git a/fedimint-nwc/src/services/multimint.rs b/fedimint-nwc/src/services/multimint.rs new file mode 100644 index 0000000..6ac77de --- /dev/null +++ b/fedimint-nwc/src/services/multimint.rs @@ -0,0 +1,40 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use anyhow::Result; +use multimint::fedimint_core::api::InviteCode; +use multimint::MultiMint; + +#[derive(Debug, Clone)] +pub struct MultiMintService { + multimint: MultiMint, +} + +impl MultiMintService { + pub async fn new(db_path: PathBuf) -> Result { + let clients = MultiMint::new(db_path).await?; + clients.update_gateway_caches().await?; + Ok(Self { multimint: clients }) + } + + pub async fn init_multimint( + &mut self, + invite_code: &str, + manual_secret: Option, + ) -> Result<()> { + match InviteCode::from_str(invite_code) { + Ok(invite_code) => { + let federation_id = self + .multimint + .register_new(invite_code, manual_secret) + .await?; + tracing::info!("Created client for federation id: {:?}", federation_id); + Ok(()) + } + Err(e) => { + tracing::error!("Invalid federation invite code: {}", e); + Err(e.into()) + } + } + } +} diff --git a/fedimint-nwc/src/services/nostr.rs b/fedimint-nwc/src/services/nostr.rs new file mode 100644 index 0000000..f353678 --- /dev/null +++ b/fedimint-nwc/src/services/nostr.rs @@ -0,0 +1,62 @@ +use anyhow::Result; +use nostr_sdk::{Client, EventBuilder, EventId, Kind, RelayPoolNotification}; +use tokio::sync::broadcast::Receiver; + +use crate::managers::key::KeyManager; +use crate::nwc::METHODS; + +#[derive(Debug, Clone)] +pub struct NostrService { + client: Client, +} + +impl NostrService { + pub async fn new(key_manager: &KeyManager, relays: &str) -> Result { + let client = Client::new(&key_manager.server_keys()); + Self::add_relays(&client, relays).await?; + Ok(Self { client }) + } + + async fn add_relays(client: &Client, relays: &str) -> Result<()> { + let lines = relays.split(',').collect::>(); + let relays = lines + .iter() + .map(|line| line.trim()) + .filter(|line| !line.is_empty()) + .map(|line| line.to_string()) + .collect::>(); + for relay in relays { + client.add_relay(relay).await?; + } + Ok(()) + } + + pub async fn broadcast_info_event(&self, keys: &KeyManager) -> Result { + let content = METHODS + .iter() + .map(ToString::to_string) + .collect::>() + .join(" "); + let info = EventBuilder::new(Kind::WalletConnectInfo, content, []) + .to_event(&keys.server_keys())?; + self.client + .send_event(info) + .await + .map_err(|e| anyhow::anyhow!("Failed to send event: {}", e)) + } + + pub async fn connect(&self) -> () { + self.client.connect().await + } + + pub async fn disconnect(&self) -> Result<()> { + self.client + .disconnect() + .await + .map_err(|e| anyhow::anyhow!("Failed to disconnect: {}", e)) + } + + pub fn notifications(&self) -> Receiver { + self.client.notifications() + } +} diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index c663c27..c70e8f1 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -1,131 +1,50 @@ use std::collections::BTreeSet; -use std::fs::{create_dir_all, File}; -use std::io::{BufReader, Write}; -use std::path::{Path, PathBuf}; -use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -use anyhow::{Context, Result}; -use multimint::fedimint_core::api::InviteCode; -use multimint::MultiMint; -use nostr_sdk::secp256k1::SecretKey; -use nostr_sdk::{Client, Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, Timestamp}; -use serde::{Deserialize, Serialize}; +use nostr_sdk::{Event, EventId, JsonUtil, Kind}; use tokio::sync::Mutex; use tracing::{debug, error, info}; use crate::config::Cli; -use crate::nwc::{handle_nwc_request, METHODS}; - -pub async fn init(cli: Cli) -> Result { - let db_path = cli.db_path.clone(); - let keys_file = cli.keys_file.clone(); - let relays = cli.relays.clone(); - - let mut state = AppState::new(db_path, &keys_file, &relays).await?; - - let manual_secret = AppState::load_manual_secret(&cli).await; - let invite_code = cli.invite_code.clone(); - state.init_multimint(&invite_code, manual_secret).await?; - if state.multimint.all().await.is_empty() { - return Err(anyhow::anyhow!( - "No multimint clients found, must have at least one client to start the server." - )); - } - - // Broadcast info event on startup - state.broadcast_info_event().await?; - - Ok(state) -} +use crate::managers::KeyManager; +use crate::nwc::handle_nwc_request; +use crate::services::{MultiMintService, NostrService}; #[derive(Debug, Clone)] pub struct AppState { - pub multimint: MultiMint, - pub user_keys: Keys, - pub server_keys: Keys, - pub sent_info: bool, - pub nostr_client: Client, + pub multimint_service: MultiMintService, + pub nostr_service: NostrService, + pub key_manager: KeyManager, pub active_requests: Arc>>, } impl AppState { - pub async fn new(fm_db_path: PathBuf, keys_file: &str, relays: &str) -> Result { - let clients = Self::init_multimint_clients(fm_db_path).await?; - let keys = Nip47Keys::load_or_generate(keys_file)?; - let nostr_client = Self::setup_nostr_client(&keys, relays).await?; + pub async fn new(cli: Cli) -> Result { + let key_manager = KeyManager::new(&cli.keys_file)?; + let multimint_service = MultiMintService::new(cli.db_path).await?; + let nostr_service = NostrService::new(&key_manager, &cli.relays).await?; let active_requests = Arc::new(Mutex::new(BTreeSet::new())); Ok(Self { - multimint: clients, - user_keys: keys.user_keys(), - server_keys: keys.server_keys(), - sent_info: keys.sent_info, - nostr_client, + multimint_service, + nostr_service, + key_manager, active_requests, }) } - pub async fn init_multimint( - &mut self, - invite_code: &str, - manual_secret: Option, - ) -> Result<()> { - match InviteCode::from_str(invite_code) { - Ok(invite_code) => { - let federation_id = self - .multimint - .register_new(invite_code, manual_secret) - .await?; - info!("Created client for federation id: {:?}", federation_id); - Ok(()) - } - Err(e) => { - error!("Invalid federation invite code: {}", e); - Err(e.into()) - } - } - } - - async fn init_multimint_clients(fm_db_path: PathBuf) -> Result { - let clients = MultiMint::new(fm_db_path).await?; - clients.update_gateway_caches().await?; - Ok(clients) - } - - async fn setup_nostr_client(keys: &Nip47Keys, relays: &str) -> Result { - info!("Setting up nostr client..."); - let nostr_client = Client::new(&keys.server_keys()); - Self::add_relays(&nostr_client, relays).await?; - info!("Setting NWC subscription..."); - let subscription = setup_subscription(keys); - nostr_client.subscribe(vec![subscription], None).await; - Ok(nostr_client) - } - - async fn add_relays(nostr_client: &Client, relays: &str) -> Result<()> { - let lines = relays.split(',').collect::>(); - let relays = lines - .iter() - .map(|line| line.trim()) - .filter(|line| !line.is_empty()) - .map(|line| line.to_string()) - .collect::>(); - info!("Adding relays..."); - for relay in relays { - nostr_client.add_relay(relay).await?; - } + pub async fn init(&mut self, cli: &Cli) -> Result<(), anyhow::Error> { + self.multimint_service + .init_multimint(&cli.invite_code, cli.manual_secret.clone()) + .await?; + self.nostr_service + .broadcast_info_event(&self.key_manager) + .await?; Ok(()) } - pub async fn load_manual_secret(cli: &Cli) -> Option { - cli.manual_secret - .clone() - .or_else(|| std::env::var("FEDIMINT_CLIENTD_MANUAL_SECRET").ok()) - } - pub async fn wait_for_active_requests(&self) { let requests = self.active_requests.lock().await; loop { @@ -137,30 +56,6 @@ impl AppState { } } - pub async fn broadcast_info_event(&mut self) -> Result<()> { - let content = METHODS - .iter() - .map(ToString::to_string) - .collect::>() - .join(" "); - let info = - EventBuilder::new(Kind::WalletConnectInfo, content, []).to_event(&self.server_keys)?; - let res = self - .nostr_client - .send_event(info) - .await - .map_err(|e| anyhow::anyhow!("Failed to send event: {}", e)); - - if res.is_ok() { - self.sent_info = true; - info!("Sent info event..."); - } else { - error!("Failed to send info event: {}", res.err().unwrap()); - } - - Ok(()) - } - pub async fn handle_event(&self, event: Event) { if event.kind == Kind::WalletConnectRequest && event.verify().is_ok() { info!("Received event: {}", event.as_json()); @@ -181,71 +76,3 @@ impl AppState { } } } - -#[derive(Debug, Clone, Deserialize, Serialize)] -struct Nip47Keys { - server_key: SecretKey, - user_key: SecretKey, - #[serde(default)] - sent_info: bool, -} - -impl Nip47Keys { - fn generate() -> Result { - let server_keys = Keys::generate(); - let server_key = server_keys.secret_key()?; - - let user_keys = Keys::generate(); - let user_key = user_keys.secret_key()?; - - Ok(Nip47Keys { - server_key: **server_key, - user_key: **user_key, - sent_info: false, - }) - } - - fn load_or_generate(keys_file: &str) -> Result { - let path = Path::new(keys_file); - match File::open(path) { - Ok(file) => { - let reader = BufReader::new(file); - serde_json::from_reader(reader).context("Failed to parse JSON") - } - Err(_) => { - let keys = Self::generate()?; - Self::write_keys(&keys, path)?; - Ok(keys) - } - } - } - - fn write_keys(keys: &Nip47Keys, path: &Path) -> Result<()> { - let json_str = serde_json::to_string(keys).context("Failed to serialize data")?; - - if let Some(parent) = path.parent() { - create_dir_all(parent).context("Failed to create directory")?; - } - - let mut file = File::create(path).context("Failed to create file")?; - file.write_all(json_str.as_bytes()) - .context("Failed to write to file")?; - Ok(()) - } - - fn server_keys(&self) -> Keys { - Keys::new(self.server_key.into()) - } - - fn user_keys(&self) -> Keys { - Keys::new(self.user_key.into()) - } -} - -fn setup_subscription(keys: &Nip47Keys) -> Filter { - Filter::new() - .kinds(vec![Kind::WalletConnectRequest]) - .author(keys.user_keys().public_key()) - .pubkey(keys.server_keys().public_key()) - .since(Timestamp::now()) -} From 55348f938e08a85d70186b7c309204bedb470b88 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 14:05:49 -0700 Subject: [PATCH 13/50] fix: fix --- fedimint-nwc/src/main.rs | 14 +++++++++++--- fedimint-nwc/src/services/nostr.rs | 13 +++++++------ fedimint-nwc/src/state.rs | 3 --- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index 40dfbd1..b61fa8a 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -49,8 +49,13 @@ async fn handle_signals(tx: oneshot::Sender<()>) -> Result<()> { match signals { Ok(mut stream) => { - while stream.recv().await.is_some() { - debug!("Received shutdown signal"); + if let Some(_) = stream.recv().await { + debug!("Received shutdown signal, sending oneshot message..."); + if let Err(e) = tx.send(()) { + debug!("Error sending oneshot message: {:?}", e); + return Err(anyhow::anyhow!("Failed to send oneshot message: {:?}", e)); + } + debug!("Oneshot message sent successfully."); } } Err(e) => { @@ -58,12 +63,15 @@ async fn handle_signals(tx: oneshot::Sender<()>) -> Result<()> { } } - let _ = tx.send(()); Ok(()) } async fn event_loop(state: AppState) -> Result<()> { state.nostr_service.connect().await; + state + .nostr_service + .broadcast_info_event(&state.key_manager) + .await?; loop { info!("Listening for events..."); let (tx, _) = tokio::sync::watch::channel(()); diff --git a/fedimint-nwc/src/services/nostr.rs b/fedimint-nwc/src/services/nostr.rs index f353678..186e6b4 100644 --- a/fedimint-nwc/src/services/nostr.rs +++ b/fedimint-nwc/src/services/nostr.rs @@ -1,6 +1,7 @@ use anyhow::Result; -use nostr_sdk::{Client, EventBuilder, EventId, Kind, RelayPoolNotification}; +use nostr_sdk::{Client, EventBuilder, JsonUtil, Kind, RelayPoolNotification}; use tokio::sync::broadcast::Receiver; +use tracing::info; use crate::managers::key::KeyManager; use crate::nwc::METHODS; @@ -31,7 +32,7 @@ impl NostrService { Ok(()) } - pub async fn broadcast_info_event(&self, keys: &KeyManager) -> Result { + pub async fn broadcast_info_event(&self, keys: &KeyManager) -> Result<(), anyhow::Error> { let content = METHODS .iter() .map(ToString::to_string) @@ -39,10 +40,10 @@ impl NostrService { .join(" "); let info = EventBuilder::new(Kind::WalletConnectInfo, content, []) .to_event(&keys.server_keys())?; - self.client - .send_event(info) - .await - .map_err(|e| anyhow::anyhow!("Failed to send event: {}", e)) + info!("Broadcasting info event: {}", info.as_json()); + let event_id = self.client.send_event(info).await?; + info!("Broadcasted info event: {}", event_id); + Ok(()) } pub async fn connect(&self) -> () { diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index c70e8f1..5591837 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -39,9 +39,6 @@ impl AppState { self.multimint_service .init_multimint(&cli.invite_code, cli.manual_secret.clone()) .await?; - self.nostr_service - .broadcast_info_event(&self.key_manager) - .await?; Ok(()) } From 09372ad3ba35baf15c68ab9023d6b329df07f735 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 14:29:35 -0700 Subject: [PATCH 14/50] fix: shutdown handling --- fedimint-nwc/src/main.rs | 92 +++++++++++++++------------------------- 1 file changed, 35 insertions(+), 57 deletions(-) diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index b61fa8a..3f37193 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -1,11 +1,8 @@ -use std::time::Duration; - use anyhow::Result; use clap::Parser; use nostr_sdk::RelayPoolNotification; -use tokio::signal::unix::SignalKind; -use tokio::sync::oneshot; -use tracing::{debug, info}; +use tokio::pin; +use tracing::{error, info}; pub mod config; pub mod managers; @@ -25,44 +22,9 @@ async fn main() -> Result<()> { let cli = Cli::parse(); let state = AppState::new(cli).await?; - // Shutdown signal handler - let (tx, rx) = oneshot::channel::<()>(); - let signal_handler = tokio::spawn(handle_signals(tx)); - info!("Shutdown signal handler started..."); - // Start the event loop event_loop(state.clone()).await?; - // Wait for shutdown signal - info!("Server is running. Press CTRL+C to exit."); - let _ = rx.await; - info!("Shutting down..."); - state.wait_for_active_requests().await; - let _ = signal_handler.await; - - Ok(()) -} - -async fn handle_signals(tx: oneshot::Sender<()>) -> Result<()> { - let signals = tokio::signal::unix::signal(SignalKind::terminate()) - .or_else(|_| tokio::signal::unix::signal(SignalKind::interrupt())); - - match signals { - Ok(mut stream) => { - if let Some(_) = stream.recv().await { - debug!("Received shutdown signal, sending oneshot message..."); - if let Err(e) = tx.send(()) { - debug!("Error sending oneshot message: {:?}", e); - return Err(anyhow::anyhow!("Failed to send oneshot message: {:?}", e)); - } - debug!("Oneshot message sent successfully."); - } - } - Err(e) => { - return Err(anyhow::anyhow!("Failed to install signal handlers: {}", e)); - } - } - Ok(()) } @@ -72,26 +34,42 @@ async fn event_loop(state: AppState) -> Result<()> { .nostr_service .broadcast_info_event(&state.key_manager) .await?; - loop { - info!("Listening for events..."); - let (tx, _) = tokio::sync::watch::channel(()); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_secs(60 * 15)).await; - let _ = tx.send(()); - }); - let mut notifications = state.nostr_service.notifications(); - while let Ok(notification) = notifications.recv().await { - match notification { - RelayPoolNotification::Event { event, .. } => state.handle_event(*event).await, - RelayPoolNotification::Shutdown => { - info!("Relay pool shutdown"); - break; + let ctrl_c = tokio::signal::ctrl_c(); + pin!(ctrl_c); // Pin the ctrl_c future + + let mut notifications = state.nostr_service.notifications(); + + info!("Listening for events..."); + + loop { + tokio::select! { + _ = &mut ctrl_c => { + info!("Ctrl+C received. Waiting for active requests to complete..."); + state.wait_for_active_requests().await; + info!("All active requests completed."); + break; + }, + notification = notifications.recv() => { + match notification { + Ok(notification) => match notification { + RelayPoolNotification::Event { event, .. } => { + state.handle_event(*event).await + }, + RelayPoolNotification::Shutdown => { + info!("Relay pool shutdown"); + break; + }, + _ => { + error!("Unhandled relay pool notification: {notification:?}"); + } + }, + Err(_) => {}, } - _ => {} } } - - state.nostr_service.disconnect().await?; } + + state.nostr_service.disconnect().await?; + Ok(()) } From f9e1155726eb33b67e87c45ef3e94de70f81cd47 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 14:32:24 -0700 Subject: [PATCH 15/50] fix: fix --- fedimint-nwc/src/main.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index 3f37193..d9447f2 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -22,21 +22,22 @@ async fn main() -> Result<()> { let cli = Cli::parse(); let state = AppState::new(cli).await?; - // Start the event loop event_loop(state.clone()).await?; Ok(()) } async fn event_loop(state: AppState) -> Result<()> { + // Connect to the relay pool and broadcast the info event state.nostr_service.connect().await; state .nostr_service .broadcast_info_event(&state.key_manager) .await?; + // Handle ctrl+c to gracefully shutdown the event loop let ctrl_c = tokio::signal::ctrl_c(); - pin!(ctrl_c); // Pin the ctrl_c future + pin!(ctrl_c); let mut notifications = state.nostr_service.notifications(); From 2d5e7c4c43272c1675a6d80789f1298824827506 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 14:35:06 -0700 Subject: [PATCH 16/50] fix: fix --- fedimint-nwc/src/main.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index d9447f2..7b1dbe9 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -22,19 +22,21 @@ async fn main() -> Result<()> { let cli = Cli::parse(); let state = AppState::new(cli).await?; - event_loop(state.clone()).await?; - - Ok(()) -} - -async fn event_loop(state: AppState) -> Result<()> { - // Connect to the relay pool and broadcast the info event + // Connect to the relay pool and broadcast the info event on startup state.nostr_service.connect().await; state .nostr_service .broadcast_info_event(&state.key_manager) .await?; + // Start the event loop + event_loop(state.clone()).await?; + + Ok(()) +} + +/// Event loop that listens for nostr events and handles them +async fn event_loop(state: AppState) -> Result<()> { // Handle ctrl+c to gracefully shutdown the event loop let ctrl_c = tokio::signal::ctrl_c(); pin!(ctrl_c); From bc0fac41a2f187d8fa2fe158bfd0278b1035eac7 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 14:37:38 -0700 Subject: [PATCH 17/50] fix: fix --- fedimint-nwc/src/main.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index 7b1dbe9..31ad7d5 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -35,16 +35,14 @@ async fn main() -> Result<()> { Ok(()) } -/// Event loop that listens for nostr events and handles them +/// Event loop that listens for nostr wallet connect events and handles them async fn event_loop(state: AppState) -> Result<()> { // Handle ctrl+c to gracefully shutdown the event loop let ctrl_c = tokio::signal::ctrl_c(); pin!(ctrl_c); let mut notifications = state.nostr_service.notifications(); - info!("Listening for events..."); - loop { tokio::select! { _ = &mut ctrl_c => { From 8c57e4008731e1e466343bf1663db8616dc897e4 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 15:07:44 -0700 Subject: [PATCH 18/50] feat: started on nwc event handling --- Cargo.lock | 38 ++++- fedimint-nwc/Cargo.toml | 1 + fedimint-nwc/src/main.rs | 11 +- fedimint-nwc/src/nwc.rs | 343 +++++++++++++++++++++++++++++++++++++- fedimint-nwc/src/state.rs | 10 +- 5 files changed, 390 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2065cd0..ef6477f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1243,7 +1243,7 @@ dependencies = [ "futures-util", "itertools 0.12.1", "lazy_static", - "lightning-invoice", + "lightning-invoice 0.26.0", "lnurl-rs", "multimint 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.12.4", @@ -1292,8 +1292,8 @@ dependencies = [ "jsonrpsee-types", "jsonrpsee-wasm-client", "jsonrpsee-ws-client", - "lightning", - "lightning-invoice", + "lightning 0.0.118", + "lightning-invoice 0.26.0", "lru", "macro_rules_attribute", "miniscript 10.0.0", @@ -1368,7 +1368,7 @@ dependencies = [ "fedimint-threshold-crypto", "futures", "itertools 0.12.1", - "lightning-invoice", + "lightning-invoice 0.26.0", "rand", "reqwest 0.11.27", "secp256k1 0.24.3", @@ -1400,8 +1400,8 @@ dependencies = [ "fedimint-threshold-crypto", "futures", "itertools 0.12.1", - "lightning", - "lightning-invoice", + "lightning 0.0.118", + "lightning-invoice 0.26.0", "rand", "secp256k1 0.24.3", "serde", @@ -1492,6 +1492,7 @@ dependencies = [ "anyhow", "clap 4.5.4", "dotenv", + "lightning-invoice 0.31.0", "multimint 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "nostr", "nostr-sdk", @@ -2500,6 +2501,16 @@ dependencies = [ "hashbrown 0.8.2", ] +[[package]] +name = "lightning" +version = "0.0.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd92d4aa159374be430c7590e169b4a6c0fb79018f5bc4ea1bffde536384db3" +dependencies = [ + "bitcoin 0.30.2", + "hex-conservative", +] + [[package]] name = "lightning-invoice" version = "0.26.0" @@ -2509,12 +2520,25 @@ dependencies = [ "bech32 0.9.1", "bitcoin 0.29.2", "bitcoin_hashes 0.11.0", - "lightning", + "lightning 0.0.118", "num-traits", "secp256k1 0.24.3", "serde", ] +[[package]] +name = "lightning-invoice" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d07d01cf197bf2184b929b7dc94aa70d935aac6df896c256a3a9475b7e9d40" +dependencies = [ + "bech32 0.9.1", + "bitcoin 0.30.2", + "lightning 0.0.123", + "secp256k1 0.27.0", + "serde", +] + [[package]] name = "lnurl-pay" version = "0.5.0" diff --git a/fedimint-nwc/Cargo.toml b/fedimint-nwc/Cargo.toml index 1bb3d5c..d588d0a 100644 --- a/fedimint-nwc/Cargo.toml +++ b/fedimint-nwc/Cargo.toml @@ -12,6 +12,7 @@ authors.workspace = true anyhow = "1.0.75" clap = { version = "4.5.4", features = ["derive", "env"] } dotenv = "0.15.0" +lightning-invoice = { version = "0.31.0", features = ["serde"] } multimint = { version = "0.3.6" } # multimint = { path = "../multimint" } nostr = { version = "0.31.2", features = ["nip47"] } diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index 31ad7d5..b61d531 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::Parser; -use nostr_sdk::RelayPoolNotification; +use nostr_sdk::{JsonUtil, Kind, RelayPoolNotification}; use tokio::pin; use tracing::{error, info}; @@ -55,7 +55,14 @@ async fn event_loop(state: AppState) -> Result<()> { match notification { Ok(notification) => match notification { RelayPoolNotification::Event { event, .. } => { - state.handle_event(*event).await + if event.kind == Kind::WalletConnectRequest + && event.pubkey == state.key_manager.user_keys().public_key() + && event.verify().is_ok() { + info!("Received event: {}", event.as_json()); + state.handle_event(*event).await + } else { + error!("Invalid nwc event: {}", event.as_json()); + } }, RelayPoolNotification::Shutdown => { info!("Relay pool shutdown"); diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index d8a95a8..ceea9e4 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -1,6 +1,15 @@ -use nostr::nips::nip47::Method; -use nostr_sdk::Event; +use std::str::FromStr; +use anyhow::{anyhow, Result}; +use lightning_invoice::Bolt11Invoice; +use nostr::nips::nip04; +use nostr::nips::nip47::{Method, Request, RequestParams}; +use nostr::Tag; +use nostr_sdk::{Event, JsonUtil}; +use tokio::spawn; +use tracing::info; + +use crate::services::{MultiMintService, NostrService}; use crate::state::AppState; pub const METHODS: [Method; 8] = [ @@ -14,6 +23,334 @@ pub const METHODS: [Method; 8] = [ Method::MultiPayKeysend, ]; -pub async fn handle_nwc_request(_state: &AppState, _event: Event) -> Result<(), anyhow::Error> { +#[derive(Debug, Clone)] +pub struct NwcConfig { + pub max_amount: u64, + pub daily_limit: u64, +} + +pub async fn handle_nwc_request(state: &AppState, event: Event) -> Result<(), anyhow::Error> { + let user_keys = state.key_manager.user_keys(); + let decrypted = nip04::decrypt(user_keys.secret_key()?, &event.pubkey, &event.content)?; + let req: Request = Request::from_json(&decrypted)?; + + info!("Request params: {:?}", req.params); + + match req.params { + RequestParams::MultiPayInvoice(params) => { + handle_multiple_payments( + params.invoices, + req.method, + &event, + state, + RequestParams::PayInvoice, + ) + .await + } + RequestParams::MultiPayKeysend(params) => { + handle_multiple_payments( + params.keysends, + req.method, + &event, + state, + RequestParams::PayKeysend, + ) + .await + } + params => { + handle_nwc_params( + params, + req.method, + &event, + &state.multimint_service, + &state.nostr_service, + &state.nwc_config, + ) + .await + } + } +} + +async fn handle_multiple_payments( + items: Vec, + method: Method, + event: &Event, + state: &AppState, + param_constructor: fn(T) -> RequestParams, +) -> Result<(), anyhow::Error> { + for item in items { + let params = param_constructor(item); + let event_clone = event.clone(); + let mm = state.multimint_service.clone(); + let nostr = state.nostr_service.clone(); + let config = state.nwc_config.clone(); + spawn(async move { + handle_nwc_params(params, method, &event_clone, &mm, &nostr, &config).await + }) + .await??; + } + Ok(()) +} + +async fn handle_nwc_params( + params: RequestParams, + method: Method, + event: &Event, + multimint: &MultiMintService, + nostr: &NostrService, + config: &NwcConfig, +) -> Result<(), anyhow::Error> { + let mut d_tag: Option = None; + let content = match params { + RequestParams::PayInvoice(params) => { + d_tag = params.id.map(|id| Tag::identifier(id.clone())); + + let invoice = Bolt11Invoice::from_str(¶ms.invoice) + .map_err(|_| anyhow!("Failed to parse invoice"))?; + let msats = invoice + .amount_milli_satoshis() + .or(params.amount) + .unwrap_or(0); + + let error_msg = if config.max_amount > 0 && msats > config.max_amount * 1_000 { + Some("Invoice amount too high.") + } else if config.daily_limit > 0 + && tracker.lock().await.sum_payments() + msats > config.daily_limit * 1_000 + { + Some("Daily limit exceeded.") + } else { + None + }; + + // verify amount, convert to msats + match error_msg { + None => { + match pay_invoice(invoice, lnd, method).await { + Ok(content) => { + // add payment to tracker + tracker.lock().await.add_payment(msats); + content + } + Err(e) => { + error!("Error paying invoice: {e}"); + + Response { + result_type: method, + error: Some(NIP47Error { + code: ErrorCode::InsufficientBalance, + message: format!("Failed to pay invoice: {e}"), + }), + result: None, + } + } + } + } + Some(err_msg) => Response { + result_type: method, + error: Some(NIP47Error { + code: ErrorCode::QuotaExceeded, + message: err_msg.to_string(), + }), + result: None, + }, + } + } + RequestParams::PayKeysend(params) => { + d_tag = params.id.map(Tag::Identifier); + + let msats = params.amount; + let error_msg = if config.max_amount > 0 && msats > config.max_amount * 1_000 { + Some("Invoice amount too high.") + } else if config.daily_limit > 0 + && tracker.lock().await.sum_payments() + msats > config.daily_limit * 1_000 + { + Some("Daily limit exceeded.") + } else { + None + }; + + // verify amount, convert to msats + match error_msg { + None => { + let pubkey = bitcoin::secp256k1::PublicKey::from_str(¶ms.pubkey)?; + match pay_keysend( + pubkey, + params.preimage, + params.tlv_records, + msats, + lnd, + method, + ) + .await + { + Ok(content) => { + // add payment to tracker + tracker.lock().await.add_payment(msats); + content + } + Err(e) => { + error!("Error paying keysend: {e}"); + + Response { + result_type: method, + error: Some(NIP47Error { + code: ErrorCode::PaymentFailed, + message: format!("Failed to pay keysend: {e}"), + }), + result: None, + } + } + } + } + Some(err_msg) => Response { + result_type: method, + error: Some(NIP47Error { + code: ErrorCode::QuotaExceeded, + message: err_msg.to_string(), + }), + result: None, + }, + } + } + RequestParams::MakeInvoice(params) => { + let description_hash: Vec = match params.description_hash { + None => vec![], + Some(str) => FromHex::from_hex(&str)?, + }; + let inv = Invoice { + memo: params.description.unwrap_or_default(), + description_hash, + value_msat: params.amount as i64, + expiry: params.expiry.unwrap_or(86_400) as i64, + private: config.route_hints, + ..Default::default() + }; + let res = lnd.add_invoice(inv).await?.into_inner(); + + info!("Created invoice: {}", res.payment_request); + + Response { + result_type: Method::MakeInvoice, + error: None, + result: Some(ResponseResult::MakeInvoice(MakeInvoiceResponseResult { + invoice: res.payment_request, + payment_hash: ::hex::encode(res.r_hash), + })), + } + } + RequestParams::LookupInvoice(params) => { + let mut invoice: Option = None; + let payment_hash: Vec = match params.payment_hash { + None => match params.invoice { + None => return Err(anyhow!("Missing payment_hash or invoice")), + Some(bolt11) => { + let inv = Bolt11Invoice::from_str(&bolt11) + .map_err(|_| anyhow!("Failed to parse invoice"))?; + invoice = Some(inv.clone()); + inv.payment_hash().into_32().to_vec() + } + }, + Some(str) => FromHex::from_hex(&str)?, + }; + + let res = lnd + .lookup_invoice(PaymentHash { + r_hash: payment_hash.clone(), + ..Default::default() + }) + .await? + .into_inner(); + + info!("Looked up invoice: {}", res.payment_request); + + let (description, description_hash) = match invoice { + Some(inv) => match inv.description() { + Bolt11InvoiceDescription::Direct(desc) => (Some(desc.to_string()), None), + Bolt11InvoiceDescription::Hash(hash) => (None, Some(hash.0.to_string())), + }, + None => (None, None), + }; + + let preimage = if res.r_preimage.is_empty() { + None + } else { + Some(hex::encode(res.r_preimage)) + }; + + let settled_at = if res.settle_date == 0 { + None + } else { + Some(res.settle_date as u64) + }; + + Response { + result_type: Method::LookupInvoice, + error: None, + result: Some(ResponseResult::LookupInvoice(LookupInvoiceResponseResult { + transaction_type: None, + invoice: Some(res.payment_request), + description, + description_hash, + preimage, + payment_hash: hex::encode(payment_hash), + amount: res.value_msat as u64, + fees_paid: 0, + created_at: res.creation_date as u64, + expires_at: (res.creation_date + res.expiry) as u64, + settled_at, + metadata: Default::default(), + })), + } + } + RequestParams::GetBalance => { + let tracker = tracker.lock().await.sum_payments(); + let remaining_msats = config.daily_limit * 1_000 - tracker; + info!("Current balance: {remaining_msats}msats"); + Response { + result_type: Method::GetBalance, + error: None, + result: Some(ResponseResult::GetBalance(GetBalanceResponseResult { + balance: remaining_msats, + })), + } + } + RequestParams::GetInfo => { + let lnd_info: GetInfoResponse = lnd.get_info(GetInfoRequest {}).await?.into_inner(); + info!("Getting info"); + Response { + result_type: Method::GetBalance, + error: None, + result: Some(ResponseResult::GetInfo(GetInfoResponseResult { + alias: lnd_info.alias, + color: lnd_info.color, + pubkey: lnd_info.identity_pubkey, + network: "".to_string(), + block_height: lnd_info.block_height, + block_hash: lnd_info.block_hash, + methods: METHODS.iter().map(|i| i.to_string()).collect(), + })), + } + } + _ => { + return Err(anyhow!("Command not supported")); + } + }; + + let encrypted = nip04::encrypt( + &keys.server_key.into(), + &keys.user_keys().public_key(), + content.as_json(), + )?; + let p_tag = Tag::public_key(event.pubkey); + let e_tag = Tag::event(event.id); + let tags = match d_tag { + None => vec![p_tag, e_tag], + Some(d_tag) => vec![p_tag, e_tag, d_tag], + }; + let response = EventBuilder::new(Kind::WalletConnectResponse, encrypted, tags) + .to_event(&keys.server_keys())?; + + client.send_event(response).await?; + Ok(()) } diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index 5591837..a64223e 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -8,7 +8,7 @@ use tracing::{debug, error, info}; use crate::config::Cli; use crate::managers::KeyManager; -use crate::nwc::handle_nwc_request; +use crate::nwc::{handle_nwc_request, NwcConfig}; use crate::services::{MultiMintService, NostrService}; #[derive(Debug, Clone)] @@ -17,6 +17,7 @@ pub struct AppState { pub nostr_service: NostrService, pub key_manager: KeyManager, pub active_requests: Arc>>, + pub nwc_config: NwcConfig, } impl AppState { @@ -26,12 +27,17 @@ impl AppState { let nostr_service = NostrService::new(&key_manager, &cli.relays).await?; let active_requests = Arc::new(Mutex::new(BTreeSet::new())); + let nwc_config = NwcConfig { + max_amount: cli.max_amount, + daily_limit: cli.daily_limit, + }; Ok(Self { multimint_service, nostr_service, key_manager, active_requests, + nwc_config, }) } @@ -53,6 +59,8 @@ impl AppState { } } + /// Adds nwc events to active requests set while waiting for them to + /// complete so they can finish processing before a shutdown. pub async fn handle_event(&self, event: Event) { if event.kind == Kind::WalletConnectRequest && event.verify().is_ok() { info!("Received event: {}", event.as_json()); From 2415bb8a250c2119204c01d4dd3923305368e052 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sat, 25 May 2024 15:47:27 -0700 Subject: [PATCH 19/50] feat: payments manager --- fedimint-nwc/src/managers/mod.rs | 2 ++ fedimint-nwc/src/managers/payments.rs | 50 +++++++++++++++++++++++++++ fedimint-nwc/src/nwc.rs | 27 +++++++-------- fedimint-nwc/src/state.rs | 17 ++++----- 4 files changed, 72 insertions(+), 24 deletions(-) create mode 100644 fedimint-nwc/src/managers/payments.rs diff --git a/fedimint-nwc/src/managers/mod.rs b/fedimint-nwc/src/managers/mod.rs index 5f2e9f2..e028e96 100644 --- a/fedimint-nwc/src/managers/mod.rs +++ b/fedimint-nwc/src/managers/mod.rs @@ -1,3 +1,5 @@ pub mod key; +pub mod payments; pub use key::KeyManager; +pub use payments::PaymentsManager; diff --git a/fedimint-nwc/src/managers/payments.rs b/fedimint-nwc/src/managers/payments.rs new file mode 100644 index 0000000..b71f393 --- /dev/null +++ b/fedimint-nwc/src/managers/payments.rs @@ -0,0 +1,50 @@ +use std::collections::VecDeque; +use std::time::{Duration, Instant}; + +const CACHE_DURATION: Duration = Duration::from_secs(86_400); // 1 day + +#[derive(Debug, Clone)] +struct Payment { + time: Instant, + amount: u64, +} + +#[derive(Debug, Clone)] +pub struct PaymentsManager { + pub payments: VecDeque, + pub max_amount: u64, + pub daily_limit: u64, +} + +impl PaymentsManager { + pub fn new(max_amount: u64, daily_limit: u64) -> Self { + PaymentsManager { + payments: VecDeque::new(), + max_amount, + daily_limit, + } + } + + pub fn add_payment(&mut self, amount: u64) { + let now = Instant::now(); + let payment = Payment { time: now, amount }; + + self.payments.push_back(payment); + } + + fn clean_old_payments(&mut self) { + let now = Instant::now(); + while let Some(payment) = self.payments.front() { + if now.duration_since(payment.time) < CACHE_DURATION { + break; + } + + self.payments.pop_front(); + } + } + + pub fn sum_payments(&mut self) -> u64 { + self.clean_old_payments(); + self.payments.iter().map(|p| p.amount).sum() + } +} diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index ceea9e4..5d17242 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -3,12 +3,13 @@ use std::str::FromStr; use anyhow::{anyhow, Result}; use lightning_invoice::Bolt11Invoice; use nostr::nips::nip04; -use nostr::nips::nip47::{Method, Request, RequestParams}; +use nostr::nips::nip47::{ErrorCode, Method, NIP47Error, Request, RequestParams, Response}; use nostr::Tag; use nostr_sdk::{Event, JsonUtil}; use tokio::spawn; -use tracing::info; +use tracing::{error, info}; +use crate::managers::PaymentsManager; use crate::services::{MultiMintService, NostrService}; use crate::state::AppState; @@ -64,7 +65,7 @@ pub async fn handle_nwc_request(state: &AppState, event: Event) -> Result<(), an &event, &state.multimint_service, &state.nostr_service, - &state.nwc_config, + &state.payments_manager, ) .await } @@ -83,10 +84,10 @@ async fn handle_multiple_payments( let event_clone = event.clone(); let mm = state.multimint_service.clone(); let nostr = state.nostr_service.clone(); - let config = state.nwc_config.clone(); - spawn(async move { - handle_nwc_params(params, method, &event_clone, &mm, &nostr, &config).await - }) + let pm = state.payments_manager.clone(); + spawn( + async move { handle_nwc_params(params, method, &event_clone, &mm, &nostr, &pm).await }, + ) .await??; } Ok(()) @@ -98,7 +99,7 @@ async fn handle_nwc_params( event: &Event, multimint: &MultiMintService, nostr: &NostrService, - config: &NwcConfig, + pm: &PaymentsManager, ) -> Result<(), anyhow::Error> { let mut d_tag: Option = None; let content = match params { @@ -112,11 +113,9 @@ async fn handle_nwc_params( .or(params.amount) .unwrap_or(0); - let error_msg = if config.max_amount > 0 && msats > config.max_amount * 1_000 { + let error_msg = if pm.max_amount > 0 && msats > pm.max_amount * 1_000 { Some("Invoice amount too high.") - } else if config.daily_limit > 0 - && tracker.lock().await.sum_payments() + msats > config.daily_limit * 1_000 - { + } else if pm.daily_limit > 0 && pm.sum_payments() + msats > pm.daily_limit * 1_000 { Some("Daily limit exceeded.") } else { None @@ -125,10 +124,10 @@ async fn handle_nwc_params( // verify amount, convert to msats match error_msg { None => { - match pay_invoice(invoice, lnd, method).await { + match pay_invoice(invoice, method, mm).await { Ok(content) => { // add payment to tracker - tracker.lock().await.add_payment(msats); + pm.add_payment(msats); content } Err(e) => { diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index a64223e..135ba8f 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -7,17 +7,17 @@ use tokio::sync::Mutex; use tracing::{debug, error, info}; use crate::config::Cli; -use crate::managers::KeyManager; -use crate::nwc::{handle_nwc_request, NwcConfig}; +use crate::managers::{KeyManager, PaymentsManager}; +use crate::nwc::handle_nwc_request; use crate::services::{MultiMintService, NostrService}; #[derive(Debug, Clone)] pub struct AppState { + pub active_requests: Arc>>, pub multimint_service: MultiMintService, pub nostr_service: NostrService, pub key_manager: KeyManager, - pub active_requests: Arc>>, - pub nwc_config: NwcConfig, + pub payments_manager: PaymentsManager, } impl AppState { @@ -27,17 +27,14 @@ impl AppState { let nostr_service = NostrService::new(&key_manager, &cli.relays).await?; let active_requests = Arc::new(Mutex::new(BTreeSet::new())); - let nwc_config = NwcConfig { - max_amount: cli.max_amount, - daily_limit: cli.daily_limit, - }; + let payments_manager = PaymentsManager::new(cli.max_amount, cli.daily_limit); Ok(Self { + active_requests, multimint_service, nostr_service, key_manager, - active_requests, - nwc_config, + payments_manager, }) } From 2527a36614f4429a9d6a873cffaceb71711421c5 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 09:09:05 -0700 Subject: [PATCH 20/50] refactor: better payments management --- fedimint-nwc/src/config.rs | 3 ++ fedimint-nwc/src/managers/payments.rs | 54 +++++++++++++++++++++++---- fedimint-nwc/src/nwc.rs | 8 +--- fedimint-nwc/src/state.rs | 3 +- 4 files changed, 52 insertions(+), 16 deletions(-) diff --git a/fedimint-nwc/src/config.rs b/fedimint-nwc/src/config.rs index 84baf61..23ec136 100644 --- a/fedimint-nwc/src/config.rs +++ b/fedimint-nwc/src/config.rs @@ -32,4 +32,7 @@ pub struct Cli { /// Max payment amount per day, in satoshis #[clap(long, env = "FEDIMINT_NWC_DAILY_LIMIT", default_value_t = 100_000)] pub daily_limit: u64, + /// Rate limit for payments, in seconds + #[clap(long, env = "FEDIMINT_NWC_RATE_LIMIT_SECS", default_value_t = 86_400)] + pub rate_limit_secs: u64, } diff --git a/fedimint-nwc/src/managers/payments.rs b/fedimint-nwc/src/managers/payments.rs index b71f393..5554c59 100644 --- a/fedimint-nwc/src/managers/payments.rs +++ b/fedimint-nwc/src/managers/payments.rs @@ -1,4 +1,4 @@ -use std::collections::VecDeque; +use std::collections::{HashMap, VecDeque}; use std::time::{Duration, Instant}; const CACHE_DURATION: Duration = Duration::from_secs(86_400); // 1 day @@ -7,29 +7,53 @@ const CACHE_DURATION: Duration = Duration::from_secs(86_400); // 1 day struct Payment { time: Instant, amount: u64, + destination: String, // Assuming destination is a string identifier } #[derive(Debug, Clone)] pub struct PaymentsManager { - pub payments: VecDeque, - pub max_amount: u64, - pub daily_limit: u64, + payments: VecDeque, + max_amount: u64, + daily_limit: u64, + rate_limit: Duration, // New: Limit for frequency of payments + max_destination_amounts: HashMap, // New: Max amounts per destination } impl PaymentsManager { - pub fn new(max_amount: u64, daily_limit: u64) -> Self { + pub fn new(max_amount: u64, daily_limit: u64, rate_limit_secs: u64) -> Self { PaymentsManager { payments: VecDeque::new(), max_amount, daily_limit, + rate_limit: Duration::from_secs(rate_limit_secs), + max_destination_amounts: HashMap::new(), } } - pub fn add_payment(&mut self, amount: u64) { + pub fn add_payment(&mut self, amount: u64, destination: String) -> Result<(), String> { let now = Instant::now(); - let payment = Payment { time: now, amount }; + // Check rate limit + if let Some(last_payment) = self.payments.back() { + if now.duration_since(last_payment.time) < self.rate_limit { + return Err("Rate limit exceeded.".to_string()); + } + } + + // Check max amount per destination + if let Some(&max_amount) = self.max_destination_amounts.get(&destination) { + if amount > max_amount { + return Err("Destination max amount exceeded.".to_string()); + } + } + + let payment = Payment { + time: now, + amount, + destination, + }; self.payments.push_back(payment); + Ok(()) } fn clean_old_payments(&mut self) { @@ -38,7 +62,6 @@ impl PaymentsManager { if now.duration_since(payment.time) < CACHE_DURATION { break; } - self.payments.pop_front(); } } @@ -47,4 +70,19 @@ impl PaymentsManager { self.clean_old_payments(); self.payments.iter().map(|p| p.amount).sum() } + + pub fn check_payment_limits(&mut self, msats: u64) -> Option { + if self.max_amount > 0 && msats > self.max_amount * 1_000 { + Some("Invoice amount too high.".to_string()) + } else if self.daily_limit > 0 && self.sum_payments() + msats > self.daily_limit * 1_000 { + Some("Daily limit exceeded.".to_string()) + } else { + None + } + } + + // New: Set maximum amount for a specific destination + pub fn set_max_amount_for_destination(&mut self, destination: String, max_amount: u64) { + self.max_destination_amounts.insert(destination, max_amount); + } } diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 5d17242..1ddbc3d 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -113,13 +113,7 @@ async fn handle_nwc_params( .or(params.amount) .unwrap_or(0); - let error_msg = if pm.max_amount > 0 && msats > pm.max_amount * 1_000 { - Some("Invoice amount too high.") - } else if pm.daily_limit > 0 && pm.sum_payments() + msats > pm.daily_limit * 1_000 { - Some("Daily limit exceeded.") - } else { - None - }; + let error_msg = pm.check_payment_limits(msats); // verify amount, convert to msats match error_msg { diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index 135ba8f..0ad52c3 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -27,7 +27,8 @@ impl AppState { let nostr_service = NostrService::new(&key_manager, &cli.relays).await?; let active_requests = Arc::new(Mutex::new(BTreeSet::new())); - let payments_manager = PaymentsManager::new(cli.max_amount, cli.daily_limit); + let payments_manager = + PaymentsManager::new(cli.max_amount, cli.daily_limit, cli.rate_limit_secs); Ok(Self { active_requests, From 3ac1161d3bff4ada7704b1875b65af2dec440c05 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 09:17:45 -0700 Subject: [PATCH 21/50] refactor: keys in nostr service --- fedimint-nwc/src/main.rs | 12 ++--- fedimint-nwc/src/managers/key.rs | 64 ---------------------- fedimint-nwc/src/managers/mod.rs | 2 - fedimint-nwc/src/nwc.rs | 2 +- fedimint-nwc/src/services/nostr.rs | 87 ++++++++++++++++++++++++++---- fedimint-nwc/src/state.rs | 7 +-- 6 files changed, 87 insertions(+), 87 deletions(-) delete mode 100644 fedimint-nwc/src/managers/key.rs diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index b61d531..b4f055d 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -14,6 +14,8 @@ use state::AppState; use crate::config::Cli; +/// Fedimint Nostr Wallet Connect +/// A nostr wallet connect implementation on top of a multimint client #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt::init(); @@ -22,12 +24,9 @@ async fn main() -> Result<()> { let cli = Cli::parse(); let state = AppState::new(cli).await?; - // Connect to the relay pool and broadcast the info event on startup + // Connect to the relay pool and broadcast the nwc info event on startup state.nostr_service.connect().await; - state - .nostr_service - .broadcast_info_event(&state.key_manager) - .await?; + state.nostr_service.broadcast_info_event().await?; // Start the event loop event_loop(state.clone()).await?; @@ -55,8 +54,9 @@ async fn event_loop(state: AppState) -> Result<()> { match notification { Ok(notification) => match notification { RelayPoolNotification::Event { event, .. } => { + // Only handle nwc events if event.kind == Kind::WalletConnectRequest - && event.pubkey == state.key_manager.user_keys().public_key() + && event.pubkey == state.nostr_service.user_keys().public_key() && event.verify().is_ok() { info!("Received event: {}", event.as_json()); state.handle_event(*event).await diff --git a/fedimint-nwc/src/managers/key.rs b/fedimint-nwc/src/managers/key.rs deleted file mode 100644 index 6945edb..0000000 --- a/fedimint-nwc/src/managers/key.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::fs::{create_dir_all, File}; -use std::io::{BufReader, Write}; -use std::path::Path; - -use anyhow::{Context, Result}; -use nostr_sdk::secp256k1::SecretKey; -use nostr_sdk::Keys; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct KeyManager { - server_key: SecretKey, - user_key: SecretKey, - #[serde(default)] - pub sent_info: bool, -} - -impl KeyManager { - pub fn new(keys_file: &str) -> Result { - let path = Path::new(keys_file); - match File::open(path) { - Ok(file) => { - let reader = BufReader::new(file); - serde_json::from_reader(reader).context("Failed to parse JSON") - } - Err(_) => { - let keys = Self::generate()?; - Self::write_keys(&keys, path)?; - Ok(keys) - } - } - } - - fn generate() -> Result { - let server_keys = Keys::generate(); - let server_key = server_keys.secret_key()?; - let user_keys = Keys::generate(); - let user_key = user_keys.secret_key()?; - Ok(Self { - server_key: **server_key, - user_key: **user_key, - sent_info: false, - }) - } - - fn write_keys(keys: &Self, path: &Path) -> Result<()> { - let json_str = serde_json::to_string(keys).context("Failed to serialize data")?; - if let Some(parent) = path.parent() { - create_dir_all(parent).context("Failed to create directory")?; - } - let mut file = File::create(path).context("Failed to create file")?; - file.write_all(json_str.as_bytes()) - .context("Failed to write to file")?; - Ok(()) - } - - pub fn server_keys(&self) -> Keys { - Keys::new(self.server_key.into()) - } - - pub fn user_keys(&self) -> Keys { - Keys::new(self.user_key.into()) - } -} diff --git a/fedimint-nwc/src/managers/mod.rs b/fedimint-nwc/src/managers/mod.rs index e028e96..0460cb8 100644 --- a/fedimint-nwc/src/managers/mod.rs +++ b/fedimint-nwc/src/managers/mod.rs @@ -1,5 +1,3 @@ -pub mod key; pub mod payments; -pub use key::KeyManager; pub use payments::PaymentsManager; diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 1ddbc3d..1e0cf28 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -31,7 +31,7 @@ pub struct NwcConfig { } pub async fn handle_nwc_request(state: &AppState, event: Event) -> Result<(), anyhow::Error> { - let user_keys = state.key_manager.user_keys(); + let user_keys = state.nostr_service.user_keys(); let decrypted = nip04::decrypt(user_keys.secret_key()?, &event.pubkey, &event.content)?; let req: Request = Request::from_json(&decrypted)?; diff --git a/fedimint-nwc/src/services/nostr.rs b/fedimint-nwc/src/services/nostr.rs index 186e6b4..74604ad 100644 --- a/fedimint-nwc/src/services/nostr.rs +++ b/fedimint-nwc/src/services/nostr.rs @@ -1,21 +1,84 @@ -use anyhow::Result; -use nostr_sdk::{Client, EventBuilder, JsonUtil, Kind, RelayPoolNotification}; +use std::fs::{create_dir_all, File}; +use std::io::{BufReader, Write}; +use std::path::Path; + +use anyhow::{Context, Result}; +use nostr_sdk::secp256k1::SecretKey; +use nostr_sdk::{Client, Event, EventBuilder, JsonUtil, Keys, Kind, RelayPoolNotification}; +use serde::{Deserialize, Serialize}; use tokio::sync::broadcast::Receiver; use tracing::info; -use crate::managers::key::KeyManager; use crate::nwc::METHODS; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct NostrService { + #[serde(skip)] client: Client, + server_key: SecretKey, + user_key: SecretKey, + #[serde(default)] + pub sent_info: bool, } impl NostrService { - pub async fn new(key_manager: &KeyManager, relays: &str) -> Result { - let client = Client::new(&key_manager.server_keys()); + pub async fn new(keys_file: &str, relays: &str) -> Result { + let path = Path::new(keys_file); + let (server_key, user_key) = match File::open(path) { + Ok(file) => { + let reader = BufReader::new(file); + let keys: Self = serde_json::from_reader(reader).context("Failed to parse JSON")?; + (keys.server_key, keys.user_key) + } + Err(_) => { + let (server_key, user_key) = Self::generate_keys()?; + Self::write_keys(server_key, user_key, path)?; + (server_key, user_key) + } + }; + + let client = Client::new(&Keys::new(server_key.into())); Self::add_relays(&client, relays).await?; - Ok(Self { client }) + Ok(Self { + client, + server_key, + user_key, + sent_info: false, + }) + } + + fn generate_keys() -> Result<(SecretKey, SecretKey)> { + let server_keys = Keys::generate(); + let server_key = server_keys.secret_key()?; + let user_keys = Keys::generate(); + let user_key = user_keys.secret_key()?; + Ok((**server_key, **user_key)) + } + + fn write_keys(server_key: SecretKey, user_key: SecretKey, path: &Path) -> Result<()> { + let keys = Self { + server_key, + user_key, + sent_info: false, + client: Client::new(&Keys::new(server_key.into())), /* Dummy client for struct + * initialization */ + }; + let json_str = serde_json::to_string(&keys).context("Failed to serialize data")?; + if let Some(parent) = path.parent() { + create_dir_all(parent).context("Failed to create directory")?; + } + let mut file = File::create(path).context("Failed to create file")?; + file.write_all(json_str.as_bytes()) + .context("Failed to write to file")?; + Ok(()) + } + + pub fn server_keys(&self) -> Keys { + Keys::new(self.server_key.into()) + } + + pub fn user_keys(&self) -> Keys { + Keys::new(self.user_key.into()) } async fn add_relays(client: &Client, relays: &str) -> Result<()> { @@ -32,14 +95,14 @@ impl NostrService { Ok(()) } - pub async fn broadcast_info_event(&self, keys: &KeyManager) -> Result<(), anyhow::Error> { + pub async fn broadcast_info_event(&self) -> Result<(), anyhow::Error> { let content = METHODS .iter() .map(ToString::to_string) .collect::>() .join(" "); let info = EventBuilder::new(Kind::WalletConnectInfo, content, []) - .to_event(&keys.server_keys())?; + .to_event(&self.server_keys())?; info!("Broadcasting info event: {}", info.as_json()); let event_id = self.client.send_event(info).await?; info!("Broadcasted info event: {}", event_id); @@ -60,4 +123,10 @@ impl NostrService { pub fn notifications(&self) -> Receiver { self.client.notifications() } + + pub fn is_nwc_event(&self, event: &Event) -> bool { + event.kind == Kind::WalletConnectRequest + && event.verify().is_ok() + && event.pubkey == self.user_keys().public_key() + } } diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index 0ad52c3..7fcc57d 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -7,7 +7,7 @@ use tokio::sync::Mutex; use tracing::{debug, error, info}; use crate::config::Cli; -use crate::managers::{KeyManager, PaymentsManager}; +use crate::managers::PaymentsManager; use crate::nwc::handle_nwc_request; use crate::services::{MultiMintService, NostrService}; @@ -16,15 +16,13 @@ pub struct AppState { pub active_requests: Arc>>, pub multimint_service: MultiMintService, pub nostr_service: NostrService, - pub key_manager: KeyManager, pub payments_manager: PaymentsManager, } impl AppState { pub async fn new(cli: Cli) -> Result { - let key_manager = KeyManager::new(&cli.keys_file)?; let multimint_service = MultiMintService::new(cli.db_path).await?; - let nostr_service = NostrService::new(&key_manager, &cli.relays).await?; + let nostr_service = NostrService::new(&cli.keys_file, &cli.relays).await?; let active_requests = Arc::new(Mutex::new(BTreeSet::new())); let payments_manager = @@ -34,7 +32,6 @@ impl AppState { active_requests, multimint_service, nostr_service, - key_manager, payments_manager, }) } From ff8dd18870d6d4620ff0110c463bdbe8d9812693 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 10:35:23 -0700 Subject: [PATCH 22/50] feat: pay invoice --- Cargo.lock | 52 ++--- Cargo.toml | 1 + fedimint-clientd/Cargo.toml | 1 + .../src/router/handlers/ln/mod.rs | 6 +- .../src/router/handlers/ln/pay.rs | 1 + fedimint-nwc/Cargo.toml | 7 +- fedimint-nwc/src/managers/payments.rs | 6 +- fedimint-nwc/src/nwc.rs | 2 +- fedimint-nwc/src/services/multimint.rs | 206 +++++++++++++++++- fedimint-nwc/src/state.rs | 6 +- multimint/Cargo.toml | 3 +- multimint/src/lib.rs | 2 +- 12 files changed, 242 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef6477f..2a07a16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1241,11 +1241,12 @@ dependencies = [ "dotenv", "fedimint", "futures-util", + "hex", "itertools 0.12.1", "lazy_static", - "lightning-invoice 0.26.0", + "lightning-invoice", "lnurl-rs", - "multimint 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "multimint 0.3.6", "reqwest 0.12.4", "serde", "serde_json", @@ -1292,8 +1293,8 @@ dependencies = [ "jsonrpsee-types", "jsonrpsee-wasm-client", "jsonrpsee-ws-client", - "lightning 0.0.118", - "lightning-invoice 0.26.0", + "lightning", + "lightning-invoice", "lru", "macro_rules_attribute", "miniscript 10.0.0", @@ -1368,7 +1369,7 @@ dependencies = [ "fedimint-threshold-crypto", "futures", "itertools 0.12.1", - "lightning-invoice 0.26.0", + "lightning-invoice", "rand", "reqwest 0.11.27", "secp256k1 0.24.3", @@ -1400,8 +1401,8 @@ dependencies = [ "fedimint-threshold-crypto", "futures", "itertools 0.12.1", - "lightning 0.0.118", - "lightning-invoice 0.26.0", + "lightning", + "lightning-invoice", "rand", "secp256k1 0.24.3", "serde", @@ -1492,8 +1493,9 @@ dependencies = [ "anyhow", "clap 4.5.4", "dotenv", - "lightning-invoice 0.31.0", - "multimint 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-util", + "lightning-invoice", + "multimint 0.3.7", "nostr", "nostr-sdk", "serde", @@ -2501,16 +2503,6 @@ dependencies = [ "hashbrown 0.8.2", ] -[[package]] -name = "lightning" -version = "0.0.123" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd92d4aa159374be430c7590e169b4a6c0fb79018f5bc4ea1bffde536384db3" -dependencies = [ - "bitcoin 0.30.2", - "hex-conservative", -] - [[package]] name = "lightning-invoice" version = "0.26.0" @@ -2520,25 +2512,12 @@ dependencies = [ "bech32 0.9.1", "bitcoin 0.29.2", "bitcoin_hashes 0.11.0", - "lightning 0.0.118", + "lightning", "num-traits", "secp256k1 0.24.3", "serde", ] -[[package]] -name = "lightning-invoice" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d07d01cf197bf2184b929b7dc94aa70d935aac6df896c256a3a9475b7e9d40" -dependencies = [ - "bech32 0.9.1", - "bitcoin 0.30.2", - "lightning 0.0.123", - "secp256k1 0.27.0", - "serde", -] - [[package]] name = "lnurl-pay" version = "0.5.0" @@ -2697,6 +2676,8 @@ dependencies = [ [[package]] name = "multimint" version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9989e73f689a15421660f72b902165b5e4579d1cb0d2f208108ea374b0bbf2d7" dependencies = [ "anyhow", "fedimint-client", @@ -2716,14 +2697,13 @@ dependencies = [ [[package]] name = "multimint" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9989e73f689a15421660f72b902165b5e4579d1cb0d2f208108ea374b0bbf2d7" +version = "0.3.7" dependencies = [ "anyhow", "fedimint-client", "fedimint-core", "fedimint-ln-client", + "fedimint-ln-common", "fedimint-mint-client", "fedimint-rocksdb", "fedimint-wallet-client", diff --git a/Cargo.toml b/Cargo.toml index 71cc265..3b8de8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ fedimint-core = "0.3.1" fedimint-wallet-client = "0.3.1" fedimint-mint-client = "0.3.1" fedimint-ln-client = "0.3.1" +fedimint-ln-common = "0.3.1" fedimint-rocksdb = "0.3.1" # Config for 'cargo dist' diff --git a/fedimint-clientd/Cargo.toml b/fedimint-clientd/Cargo.toml index 40607bf..2bc2c40 100644 --- a/fedimint-clientd/Cargo.toml +++ b/fedimint-clientd/Cargo.toml @@ -40,3 +40,4 @@ clap = { version = "3", features = ["derive", "env"] } multimint = { version = "0.3.6" } # multimint = { path = "../multimint" } axum-otel-metrics = "0.8.0" +hex = "0.4.3" diff --git a/fedimint-clientd/src/router/handlers/ln/mod.rs b/fedimint-clientd/src/router/handlers/ln/mod.rs index ad2a2cf..8b6cadd 100644 --- a/fedimint-clientd/src/router/handlers/ln/mod.rs +++ b/fedimint-clientd/src/router/handlers/ln/mod.rs @@ -80,12 +80,13 @@ pub async fn wait_for_ln_payment( while let Some(update) = updates.next().await { match update { - InternalPayState::Preimage(_preimage) => { + InternalPayState::Preimage(preimage) => { return Ok(Some(LnPayResponse { operation_id, payment_type, contract_id, fee: Amount::ZERO, + preimage: hex::encode(preimage.0), })); } InternalPayState::RefundSuccess { out_points, error } => { @@ -120,12 +121,13 @@ pub async fn wait_for_ln_payment( while let Some(update) = updates.next().await { let update_clone = update.clone(); match update_clone { - LnPayState::Success { preimage: _ } => { + LnPayState::Success { preimage } => { return Ok(Some(LnPayResponse { operation_id, payment_type, contract_id, fee: Amount::ZERO, + preimage, })); } LnPayState::Refunded { gateway_error } => { diff --git a/fedimint-clientd/src/router/handlers/ln/pay.rs b/fedimint-clientd/src/router/handlers/ln/pay.rs index 30ed038..2da88c8 100644 --- a/fedimint-clientd/src/router/handlers/ln/pay.rs +++ b/fedimint-clientd/src/router/handlers/ln/pay.rs @@ -33,6 +33,7 @@ pub struct LnPayResponse { pub payment_type: PayType, pub contract_id: String, pub fee: Amount, + pub preimage: String, } async fn _pay(client: ClientHandleArc, req: LnPayRequest) -> Result { diff --git a/fedimint-nwc/Cargo.toml b/fedimint-nwc/Cargo.toml index d588d0a..b3d3ff5 100644 --- a/fedimint-nwc/Cargo.toml +++ b/fedimint-nwc/Cargo.toml @@ -12,9 +12,10 @@ authors.workspace = true anyhow = "1.0.75" clap = { version = "4.5.4", features = ["derive", "env"] } dotenv = "0.15.0" -lightning-invoice = { version = "0.31.0", features = ["serde"] } -multimint = { version = "0.3.6" } -# multimint = { path = "../multimint" } +futures-util = "0.3.30" +lightning-invoice = { version = "0.26.0", features = ["serde"] } +# multimint = { version = "0.3.6" } +multimint = { path = "../multimint" } nostr = { version = "0.31.2", features = ["nip47"] } nostr-sdk = { version = "0.31.0", features = ["nip47"] } serde = "1.0.193" diff --git a/fedimint-nwc/src/managers/payments.rs b/fedimint-nwc/src/managers/payments.rs index 5554c59..b82ee30 100644 --- a/fedimint-nwc/src/managers/payments.rs +++ b/fedimint-nwc/src/managers/payments.rs @@ -7,7 +7,7 @@ const CACHE_DURATION: Duration = Duration::from_secs(86_400); // 1 day struct Payment { time: Instant, amount: u64, - destination: String, // Assuming destination is a string identifier + destination: String, } #[derive(Debug, Clone)] @@ -15,8 +15,8 @@ pub struct PaymentsManager { payments: VecDeque, max_amount: u64, daily_limit: u64, - rate_limit: Duration, // New: Limit for frequency of payments - max_destination_amounts: HashMap, // New: Max amounts per destination + rate_limit: Duration, + max_destination_amounts: HashMap, } impl PaymentsManager { diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 1e0cf28..c670502 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -118,7 +118,7 @@ async fn handle_nwc_params( // verify amount, convert to msats match error_msg { None => { - match pay_invoice(invoice, method, mm).await { + match multimint.pay_invoice(invoice, method).await { Ok(content) => { // add payment to tracker pm.add_payment(msats); diff --git a/fedimint-nwc/src/services/multimint.rs b/fedimint-nwc/src/services/multimint.rs index 6ac77de..1b838d8 100644 --- a/fedimint-nwc/src/services/multimint.rs +++ b/fedimint-nwc/src/services/multimint.rs @@ -1,20 +1,42 @@ use std::path::PathBuf; use std::str::FromStr; -use anyhow::Result; +use anyhow::{anyhow, bail, Result}; +use futures_util::StreamExt; +use lightning_invoice::Bolt11Invoice; +use multimint::fedimint_client::ClientHandleArc; use multimint::fedimint_core::api::InviteCode; +use multimint::fedimint_core::config::{FederationId, FederationIdPrefix}; +use multimint::fedimint_core::core::OperationId; +use multimint::fedimint_core::Amount; +use multimint::fedimint_ln_client::{ + InternalPayState, LightningClientModule, LnPayState, OutgoingLightningPayment, PayType, +}; +use multimint::fedimint_ln_common::LightningGateway; use multimint::MultiMint; +use nostr::nips::nip47::{ + ErrorCode, Method, NIP47Error, PayInvoiceResponseResult, Response, ResponseResult, +}; +use nostr::util::hex; +use tracing::info; #[derive(Debug, Clone)] pub struct MultiMintService { multimint: MultiMint, + default_federation_id: Option, } impl MultiMintService { - pub async fn new(db_path: PathBuf) -> Result { + pub async fn new( + db_path: PathBuf, + default_federation_id: Option, + ) -> Result { let clients = MultiMint::new(db_path).await?; clients.update_gateway_caches().await?; - Ok(Self { multimint: clients }) + Ok(Self { + multimint: clients, + default_federation_id, + }) } pub async fn init_multimint( @@ -37,4 +59,182 @@ impl MultiMintService { } } } + + // Helper function to get a specific client from the state or default + pub async fn get_client( + &self, + federation_id: Option, + ) -> Result { + let federation_id = match federation_id { + Some(id) => id, + None => match self.default_federation_id.clone() { + Some(id) => id, + None => return Err(anyhow!("No default federation id set")), + }, + }; + match self.multimint.get(&federation_id).await { + Some(client) => Ok(client), + None => Err(anyhow!("No client found for federation id")), + } + } + + pub async fn get_client_by_prefix( + &self, + federation_id_prefix: &FederationIdPrefix, + ) -> Result { + match self.multimint.get_by_prefix(federation_id_prefix).await { + Some(client) => Ok(client), + None => Err(anyhow!("No client found for federation id prefix")), + } + } + + // Helper method to select a gateway + async fn get_gateway(&self, client: &ClientHandleArc) -> Result { + let lightning_module = client.get_first_module::(); + let gateways = lightning_module.list_gateways().await; + + let selected_gateway = gateways + .first() + .ok_or_else(|| anyhow!("No gateways available"))? + .info + .clone(); + + Ok(selected_gateway) + } + + pub async fn pay_invoice(&self, invoice: Bolt11Invoice, method: Method) -> Result { + let client = self.get_client(None).await?; + let gateway = self.get_gateway(&client).await?; + info!("Paying invoice: {invoice:?}"); + let lightning_module = client.get_first_module::(); + let payment = lightning_module + .pay_bolt11_invoice(Some(gateway), invoice, ()) + .await?; + + let response = wait_for_ln_payment(&client, payment, false).await?; + + let response = match response { + Some(ln_response) => { + info!("Paid invoice: {}", ln_response.contract_id); + let preimage = hex::encode(ln_response.preimage); + Response { + result_type: method, + error: None, + result: Some(ResponseResult::PayInvoice(PayInvoiceResponseResult { + preimage, + })), + } + } + None => { + let error_msg = "Payment failed".to_string(); + Response { + result_type: method, + error: Some(NIP47Error { + code: ErrorCode::PaymentFailed, + message: error_msg, + }), + result: None, + } + } + }; + + Ok(response) + } +} + +#[derive(Debug, Clone)] +pub struct LnPayResponse { + pub operation_id: OperationId, + pub payment_type: PayType, + pub contract_id: String, + pub fee: Amount, + pub preimage: String, +} + +pub async fn wait_for_ln_payment( + client: &ClientHandleArc, + payment: OutgoingLightningPayment, + return_on_funding: bool, +) -> anyhow::Result> { + let lightning_module = client.get_first_module::(); + match payment.payment_type { + PayType::Internal(operation_id) => { + let mut updates = lightning_module + .subscribe_internal_pay(operation_id) + .await? + .into_stream(); + + while let Some(update) = updates.next().await { + match update { + InternalPayState::Preimage(preimage) => { + return Ok(Some(LnPayResponse { + operation_id, + payment_type: payment.payment_type, + contract_id: payment.contract_id.to_string(), + fee: Amount::ZERO, + preimage: hex::encode(preimage.0), + })); + } + InternalPayState::RefundSuccess { out_points, error } => { + let e = format!( + "Internal payment failed. A refund was issued to {:?} Error: {error}", + out_points + ); + bail!("{e}"); + } + InternalPayState::UnexpectedError(e) => { + bail!("{e}"); + } + InternalPayState::Funding if return_on_funding => return Ok(None), + InternalPayState::Funding => {} + InternalPayState::RefundError { + error_message, + error, + } => bail!("RefundError: {error_message} {error}"), + InternalPayState::FundingFailed { error } => { + bail!("FundingFailed: {error}") + } + } + info!("Update: {update:?}"); + } + } + PayType::Lightning(operation_id) => { + let mut updates = lightning_module + .subscribe_ln_pay(operation_id) + .await? + .into_stream(); + + while let Some(update) = updates.next().await { + let update_clone = update.clone(); + match update_clone { + LnPayState::Success { preimage } => { + return Ok(Some(LnPayResponse { + operation_id, + payment_type: payment.payment_type, + contract_id: payment.contract_id.to_string(), + fee: Amount::ZERO, + preimage, + })); + } + LnPayState::Refunded { gateway_error } => { + info!("{gateway_error}"); + Err(anyhow::anyhow!("Payment was refunded"))?; + } + LnPayState::Canceled => { + Err(anyhow::anyhow!("Payment was canceled"))?; + } + LnPayState::Created + | LnPayState::AwaitingChange + | LnPayState::WaitingForRefund { .. } => {} + LnPayState::Funded if return_on_funding => return Ok(None), + LnPayState::Funded => {} + LnPayState::UnexpectedError { error_message } => { + bail!("UnexpectedError: {error_message}") + } + } + info!("Update: {update:?}"); + } + } + }; + bail!("Lightning Payment failed") } diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index 7fcc57d..6937f2e 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -1,7 +1,9 @@ use std::collections::BTreeSet; +use std::str::FromStr; use std::sync::Arc; use std::time::Duration; +use multimint::fedimint_core::api::InviteCode; use nostr_sdk::{Event, EventId, JsonUtil, Kind}; use tokio::sync::Mutex; use tracing::{debug, error, info}; @@ -21,7 +23,9 @@ pub struct AppState { impl AppState { pub async fn new(cli: Cli) -> Result { - let multimint_service = MultiMintService::new(cli.db_path).await?; + let invite_code = InviteCode::from_str(&cli.invite_code)?; + let multimint_service = + MultiMintService::new(cli.db_path, Some(invite_code.federation_id())).await?; let nostr_service = NostrService::new(&cli.keys_file, &cli.relays).await?; let active_requests = Arc::new(Mutex::new(BTreeSet::new())); diff --git a/multimint/Cargo.toml b/multimint/Cargo.toml index cd721f8..8cf5efa 100644 --- a/multimint/Cargo.toml +++ b/multimint/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "multimint" description = "A library for managing fedimint clients across multiple federations" -version = "0.3.6" +version = "0.3.7" edition.workspace = true repository.workspace = true keywords.workspace = true @@ -22,6 +22,7 @@ fedimint-core = { workspace = true } fedimint-wallet-client = { workspace = true } fedimint-mint-client = { workspace = true } fedimint-ln-client = { workspace = true } +fedimint-ln-common = { workspace = true } fedimint-rocksdb = { workspace = true } futures-util = "0.3.30" rand = "0.8.5" diff --git a/multimint/src/lib.rs b/multimint/src/lib.rs index 87e4748..4b8d986 100644 --- a/multimint/src/lib.rs +++ b/multimint/src/lib.rs @@ -80,7 +80,7 @@ use tracing::warn; use types::InfoResponse; // Reexport all the fedimint crates for ease of use pub use { - fedimint_client, fedimint_core, fedimint_ln_client, fedimint_mint_client, + fedimint_client, fedimint_core, fedimint_ln_client, fedimint_ln_common, fedimint_mint_client, fedimint_wallet_client, }; From 287557caee297cd199f3bd8317292c2c8af384b6 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 10:42:11 -0700 Subject: [PATCH 23/50] feat: start keysend --- fedimint-nwc/src/managers/payments.rs | 4 ++- fedimint-nwc/src/nwc.rs | 44 +++++++++++++-------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/fedimint-nwc/src/managers/payments.rs b/fedimint-nwc/src/managers/payments.rs index b82ee30..b248e56 100644 --- a/fedimint-nwc/src/managers/payments.rs +++ b/fedimint-nwc/src/managers/payments.rs @@ -71,11 +71,13 @@ impl PaymentsManager { self.payments.iter().map(|p| p.amount).sum() } - pub fn check_payment_limits(&mut self, msats: u64) -> Option { + pub fn check_payment_limits(&mut self, msats: u64, dest: String) -> Option { if self.max_amount > 0 && msats > self.max_amount * 1_000 { Some("Invoice amount too high.".to_string()) } else if self.daily_limit > 0 && self.sum_payments() + msats > self.daily_limit * 1_000 { Some("Daily limit exceeded.".to_string()) + } else if self.max_destination_amounts.get(&dest).is_some() { + Some("Destination max amount exceeded.".to_string()) } else { None } diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index c670502..05a5c74 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -112,8 +112,13 @@ async fn handle_nwc_params( .amount_milli_satoshis() .or(params.amount) .unwrap_or(0); + let dest = match invoice.payee_pub_key() { + Some(dest) => dest.to_string(), + None => "".to_string(), /* FIXME: this is a hack, should handle + * no pubkey case better */ + }; - let error_msg = pm.check_payment_limits(msats); + let error_msg = pm.check_payment_limits(msats, dest); // verify amount, convert to msats match error_msg { @@ -121,7 +126,7 @@ async fn handle_nwc_params( match multimint.pay_invoice(invoice, method).await { Ok(content) => { // add payment to tracker - pm.add_payment(msats); + pm.add_payment(msats, dest); content } Err(e) => { @@ -149,32 +154,27 @@ async fn handle_nwc_params( } } RequestParams::PayKeysend(params) => { - d_tag = params.id.map(Tag::Identifier); + d_tag = params.id.map(|id| Tag::identifier(id.clone())); let msats = params.amount; - let error_msg = if config.max_amount > 0 && msats > config.max_amount * 1_000 { - Some("Invoice amount too high.") - } else if config.daily_limit > 0 - && tracker.lock().await.sum_payments() + msats > config.daily_limit * 1_000 - { - Some("Daily limit exceeded.") - } else { - None - }; + let dest = params.pubkey.clone(); + + let error_msg = pm.check_payment_limits(msats, dest); // verify amount, convert to msats match error_msg { None => { - let pubkey = bitcoin::secp256k1::PublicKey::from_str(¶ms.pubkey)?; - match pay_keysend( - pubkey, - params.preimage, - params.tlv_records, - msats, - lnd, - method, - ) - .await + let pubkey = Pubkey::from_str(¶ms.pubkey)?; + match multimint + .pay_keysend( + pubkey, + params.preimage, + params.tlv_records, + msats, + lnd, + method, + ) + .await { Ok(content) => { // add payment to tracker From 97cb4d154ff5ec6bfca2e92bbe684c0b8cc5ece3 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 11:04:19 -0700 Subject: [PATCH 24/50] feat: make invoice --- fedimint-nwc/src/nwc.rs | 81 +++++++++----------------- fedimint-nwc/src/services/multimint.rs | 35 ++++++++++- 2 files changed, 62 insertions(+), 54 deletions(-) diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 05a5c74..0d24ad5 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -3,7 +3,9 @@ use std::str::FromStr; use anyhow::{anyhow, Result}; use lightning_invoice::Bolt11Invoice; use nostr::nips::nip04; -use nostr::nips::nip47::{ErrorCode, Method, NIP47Error, Request, RequestParams, Response}; +use nostr::nips::nip47::{ + ErrorCode, Method, NIP47Error, Request, RequestParams, Response, ResponseResult, +}; use nostr::Tag; use nostr_sdk::{Event, JsonUtil}; use tokio::spawn; @@ -164,35 +166,16 @@ async fn handle_nwc_params( // verify amount, convert to msats match error_msg { None => { - let pubkey = Pubkey::from_str(¶ms.pubkey)?; - match multimint - .pay_keysend( - pubkey, - params.preimage, - params.tlv_records, - msats, - lnd, - method, - ) - .await - { - Ok(content) => { - // add payment to tracker - tracker.lock().await.add_payment(msats); - content - } - Err(e) => { - error!("Error paying keysend: {e}"); - - Response { - result_type: method, - error: Some(NIP47Error { - code: ErrorCode::PaymentFailed, - message: format!("Failed to pay keysend: {e}"), - }), - result: None, - } - } + error!("Error paying keysend: UNSUPPORTED IN IMPLEMENTATION"); + Response { + result_type: method, + error: Some(NIP47Error { + code: ErrorCode::PaymentFailed, + message: format!( + "Failed to pay keysend: UNSUPPORTED IN IMPLEMENTATION" + ), + }), + result: None, } } Some(err_msg) => Response { @@ -206,29 +189,23 @@ async fn handle_nwc_params( } } RequestParams::MakeInvoice(params) => { - let description_hash: Vec = match params.description_hash { - None => vec![], - Some(str) => FromHex::from_hex(&str)?, - }; - let inv = Invoice { - memo: params.description.unwrap_or_default(), - description_hash, - value_msat: params.amount as i64, - expiry: params.expiry.unwrap_or(86_400) as i64, - private: config.route_hints, - ..Default::default() + let description = match params.description { + None => "".to_string(), + Some(desc) => desc, }; - let res = lnd.add_invoice(inv).await?.into_inner(); - - info!("Created invoice: {}", res.payment_request); - - Response { - result_type: Method::MakeInvoice, - error: None, - result: Some(ResponseResult::MakeInvoice(MakeInvoiceResponseResult { - invoice: res.payment_request, - payment_hash: ::hex::encode(res.r_hash), - })), + let res = multimint + .make_invoice(params.amount, description, params.expiry) + .await; + match res { + Ok(res) => res, + Err(e) => Response { + result_type: Method::MakeInvoice, + error: Some(NIP47Error { + code: ErrorCode::PaymentFailed, + message: format!("Failed to make invoice: {e}"), + }), + result: None, + }, } } RequestParams::LookupInvoice(params) => { diff --git a/fedimint-nwc/src/services/multimint.rs b/fedimint-nwc/src/services/multimint.rs index 1b838d8..2dacac0 100644 --- a/fedimint-nwc/src/services/multimint.rs +++ b/fedimint-nwc/src/services/multimint.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use anyhow::{anyhow, bail, Result}; use futures_util::StreamExt; -use lightning_invoice::Bolt11Invoice; +use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; use multimint::fedimint_client::ClientHandleArc; use multimint::fedimint_core::api::InviteCode; use multimint::fedimint_core::config::{FederationId, FederationIdPrefix}; @@ -15,7 +15,8 @@ use multimint::fedimint_ln_client::{ use multimint::fedimint_ln_common::LightningGateway; use multimint::MultiMint; use nostr::nips::nip47::{ - ErrorCode, Method, NIP47Error, PayInvoiceResponseResult, Response, ResponseResult, + ErrorCode, MakeInvoiceResponseResult, Method, NIP47Error, PayInvoiceResponseResult, Response, + ResponseResult, }; use nostr::util::hex; use tracing::info; @@ -140,6 +141,36 @@ impl MultiMintService { Ok(response) } + + pub async fn make_invoice( + &self, + amount_msat: u64, + description: String, + expiry_time: Option, + ) -> Result { + let client = self.get_client(None).await?; + let gateway = self.get_gateway(&client).await?; + let lightning_module = client.get_first_module::(); + // TODO: spawn invoice subscription to this operation + let (_, invoice, _) = lightning_module + .create_bolt11_invoice( + Amount::from_msats(amount_msat), + Bolt11InvoiceDescription::Direct(&Description::new(description)?), + expiry_time, + (), + Some(gateway), + ) + .await?; + + Ok(Response { + result_type: Method::MakeInvoice, + error: None, + result: Some(ResponseResult::MakeInvoice(MakeInvoiceResponseResult { + invoice: invoice.to_string(), + payment_hash: hex::encode(invoice.payment_hash()), + })), + }) + } } #[derive(Debug, Clone)] From fc402fc86dd43ce007641a05a345bab1a97ffa0c Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 11:09:43 -0700 Subject: [PATCH 25/50] refactor: send encrypted response --- fedimint-nwc/src/nwc.rs | 20 +++------------ fedimint-nwc/src/services/nostr.rs | 39 ++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 0d24ad5..a9ab486 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -7,7 +7,7 @@ use nostr::nips::nip47::{ ErrorCode, Method, NIP47Error, Request, RequestParams, Response, ResponseResult, }; use nostr::Tag; -use nostr_sdk::{Event, JsonUtil}; +use nostr_sdk::{Event, EventBuilder, JsonUtil, Kind}; use tokio::spawn; use tracing::{error, info}; @@ -306,21 +306,9 @@ async fn handle_nwc_params( } }; - let encrypted = nip04::encrypt( - &keys.server_key.into(), - &keys.user_keys().public_key(), - content.as_json(), - )?; - let p_tag = Tag::public_key(event.pubkey); - let e_tag = Tag::event(event.id); - let tags = match d_tag { - None => vec![p_tag, e_tag], - Some(d_tag) => vec![p_tag, e_tag, d_tag], - }; - let response = EventBuilder::new(Kind::WalletConnectResponse, encrypted, tags) - .to_event(&keys.server_keys())?; - - client.send_event(response).await?; + nostr + .send_encrypted_response(&event, content, d_tag) + .await?; Ok(()) } diff --git a/fedimint-nwc/src/services/nostr.rs b/fedimint-nwc/src/services/nostr.rs index 74604ad..cbb154b 100644 --- a/fedimint-nwc/src/services/nostr.rs +++ b/fedimint-nwc/src/services/nostr.rs @@ -2,9 +2,13 @@ use std::fs::{create_dir_all, File}; use std::io::{BufReader, Write}; use std::path::Path; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; +use nostr::nips::nip04; +use nostr::nips::nip47::Response; use nostr_sdk::secp256k1::SecretKey; -use nostr_sdk::{Client, Event, EventBuilder, JsonUtil, Keys, Kind, RelayPoolNotification}; +use nostr_sdk::{ + Client, Event, EventBuilder, EventId, JsonUtil, Keys, Kind, RelayPoolNotification, Tag, +}; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast::Receiver; use tracing::info; @@ -95,6 +99,37 @@ impl NostrService { Ok(()) } + pub async fn send_event(&self, event: Event) -> Result { + self.client + .send_event(event) + .await + .map_err(|e| anyhow!("Failed to send event: {}", e)) + } + + pub async fn send_encrypted_response( + &self, + event: &Event, + content: Response, + d_tag: Option, + ) -> Result<(), anyhow::Error> { + let encrypted = nip04::encrypt( + self.server_keys().secret_key()?, + &self.user_keys().public_key(), + content.as_json(), + )?; + let p_tag = Tag::public_key(event.pubkey); + let e_tag = Tag::event(event.id); + let tags = match d_tag { + None => vec![p_tag, e_tag], + Some(d_tag) => vec![p_tag, e_tag, d_tag], + }; + let response = EventBuilder::new(Kind::WalletConnectResponse, encrypted, tags) + .to_event(&self.server_keys())?; + + self.send_event(response).await?; + Ok(()) + } + pub async fn broadcast_info_event(&self) -> Result<(), anyhow::Error> { let content = METHODS .iter() From cd46c2c6ffc11353e277f0d287b9797c55033ef1 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 11:19:34 -0700 Subject: [PATCH 26/50] feat: handlers --- fedimint-nwc/src/nwc.rs | 205 +++++++++++++++++++++++----------------- 1 file changed, 116 insertions(+), 89 deletions(-) diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index a9ab486..7a7cfdf 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -1,11 +1,13 @@ use std::str::FromStr; use anyhow::{anyhow, Result}; -use lightning_invoice::Bolt11Invoice; +use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; use nostr::nips::nip04; use nostr::nips::nip47::{ - ErrorCode, Method, NIP47Error, Request, RequestParams, Response, ResponseResult, + ErrorCode, LookupInvoiceResponseResult, Method, NIP47Error, PayInvoiceRequestParams, + PayKeysendRequestParams, Request, RequestParams, Response, ResponseResult, }; +use nostr::util::hex; use nostr::Tag; use nostr_sdk::{Event, EventBuilder, JsonUtil, Kind}; use tokio::spawn; @@ -61,13 +63,14 @@ pub async fn handle_nwc_request(state: &AppState, event: Event) -> Result<(), an .await } params => { + let mut pm = state.payments_manager.clone(); handle_nwc_params( params, req.method, &event, &state.multimint_service, &state.nostr_service, - &state.payments_manager, + &mut pm, ) .await } @@ -86,10 +89,10 @@ async fn handle_multiple_payments( let event_clone = event.clone(); let mm = state.multimint_service.clone(); let nostr = state.nostr_service.clone(); - let pm = state.payments_manager.clone(); - spawn( - async move { handle_nwc_params(params, method, &event_clone, &mm, &nostr, &pm).await }, - ) + let mut pm = state.payments_manager.clone(); + spawn(async move { + handle_nwc_params(params, method, &event_clone, &mm, &nostr, &mut pm).await + }) .await??; } Ok(()) @@ -101,93 +104,14 @@ async fn handle_nwc_params( event: &Event, multimint: &MultiMintService, nostr: &NostrService, - pm: &PaymentsManager, + pm: &mut PaymentsManager, ) -> Result<(), anyhow::Error> { let mut d_tag: Option = None; let content = match params { RequestParams::PayInvoice(params) => { - d_tag = params.id.map(|id| Tag::identifier(id.clone())); - - let invoice = Bolt11Invoice::from_str(¶ms.invoice) - .map_err(|_| anyhow!("Failed to parse invoice"))?; - let msats = invoice - .amount_milli_satoshis() - .or(params.amount) - .unwrap_or(0); - let dest = match invoice.payee_pub_key() { - Some(dest) => dest.to_string(), - None => "".to_string(), /* FIXME: this is a hack, should handle - * no pubkey case better */ - }; - - let error_msg = pm.check_payment_limits(msats, dest); - - // verify amount, convert to msats - match error_msg { - None => { - match multimint.pay_invoice(invoice, method).await { - Ok(content) => { - // add payment to tracker - pm.add_payment(msats, dest); - content - } - Err(e) => { - error!("Error paying invoice: {e}"); - - Response { - result_type: method, - error: Some(NIP47Error { - code: ErrorCode::InsufficientBalance, - message: format!("Failed to pay invoice: {e}"), - }), - result: None, - } - } - } - } - Some(err_msg) => Response { - result_type: method, - error: Some(NIP47Error { - code: ErrorCode::QuotaExceeded, - message: err_msg.to_string(), - }), - result: None, - }, - } - } - RequestParams::PayKeysend(params) => { - d_tag = params.id.map(|id| Tag::identifier(id.clone())); - - let msats = params.amount; - let dest = params.pubkey.clone(); - - let error_msg = pm.check_payment_limits(msats, dest); - - // verify amount, convert to msats - match error_msg { - None => { - error!("Error paying keysend: UNSUPPORTED IN IMPLEMENTATION"); - Response { - result_type: method, - error: Some(NIP47Error { - code: ErrorCode::PaymentFailed, - message: format!( - "Failed to pay keysend: UNSUPPORTED IN IMPLEMENTATION" - ), - }), - result: None, - } - } - Some(err_msg) => Response { - result_type: method, - error: Some(NIP47Error { - code: ErrorCode::QuotaExceeded, - message: err_msg.to_string(), - }), - result: None, - }, - } + handle_pay_invoice(params, method, multimint, pm).await } + RequestParams::PayKeysend(params) => handle_pay_keysend(params, method, pm).await, RequestParams::MakeInvoice(params) => { let description = match params.description { None => "".to_string(), @@ -312,3 +236,106 @@ async fn handle_nwc_params( Ok(()) } + +async fn handle_pay_invoice( + params: PayInvoiceRequestParams, + method: Method, + multimint: &MultiMintService, + pm: &mut PaymentsManager, +) -> Response { + let invoice = match Bolt11Invoice::from_str(¶ms.invoice) + .map_err(|_| anyhow!("Failed to parse invoice")) + { + Ok(invoice) => invoice, + Err(e) => { + error!("Error parsing invoice: {e}"); + return Response { + result_type: method, + error: Some(NIP47Error { + code: ErrorCode::PaymentFailed, + message: format!("Failed to parse invoice: {e}"), + }), + result: None, + }; + } + }; + + let msats = invoice + .amount_milli_satoshis() + .or(params.amount) + .unwrap_or(0); + let dest = match invoice.payee_pub_key() { + Some(dest) => dest.to_string(), + None => "".to_string(), /* FIXME: this is a hack, should handle + * no pubkey case better */ + }; + + let error_msg = pm.check_payment_limits(msats, dest.clone()); + + // verify amount, convert to msats + match error_msg { + None => { + match multimint.pay_invoice(invoice, method).await { + Ok(content) => { + // add payment to tracker + pm.add_payment(msats, dest); + content + } + Err(e) => { + error!("Error paying invoice: {e}"); + + Response { + result_type: method, + error: Some(NIP47Error { + code: ErrorCode::InsufficientBalance, + message: format!("Failed to pay invoice: {e}"), + }), + result: None, + } + } + } + } + Some(err_msg) => Response { + result_type: method, + error: Some(NIP47Error { + code: ErrorCode::QuotaExceeded, + message: err_msg.to_string(), + }), + result: None, + }, + } +} + +async fn handle_pay_keysend( + params: PayKeysendRequestParams, + method: Method, + pm: &mut PaymentsManager, +) -> Response { + let d_tag = params.id.map(|id| Tag::identifier(id.clone())); + let msats = params.amount; + let dest = params.pubkey.clone(); + + let error_msg = pm.check_payment_limits(msats, dest); + + match error_msg { + None => { + error!("Error paying keysend: UNSUPPORTED IN IMPLEMENTATION"); + Response { + result_type: method, + error: Some(NIP47Error { + code: ErrorCode::PaymentFailed, + message: "Failed to pay keysend: UNSUPPORTED IN IMPLEMENTATION".to_string(), + }), + result: None, + } + } + Some(err_msg) => Response { + result_type: method, + error: Some(NIP47Error { + code: ErrorCode::QuotaExceeded, + message: err_msg, + }), + result: None, + }, + } +} From 95fe5508e48736cbb47de99428cad0c1ec579d0a Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 11:20:06 -0700 Subject: [PATCH 27/50] fix: fix --- fedimint-nwc/src/nwc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 7a7cfdf..7926d67 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -9,7 +9,7 @@ use nostr::nips::nip47::{ }; use nostr::util::hex; use nostr::Tag; -use nostr_sdk::{Event, EventBuilder, JsonUtil, Kind}; +use nostr_sdk::{Event, JsonUtil}; use tokio::spawn; use tracing::{error, info}; From d5d03f9f7f3fd472bc6767b727d54942622d4794 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 11:21:12 -0700 Subject: [PATCH 28/50] refactor: make invoice handler --- fedimint-nwc/src/nwc.rs | 50 ++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 7926d67..5997836 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -4,8 +4,9 @@ use anyhow::{anyhow, Result}; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; use nostr::nips::nip04; use nostr::nips::nip47::{ - ErrorCode, LookupInvoiceResponseResult, Method, NIP47Error, PayInvoiceRequestParams, - PayKeysendRequestParams, Request, RequestParams, Response, ResponseResult, + ErrorCode, LookupInvoiceResponseResult, MakeInvoiceRequestParams, Method, NIP47Error, + PayInvoiceRequestParams, PayKeysendRequestParams, Request, RequestParams, Response, + ResponseResult, }; use nostr::util::hex; use nostr::Tag; @@ -113,24 +114,7 @@ async fn handle_nwc_params( } RequestParams::PayKeysend(params) => handle_pay_keysend(params, method, pm).await, RequestParams::MakeInvoice(params) => { - let description = match params.description { - None => "".to_string(), - Some(desc) => desc, - }; - let res = multimint - .make_invoice(params.amount, description, params.expiry) - .await; - match res { - Ok(res) => res, - Err(e) => Response { - result_type: Method::MakeInvoice, - error: Some(NIP47Error { - code: ErrorCode::PaymentFailed, - message: format!("Failed to make invoice: {e}"), - }), - result: None, - }, - } + handle_make_invoice(params, method, multimint, pm).await } RequestParams::LookupInvoice(params) => { let mut invoice: Option = None; @@ -339,3 +323,29 @@ async fn handle_pay_keysend( }, } } + +async fn handle_make_invoice( + params: MakeInvoiceRequestParams, + method: Method, + multimint: &MultiMintService, + pm: &mut PaymentsManager, +) -> Response { + let description = match params.description { + None => "".to_string(), + Some(desc) => desc, + }; + let res = multimint + .make_invoice(params.amount, description, params.expiry) + .await; + match res { + Ok(res) => res, + Err(e) => Response { + result_type: Method::MakeInvoice, + error: Some(NIP47Error { + code: ErrorCode::PaymentFailed, + message: format!("Failed to make invoice: {e}"), + }), + result: None, + }, + } +} From 54cabe220df5e4213139504214326acd13be6328 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 11:22:55 -0700 Subject: [PATCH 29/50] refactor: all handlers --- fedimint-nwc/src/nwc.rs | 195 +++++++++++++++++++++------------------- 1 file changed, 104 insertions(+), 91 deletions(-) diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 5997836..8045914 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -117,98 +117,10 @@ async fn handle_nwc_params( handle_make_invoice(params, method, multimint, pm).await } RequestParams::LookupInvoice(params) => { - let mut invoice: Option = None; - let payment_hash: Vec = match params.payment_hash { - None => match params.invoice { - None => return Err(anyhow!("Missing payment_hash or invoice")), - Some(bolt11) => { - let inv = Bolt11Invoice::from_str(&bolt11) - .map_err(|_| anyhow!("Failed to parse invoice"))?; - invoice = Some(inv.clone()); - inv.payment_hash().into_32().to_vec() - } - }, - Some(str) => FromHex::from_hex(&str)?, - }; - - let res = lnd - .lookup_invoice(PaymentHash { - r_hash: payment_hash.clone(), - ..Default::default() - }) - .await? - .into_inner(); - - info!("Looked up invoice: {}", res.payment_request); - - let (description, description_hash) = match invoice { - Some(inv) => match inv.description() { - Bolt11InvoiceDescription::Direct(desc) => (Some(desc.to_string()), None), - Bolt11InvoiceDescription::Hash(hash) => (None, Some(hash.0.to_string())), - }, - None => (None, None), - }; - - let preimage = if res.r_preimage.is_empty() { - None - } else { - Some(hex::encode(res.r_preimage)) - }; - - let settled_at = if res.settle_date == 0 { - None - } else { - Some(res.settle_date as u64) - }; - - Response { - result_type: Method::LookupInvoice, - error: None, - result: Some(ResponseResult::LookupInvoice(LookupInvoiceResponseResult { - transaction_type: None, - invoice: Some(res.payment_request), - description, - description_hash, - preimage, - payment_hash: hex::encode(payment_hash), - amount: res.value_msat as u64, - fees_paid: 0, - created_at: res.creation_date as u64, - expires_at: (res.creation_date + res.expiry) as u64, - settled_at, - metadata: Default::default(), - })), - } - } - RequestParams::GetBalance => { - let tracker = tracker.lock().await.sum_payments(); - let remaining_msats = config.daily_limit * 1_000 - tracker; - info!("Current balance: {remaining_msats}msats"); - Response { - result_type: Method::GetBalance, - error: None, - result: Some(ResponseResult::GetBalance(GetBalanceResponseResult { - balance: remaining_msats, - })), - } - } - RequestParams::GetInfo => { - let lnd_info: GetInfoResponse = lnd.get_info(GetInfoRequest {}).await?.into_inner(); - info!("Getting info"); - Response { - result_type: Method::GetBalance, - error: None, - result: Some(ResponseResult::GetInfo(GetInfoResponseResult { - alias: lnd_info.alias, - color: lnd_info.color, - pubkey: lnd_info.identity_pubkey, - network: "".to_string(), - block_height: lnd_info.block_height, - block_hash: lnd_info.block_hash, - methods: METHODS.iter().map(|i| i.to_string()).collect(), - })), - } + handle_lookup_invoice(params, method, multimint, pm).await } + RequestParams::GetBalance => handle_get_balance(method, pm).await, + RequestParams::GetInfo => handle_get_info(method, nostr).await, _ => { return Err(anyhow!("Command not supported")); } @@ -349,3 +261,104 @@ async fn handle_make_invoice( }, } } + +async fn handle_lookup_invoice( + params: LookupInvoiceRequestParams, + method: Method, + multimint: &MultiMintService, + pm: &mut PaymentsManager, +) -> Response { + let mut invoice: Option = None; + let payment_hash: Vec = match params.payment_hash { + None => match params.invoice { + None => return Err(anyhow!("Missing payment_hash or invoice")), + Some(bolt11) => { + let inv = Bolt11Invoice::from_str(&bolt11) + .map_err(|_| anyhow!("Failed to parse invoice"))?; + invoice = Some(inv.clone()); + inv.payment_hash().into_32().to_vec() + } + }, + Some(str) => FromHex::from_hex(&str)?, + }; + + let res = lnd + .lookup_invoice(PaymentHash { + r_hash: payment_hash.clone(), + ..Default::default() + }) + .await? + .into_inner(); + + info!("Looked up invoice: {}", res.payment_request); + + let (description, description_hash) = match invoice { + Some(inv) => match inv.description() { + Bolt11InvoiceDescription::Direct(desc) => (Some(desc.to_string()), None), + Bolt11InvoiceDescription::Hash(hash) => (None, Some(hash.0.to_string())), + }, + None => (None, None), + }; + + let preimage = if res.r_preimage.is_empty() { + None + } else { + Some(hex::encode(res.r_preimage)) + }; + + let settled_at = if res.settle_date == 0 { + None + } else { + Some(res.settle_date as u64) + }; + + Response { + result_type: Method::LookupInvoice, + error: None, + result: Some(ResponseResult::LookupInvoice(LookupInvoiceResponseResult { + transaction_type: None, + invoice: Some(res.payment_request), + description, + description_hash, + preimage, + payment_hash: hex::encode(payment_hash), + amount: res.value_msat as u64, + fees_paid: 0, + created_at: res.creation_date as u64, + expires_at: (res.creation_date + res.expiry) as u64, + settled_at, + metadata: Default::default(), + })), + } +} + +async fn handle_get_balance(method: Method, pm: &mut PaymentsManager) -> Response { + let tracker = tracker.lock().await.sum_payments(); + let remaining_msats = config.daily_limit * 1_000 - tracker; + info!("Current balance: {remaining_msats}msats"); + Response { + result_type: Method::GetBalance, + error: None, + result: Some(ResponseResult::GetBalance(GetBalanceResponseResult { + balance: remaining_msats, + })), + } +} + +async fn handle_get_info(method: Method, nostr: &NostrService) -> Response { + let lnd_info: GetInfoResponse = lnd.get_info(GetInfoRequest {}).await?.into_inner(); + info!("Getting info"); + Response { + result_type: Method::GetInfo, + error: None, + result: Some(ResponseResult::GetInfo(GetInfoResponseResult { + alias: lnd_info.alias, + color: lnd_info.color, + pubkey: lnd_info.identity_pubkey, + network: "".to_string(), + block_height: lnd_info.block_height, + block_hash: lnd_info.block_hash, + methods: METHODS.iter().map(|i| i.to_string()).collect(), + })), + } +} From bd441c9c982f7781aaeb8b42e85175c73f242a26 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 11:23:30 -0700 Subject: [PATCH 30/50] fix: fix --- fedimint-nwc/src/nwc.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 8045914..3f502f3 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -207,7 +207,6 @@ async fn handle_pay_keysend( method: Method, pm: &mut PaymentsManager, ) -> Response { - let d_tag = params.id.map(|id| Tag::identifier(id.clone())); let msats = params.amount; let dest = params.pubkey.clone(); @@ -238,9 +237,7 @@ async fn handle_pay_keysend( async fn handle_make_invoice( params: MakeInvoiceRequestParams, - method: Method, multimint: &MultiMintService, - pm: &mut PaymentsManager, ) -> Response { let description = match params.description { None => "".to_string(), From a2faf8c57c75d38784bfcee8a86c859ee3f3229c Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 11:26:40 -0700 Subject: [PATCH 31/50] fix: fix --- fedimint-nwc/src/nwc.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 3f502f3..9a57c03 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -29,12 +29,6 @@ pub const METHODS: [Method; 8] = [ Method::MultiPayKeysend, ]; -#[derive(Debug, Clone)] -pub struct NwcConfig { - pub max_amount: u64, - pub daily_limit: u64, -} - pub async fn handle_nwc_request(state: &AppState, event: Event) -> Result<(), anyhow::Error> { let user_keys = state.nostr_service.user_keys(); let decrypted = nip04::decrypt(user_keys.secret_key()?, &event.pubkey, &event.content)?; @@ -113,9 +107,7 @@ async fn handle_nwc_params( handle_pay_invoice(params, method, multimint, pm).await } RequestParams::PayKeysend(params) => handle_pay_keysend(params, method, pm).await, - RequestParams::MakeInvoice(params) => { - handle_make_invoice(params, method, multimint, pm).await - } + RequestParams::MakeInvoice(params) => handle_make_invoice(params, multimint).await, RequestParams::LookupInvoice(params) => { handle_lookup_invoice(params, method, multimint, pm).await } From ebd5dac743c6eb2ee2f2e55f58388cf8e3b58ed8 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 11:42:22 -0700 Subject: [PATCH 32/50] feat: start lookup invoice --- fedimint-nwc/src/nwc.rs | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 9a57c03..c3978fb 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -4,9 +4,9 @@ use anyhow::{anyhow, Result}; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; use nostr::nips::nip04; use nostr::nips::nip47::{ - ErrorCode, LookupInvoiceResponseResult, MakeInvoiceRequestParams, Method, NIP47Error, - PayInvoiceRequestParams, PayKeysendRequestParams, Request, RequestParams, Response, - ResponseResult, + ErrorCode, LookupInvoiceRequestParams, LookupInvoiceResponseResult, MakeInvoiceRequestParams, + Method, NIP47Error, PayInvoiceRequestParams, PayKeysendRequestParams, Request, RequestParams, + Response, ResponseResult, }; use nostr::util::hex; use nostr::Tag; @@ -257,29 +257,9 @@ async fn handle_lookup_invoice( multimint: &MultiMintService, pm: &mut PaymentsManager, ) -> Response { - let mut invoice: Option = None; - let payment_hash: Vec = match params.payment_hash { - None => match params.invoice { - None => return Err(anyhow!("Missing payment_hash or invoice")), - Some(bolt11) => { - let inv = Bolt11Invoice::from_str(&bolt11) - .map_err(|_| anyhow!("Failed to parse invoice"))?; - invoice = Some(inv.clone()); - inv.payment_hash().into_32().to_vec() - } - }, - Some(str) => FromHex::from_hex(&str)?, - }; - - let res = lnd - .lookup_invoice(PaymentHash { - r_hash: payment_hash.clone(), - ..Default::default() - }) - .await? - .into_inner(); + let invoice = multimint.lookup_invoice(params).await; - info!("Looked up invoice: {}", res.payment_request); + info!("Looked up invoice: {}", invoice.as_ref().unwrap().invoice); let (description, description_hash) = match invoice { Some(inv) => match inv.description() { From 1d0edb3e844b6253a165f2227f0d618a26ecb999 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 12:56:35 -0700 Subject: [PATCH 33/50] refactor: redb for payments and invoices --- Cargo.lock | 21 ++++++ fedimint-nwc/Cargo.toml | 3 + fedimint-nwc/src/config.rs | 9 +-- fedimint-nwc/src/database/db.rs | 92 +++++++++++++++++++++++++++ fedimint-nwc/src/database/invoice.rs | 36 +++++++++++ fedimint-nwc/src/database/mod.rs | 5 ++ fedimint-nwc/src/database/payment.rs | 47 ++++++++++++++ fedimint-nwc/src/main.rs | 2 +- fedimint-nwc/src/managers/mod.rs | 3 - fedimint-nwc/src/managers/payments.rs | 90 -------------------------- fedimint-nwc/src/services/nostr.rs | 22 +++---- fedimint-nwc/src/state.rs | 28 ++++++-- 12 files changed, 240 insertions(+), 118 deletions(-) create mode 100644 fedimint-nwc/src/database/db.rs create mode 100644 fedimint-nwc/src/database/invoice.rs create mode 100644 fedimint-nwc/src/database/mod.rs create mode 100644 fedimint-nwc/src/database/payment.rs delete mode 100644 fedimint-nwc/src/managers/mod.rs delete mode 100644 fedimint-nwc/src/managers/payments.rs diff --git a/Cargo.lock b/Cargo.lock index 2a07a16..253fda7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1491,13 +1491,16 @@ name = "fedimint-nwc" version = "0.3.5" dependencies = [ "anyhow", + "bincode", "clap 4.5.4", "dotenv", "futures-util", + "itertools 0.13.0", "lightning-invoice", "multimint 0.3.7", "nostr", "nostr-sdk", + "redb", "serde", "serde_json", "tokio", @@ -2309,6 +2312,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -3269,6 +3281,15 @@ dependencies = [ "rand_core", ] +[[package]] +name = "redb" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7508e692a49b6b2290b56540384ccae9b1fb4d77065640b165835b56ffe3bb" +dependencies = [ + "libc", +] + [[package]] name = "redox_syscall" version = "0.4.1" diff --git a/fedimint-nwc/Cargo.toml b/fedimint-nwc/Cargo.toml index b3d3ff5..a4bbd4a 100644 --- a/fedimint-nwc/Cargo.toml +++ b/fedimint-nwc/Cargo.toml @@ -10,14 +10,17 @@ authors.workspace = true [dependencies] anyhow = "1.0.75" +bincode = "1.3.3" clap = { version = "4.5.4", features = ["derive", "env"] } dotenv = "0.15.0" futures-util = "0.3.30" +itertools = "0.13.0" lightning-invoice = { version = "0.26.0", features = ["serde"] } # multimint = { version = "0.3.6" } multimint = { path = "../multimint" } nostr = { version = "0.31.2", features = ["nip47"] } nostr-sdk = { version = "0.31.0", features = ["nip47"] } +redb = "2.1.0" serde = "1.0.193" serde_json = "1.0.108" tokio = { version = "1.34.0", features = ["full"] } diff --git a/fedimint-nwc/src/config.rs b/fedimint-nwc/src/config.rs index 23ec136..2bf2868 100644 --- a/fedimint-nwc/src/config.rs +++ b/fedimint-nwc/src/config.rs @@ -14,15 +14,12 @@ pub struct Cli { /// Federation invite code #[clap(long, env = "FEDIMINT_CLIENTD_INVITE_CODE", required = false)] pub invite_code: String, - /// Path to FM database - #[clap(long, env = "FEDIMINT_CLIENTD_DB_PATH", required = true)] - pub db_path: PathBuf, + /// Working directory for all files + #[clap(long, env = "FEDIMINT_CLIENTD_WORK_DIR", required = true)] + pub work_dir: PathBuf, /// Manual secret #[clap(long, env = "FEDIMINT_CLIENTD_MANUAL_SECRET", required = false)] pub manual_secret: Option, - /// Location of keys file - #[clap(long, env = "FEDIMINT_NWC_KEYS_FILE", default_value_t = String::from("keys.json"))] - pub keys_file: String, /// Nostr relay to use #[clap(long, env = "FEDIMINT_NWC_RELAYS", default_value_t = String::from("wss://relay.damus.io"))] pub relays: String, diff --git a/fedimint-nwc/src/database/db.rs b/fedimint-nwc/src/database/db.rs new file mode 100644 index 0000000..d01b159 --- /dev/null +++ b/fedimint-nwc/src/database/db.rs @@ -0,0 +1,92 @@ +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{Context, Result}; +use itertools::Itertools; +use redb::{Database as RedbDatabase, ReadTransaction, ReadableTable, WriteTransaction}; + +use super::payment::{Payment, PAYMENTS_TABLE}; + +#[derive(Debug, Clone)] +pub struct Database { + db: Arc, + max_amount: u64, + daily_limit: u64, + rate_limit: Duration, +} + +impl From for Database { + fn from(db: RedbDatabase) -> Self { + Self { + db: Arc::new(db), + max_amount: 0, + daily_limit: 0, + rate_limit: Duration::from_secs(0), + } + } +} + +impl Database { + pub fn new( + db_path: &PathBuf, + max_amount: u64, + daily_limit: u64, + rate_limit_secs: u64, + ) -> Result { + let db = RedbDatabase::create(db_path) + .with_context(|| format!("Failed to create database at {}", db_path.display()))?; + Ok(Self { + db: Arc::new(db), + max_amount, + daily_limit, + rate_limit: Duration::from_secs(rate_limit_secs), + }) + } + + pub fn write_with(&self, f: impl FnOnce(&'_ WriteTransaction) -> Result) -> Result { + let mut dbtx = self.db.begin_write()?; + let res = f(&mut dbtx)?; + dbtx.commit()?; + Ok(res) + } + + pub fn read_with(&self, f: impl FnOnce(&'_ ReadTransaction) -> Result) -> Result { + let dbtx = self.db.begin_read()?; + f(&dbtx) + } + + pub fn add_payment(&self, amount: u64, destination: String) -> Result<()> { + self.write_with(|dbtx| { + let mut payments = dbtx.open_table(PAYMENTS_TABLE)?; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs(); + let payment = Payment::new(now, amount, &destination); + payments.insert(&now, &payment)?; + Ok(()) + }) + } + + pub fn sum_payments(&self) -> Result { + self.read_with(|dbtx| { + let payments = dbtx.open_table(PAYMENTS_TABLE)?; + payments + .iter()? + .map_ok(|(_, v)| v.value().amount) + .try_fold(0u64, |acc, amount| amount.map(|amt| acc + amt)) + .map_err(anyhow::Error::new) + }) + } + + pub fn check_payment_limits(&self, msats: u64, _dest: String) -> Result> { + let total_msats = self.sum_payments()? * 1_000; + if self.max_amount > 0 && msats > self.max_amount * 1_000 { + Ok(Some("Invoice amount too high.".to_string())) + } else if self.daily_limit > 0 && total_msats + msats > self.daily_limit * 1_000 { + Ok(Some("Daily limit exceeded.".to_string())) + } else { + Ok(None) + } + } +} diff --git a/fedimint-nwc/src/database/invoice.rs b/fedimint-nwc/src/database/invoice.rs new file mode 100644 index 0000000..bf1ed64 --- /dev/null +++ b/fedimint-nwc/src/database/invoice.rs @@ -0,0 +1,36 @@ +use lightning_invoice::Bolt11Invoice; +use redb::{TableDefinition, TypeName, Value}; +use serde::{Deserialize, Serialize}; + +pub const INVOICES_TABLE: TableDefinition<&str, Invoice> = TableDefinition::new("invoices"); + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Invoice { + invoice: Bolt11Invoice, +} + +impl Value for Invoice { + type SelfType<'a> = Self where Self: 'a; + type AsBytes<'a> = Vec where Self: 'a; + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Vec { + // nosemgrep: use-of-unwrap + bincode::serialize(value).unwrap() + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + // nosemgrep: use-of-unwrap + bincode::deserialize(data).unwrap() + } + + fn fixed_width() -> Option { + None // Return Some(width) if fixed width, None if variable width + } + + fn type_name() -> TypeName { + TypeName::new("Invoice") + } +} diff --git a/fedimint-nwc/src/database/mod.rs b/fedimint-nwc/src/database/mod.rs new file mode 100644 index 0000000..922cae7 --- /dev/null +++ b/fedimint-nwc/src/database/mod.rs @@ -0,0 +1,5 @@ +pub mod db; +pub mod invoice; +pub mod payment; + +pub use db::Database; diff --git a/fedimint-nwc/src/database/payment.rs b/fedimint-nwc/src/database/payment.rs new file mode 100644 index 0000000..ed61d28 --- /dev/null +++ b/fedimint-nwc/src/database/payment.rs @@ -0,0 +1,47 @@ +use redb::{TableDefinition, TypeName, Value}; +use serde::{Deserialize, Serialize}; + +pub const PAYMENTS_TABLE: TableDefinition = TableDefinition::new("payments"); + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Payment { + pub time: u64, + pub amount: u64, + pub destination: String, +} + +impl Payment { + pub fn new(time: u64, amount: u64, destination: &str) -> Self { + Self { + time, + amount, + destination: destination.to_string(), + } + } +} + +impl Value for Payment { + type SelfType<'a> = Self where Self: 'a; + type AsBytes<'a> = Vec where Self: 'a; + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Vec { + // nosemgrep: use-of-unwrap + bincode::serialize(value).unwrap() + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + // nosemgrep: use-of-unwrap + bincode::deserialize(data).unwrap() + } + + fn fixed_width() -> Option { + None // Return Some(width) if fixed width, None if variable width + } + + fn type_name() -> TypeName { + TypeName::new("Payment") + } +} diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index b4f055d..ddce05b 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -5,7 +5,7 @@ use tokio::pin; use tracing::{error, info}; pub mod config; -pub mod managers; +pub mod database; pub mod nwc; pub mod services; pub mod state; diff --git a/fedimint-nwc/src/managers/mod.rs b/fedimint-nwc/src/managers/mod.rs deleted file mode 100644 index 0460cb8..0000000 --- a/fedimint-nwc/src/managers/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod payments; - -pub use payments::PaymentsManager; diff --git a/fedimint-nwc/src/managers/payments.rs b/fedimint-nwc/src/managers/payments.rs deleted file mode 100644 index b248e56..0000000 --- a/fedimint-nwc/src/managers/payments.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::collections::{HashMap, VecDeque}; -use std::time::{Duration, Instant}; - -const CACHE_DURATION: Duration = Duration::from_secs(86_400); // 1 day - -#[derive(Debug, Clone)] -struct Payment { - time: Instant, - amount: u64, - destination: String, -} - -#[derive(Debug, Clone)] -pub struct PaymentsManager { - payments: VecDeque, - max_amount: u64, - daily_limit: u64, - rate_limit: Duration, - max_destination_amounts: HashMap, -} - -impl PaymentsManager { - pub fn new(max_amount: u64, daily_limit: u64, rate_limit_secs: u64) -> Self { - PaymentsManager { - payments: VecDeque::new(), - max_amount, - daily_limit, - rate_limit: Duration::from_secs(rate_limit_secs), - max_destination_amounts: HashMap::new(), - } - } - - pub fn add_payment(&mut self, amount: u64, destination: String) -> Result<(), String> { - let now = Instant::now(); - - // Check rate limit - if let Some(last_payment) = self.payments.back() { - if now.duration_since(last_payment.time) < self.rate_limit { - return Err("Rate limit exceeded.".to_string()); - } - } - - // Check max amount per destination - if let Some(&max_amount) = self.max_destination_amounts.get(&destination) { - if amount > max_amount { - return Err("Destination max amount exceeded.".to_string()); - } - } - - let payment = Payment { - time: now, - amount, - destination, - }; - self.payments.push_back(payment); - Ok(()) - } - - fn clean_old_payments(&mut self) { - let now = Instant::now(); - while let Some(payment) = self.payments.front() { - if now.duration_since(payment.time) < CACHE_DURATION { - break; - } - self.payments.pop_front(); - } - } - - pub fn sum_payments(&mut self) -> u64 { - self.clean_old_payments(); - self.payments.iter().map(|p| p.amount).sum() - } - - pub fn check_payment_limits(&mut self, msats: u64, dest: String) -> Option { - if self.max_amount > 0 && msats > self.max_amount * 1_000 { - Some("Invoice amount too high.".to_string()) - } else if self.daily_limit > 0 && self.sum_payments() + msats > self.daily_limit * 1_000 { - Some("Daily limit exceeded.".to_string()) - } else if self.max_destination_amounts.get(&dest).is_some() { - Some("Destination max amount exceeded.".to_string()) - } else { - None - } - } - - // New: Set maximum amount for a specific destination - pub fn set_max_amount_for_destination(&mut self, destination: String, max_amount: u64) { - self.max_destination_amounts.insert(destination, max_amount); - } -} diff --git a/fedimint-nwc/src/services/nostr.rs b/fedimint-nwc/src/services/nostr.rs index cbb154b..c77e1c9 100644 --- a/fedimint-nwc/src/services/nostr.rs +++ b/fedimint-nwc/src/services/nostr.rs @@ -1,6 +1,6 @@ -use std::fs::{create_dir_all, File}; +use std::fs::File; use std::io::{BufReader, Write}; -use std::path::Path; +use std::path::PathBuf; use anyhow::{anyhow, Context, Result}; use nostr::nips::nip04; @@ -26,9 +26,8 @@ pub struct NostrService { } impl NostrService { - pub async fn new(keys_file: &str, relays: &str) -> Result { - let path = Path::new(keys_file); - let (server_key, user_key) = match File::open(path) { + pub async fn new(keys_file_path: &PathBuf, relays: &str) -> Result { + let (server_key, user_key) = match File::open(keys_file_path) { Ok(file) => { let reader = BufReader::new(file); let keys: Self = serde_json::from_reader(reader).context("Failed to parse JSON")?; @@ -36,7 +35,7 @@ impl NostrService { } Err(_) => { let (server_key, user_key) = Self::generate_keys()?; - Self::write_keys(server_key, user_key, path)?; + Self::write_keys(server_key, user_key, keys_file_path)?; (server_key, user_key) } }; @@ -59,7 +58,11 @@ impl NostrService { Ok((**server_key, **user_key)) } - fn write_keys(server_key: SecretKey, user_key: SecretKey, path: &Path) -> Result<()> { + fn write_keys( + server_key: SecretKey, + user_key: SecretKey, + keys_file_path: &PathBuf, + ) -> Result<()> { let keys = Self { server_key, user_key, @@ -68,10 +71,7 @@ impl NostrService { * initialization */ }; let json_str = serde_json::to_string(&keys).context("Failed to serialize data")?; - if let Some(parent) = path.parent() { - create_dir_all(parent).context("Failed to create directory")?; - } - let mut file = File::create(path).context("Failed to create file")?; + let mut file = File::create(keys_file_path).context("Failed to create file")?; file.write_all(json_str.as_bytes()) .context("Failed to write to file")?; Ok(()) diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index 6937f2e..5274017 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -9,7 +9,7 @@ use tokio::sync::Mutex; use tracing::{debug, error, info}; use crate::config::Cli; -use crate::managers::PaymentsManager; +use crate::database::Database; use crate::nwc::handle_nwc_request; use crate::services::{MultiMintService, NostrService}; @@ -18,25 +18,39 @@ pub struct AppState { pub active_requests: Arc>>, pub multimint_service: MultiMintService, pub nostr_service: NostrService, - pub payments_manager: PaymentsManager, + pub db: Database, } impl AppState { pub async fn new(cli: Cli) -> Result { let invite_code = InviteCode::from_str(&cli.invite_code)?; + + // Define paths for MultiMint and Redb databases within the work_dir + let multimint_db_path = cli.work_dir.join("multimint_db"); + let redb_db_path = cli.work_dir.join("redb_db"); + let keys_file_path = cli.work_dir.join("keys.json"); + + // Ensure directories exist + std::fs::create_dir_all(&multimint_db_path)?; + std::fs::create_dir_all(&redb_db_path)?; + let multimint_service = - MultiMintService::new(cli.db_path, Some(invite_code.federation_id())).await?; - let nostr_service = NostrService::new(&cli.keys_file, &cli.relays).await?; + MultiMintService::new(multimint_db_path, Some(invite_code.federation_id())).await?; + let nostr_service = NostrService::new(&keys_file_path, &cli.relays).await?; let active_requests = Arc::new(Mutex::new(BTreeSet::new())); - let payments_manager = - PaymentsManager::new(cli.max_amount, cli.daily_limit, cli.rate_limit_secs); + let db = Database::new( + &redb_db_path, + cli.max_amount, + cli.daily_limit, + cli.rate_limit_secs, + )?; Ok(Self { active_requests, multimint_service, nostr_service, - payments_manager, + db, }) } From 466dde2837ee9476b9e5cec3d14a77e81b69ed96 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 13:03:22 -0700 Subject: [PATCH 34/50] fix: finish db refactor --- fedimint-nwc/src/database/db.rs | 10 ++++----- fedimint-nwc/src/nwc.rs | 38 ++++++++++++++++----------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/fedimint-nwc/src/database/db.rs b/fedimint-nwc/src/database/db.rs index d01b159..5d9e7c2 100644 --- a/fedimint-nwc/src/database/db.rs +++ b/fedimint-nwc/src/database/db.rs @@ -79,14 +79,14 @@ impl Database { }) } - pub fn check_payment_limits(&self, msats: u64, _dest: String) -> Result> { - let total_msats = self.sum_payments()? * 1_000; + pub fn check_payment_limits(&self, msats: u64, _dest: String) -> Option { + let total_msats = self.sum_payments().unwrap_or(0) * 1_000; if self.max_amount > 0 && msats > self.max_amount * 1_000 { - Ok(Some("Invoice amount too high.".to_string())) + Some("Invoice amount too high.".to_string()) } else if self.daily_limit > 0 && total_msats + msats > self.daily_limit * 1_000 { - Ok(Some("Daily limit exceeded.".to_string())) + Some("Daily limit exceeded.".to_string()) } else { - Ok(None) + None } } } diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index c3978fb..de3d91e 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -14,7 +14,7 @@ use nostr_sdk::{Event, JsonUtil}; use tokio::spawn; use tracing::{error, info}; -use crate::managers::PaymentsManager; +use crate::database::Database; use crate::services::{MultiMintService, NostrService}; use crate::state::AppState; @@ -58,14 +58,13 @@ pub async fn handle_nwc_request(state: &AppState, event: Event) -> Result<(), an .await } params => { - let mut pm = state.payments_manager.clone(); handle_nwc_params( params, req.method, &event, &state.multimint_service, &state.nostr_service, - &mut pm, + &state.db, ) .await } @@ -84,9 +83,9 @@ async fn handle_multiple_payments( let event_clone = event.clone(); let mm = state.multimint_service.clone(); let nostr = state.nostr_service.clone(); - let mut pm = state.payments_manager.clone(); + let mut db = state.db.clone(); spawn(async move { - handle_nwc_params(params, method, &event_clone, &mm, &nostr, &mut pm).await + handle_nwc_params(params, method, &event_clone, &mm, &nostr, &mut db).await }) .await??; } @@ -99,19 +98,19 @@ async fn handle_nwc_params( event: &Event, multimint: &MultiMintService, nostr: &NostrService, - pm: &mut PaymentsManager, + db: &Database, ) -> Result<(), anyhow::Error> { - let mut d_tag: Option = None; + let d_tag: Option = None; let content = match params { RequestParams::PayInvoice(params) => { - handle_pay_invoice(params, method, multimint, pm).await + handle_pay_invoice(params, method, multimint, db).await } - RequestParams::PayKeysend(params) => handle_pay_keysend(params, method, pm).await, + RequestParams::PayKeysend(params) => handle_pay_keysend(params, method, db).await, RequestParams::MakeInvoice(params) => handle_make_invoice(params, multimint).await, RequestParams::LookupInvoice(params) => { - handle_lookup_invoice(params, method, multimint, pm).await + handle_lookup_invoice(params, method, multimint, db).await } - RequestParams::GetBalance => handle_get_balance(method, pm).await, + RequestParams::GetBalance => handle_get_balance(method, db).await, RequestParams::GetInfo => handle_get_info(method, nostr).await, _ => { return Err(anyhow!("Command not supported")); @@ -129,7 +128,7 @@ async fn handle_pay_invoice( params: PayInvoiceRequestParams, method: Method, multimint: &MultiMintService, - pm: &mut PaymentsManager, + db: &Database, ) -> Response { let invoice = match Bolt11Invoice::from_str(¶ms.invoice) .map_err(|_| anyhow!("Failed to parse invoice")) @@ -158,7 +157,7 @@ async fn handle_pay_invoice( * no pubkey case better */ }; - let error_msg = pm.check_payment_limits(msats, dest.clone()); + let error_msg = db.check_payment_limits(msats, dest.clone()); // verify amount, convert to msats match error_msg { @@ -166,7 +165,8 @@ async fn handle_pay_invoice( match multimint.pay_invoice(invoice, method).await { Ok(content) => { // add payment to tracker - pm.add_payment(msats, dest); + // nosemgrep: use-of-unwrap + db.add_payment(msats, dest).unwrap(); content } Err(e) => { @@ -197,12 +197,12 @@ async fn handle_pay_invoice( async fn handle_pay_keysend( params: PayKeysendRequestParams, method: Method, - pm: &mut PaymentsManager, + db: &Database, ) -> Response { let msats = params.amount; let dest = params.pubkey.clone(); - let error_msg = pm.check_payment_limits(msats, dest); + let error_msg = db.check_payment_limits(msats, dest); match error_msg { None => { @@ -255,9 +255,9 @@ async fn handle_lookup_invoice( params: LookupInvoiceRequestParams, method: Method, multimint: &MultiMintService, - pm: &mut PaymentsManager, + db: &Database, ) -> Response { - let invoice = multimint.lookup_invoice(params).await; + let invoice = db.lookup_invoice(params).await; info!("Looked up invoice: {}", invoice.as_ref().unwrap().invoice); @@ -301,7 +301,7 @@ async fn handle_lookup_invoice( } } -async fn handle_get_balance(method: Method, pm: &mut PaymentsManager) -> Response { +async fn handle_get_balance(method: Method, db: &Database) -> Response { let tracker = tracker.lock().await.sum_payments(); let remaining_msats = config.daily_limit * 1_000 - tracker; info!("Current balance: {remaining_msats}msats"); From 602a07207528f2b61b6876d270f9d1a92382ca25 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 13:08:28 -0700 Subject: [PATCH 35/50] fix: workspace version --- Cargo.lock | 2 +- fedimint-clientd/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 253fda7..8475a92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "fedimint-clientd" -version = "0.3.6" +version = "0.3.5" dependencies = [ "anyhow", "async-utility", diff --git a/fedimint-clientd/Cargo.toml b/fedimint-clientd/Cargo.toml index 2bc2c40..e66f458 100644 --- a/fedimint-clientd/Cargo.toml +++ b/fedimint-clientd/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "fedimint-clientd" description = "A fedimint client daemon for server side applications to hold, use, and manage Bitcoin" -version = "0.3.6" +version.workspace = true edition.workspace = true repository.workspace = true keywords.workspace = true From 40d00fff0e257d844b5d192ee5348cf19ebcc2d4 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 13:14:59 -0700 Subject: [PATCH 36/50] fix: geet it building --- .gitignore | 3 +- fedimint-nwc/src/nwc.rs | 181 +++++++++++++++++++++++----------------- 2 files changed, 106 insertions(+), 78 deletions(-) diff --git a/.gitignore b/.gitignore index 65c55d7..e6b7f0d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,4 @@ result /vendor federations.txt -keys.json -fedimint-nwc/keys.json +work_dir diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index de3d91e..a56e70c 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -108,10 +108,38 @@ async fn handle_nwc_params( RequestParams::PayKeysend(params) => handle_pay_keysend(params, method, db).await, RequestParams::MakeInvoice(params) => handle_make_invoice(params, multimint).await, RequestParams::LookupInvoice(params) => { - handle_lookup_invoice(params, method, multimint, db).await + // handle_lookup_invoice(params, method, multimint, db).await + Response { + result_type: method, + error: Some(NIP47Error { + code: ErrorCode::Unauthorized, + message: "LookupInvoice functionality is not implemented yet.".to_string(), + }), + result: None, + } + } + RequestParams::GetBalance => { + //handle_get_balance(method, db).await, + Response { + result_type: method, + error: Some(NIP47Error { + code: ErrorCode::Unauthorized, + message: "GetBalance functionality is not implemented yet.".to_string(), + }), + result: None, + } + } + RequestParams::GetInfo => { + // handle_get_info(method, nostr).await, + Response { + result_type: method, + error: Some(NIP47Error { + code: ErrorCode::Unauthorized, + message: "GetInfo functionality is not implemented yet.".to_string(), + }), + result: None, + } } - RequestParams::GetBalance => handle_get_balance(method, db).await, - RequestParams::GetInfo => handle_get_info(method, nostr).await, _ => { return Err(anyhow!("Command not supported")); } @@ -251,83 +279,84 @@ async fn handle_make_invoice( } } -async fn handle_lookup_invoice( - params: LookupInvoiceRequestParams, - method: Method, - multimint: &MultiMintService, - db: &Database, -) -> Response { - let invoice = db.lookup_invoice(params).await; +// async fn handle_lookup_invoice( +// params: LookupInvoiceRequestParams, +// method: Method, +// multimint: &MultiMintService, +// db: &Database, +// ) -> Response { +// let invoice = db.lookup_invoice(params).await; - info!("Looked up invoice: {}", invoice.as_ref().unwrap().invoice); +// info!("Looked up invoice: {}", invoice.as_ref().unwrap().invoice); - let (description, description_hash) = match invoice { - Some(inv) => match inv.description() { - Bolt11InvoiceDescription::Direct(desc) => (Some(desc.to_string()), None), - Bolt11InvoiceDescription::Hash(hash) => (None, Some(hash.0.to_string())), - }, - None => (None, None), - }; +// let (description, description_hash) = match invoice { +// Some(inv) => match inv.description() { +// Bolt11InvoiceDescription::Direct(desc) => +// (Some(desc.to_string()), None), +// Bolt11InvoiceDescription::Hash(hash) => (None, Some(hash.0.to_string())), +// }, +// None => (None, None), +// }; - let preimage = if res.r_preimage.is_empty() { - None - } else { - Some(hex::encode(res.r_preimage)) - }; +// let preimage = if res.r_preimage.is_empty() { +// None +// } else { +// Some(hex::encode(res.r_preimage)) +// }; - let settled_at = if res.settle_date == 0 { - None - } else { - Some(res.settle_date as u64) - }; +// let settled_at = if res.settle_date == 0 { +// None +// } else { +// Some(res.settle_date as u64) +// }; - Response { - result_type: Method::LookupInvoice, - error: None, - result: Some(ResponseResult::LookupInvoice(LookupInvoiceResponseResult { - transaction_type: None, - invoice: Some(res.payment_request), - description, - description_hash, - preimage, - payment_hash: hex::encode(payment_hash), - amount: res.value_msat as u64, - fees_paid: 0, - created_at: res.creation_date as u64, - expires_at: (res.creation_date + res.expiry) as u64, - settled_at, - metadata: Default::default(), - })), - } -} +// Response { +// result_type: Method::LookupInvoice, +// error: None, +// result: +// Some(ResponseResult::LookupInvoice(LookupInvoiceResponseResult { +// transaction_type: None, invoice: Some(res.payment_request), +// description, +// description_hash, +// preimage, +// payment_hash: hex::encode(payment_hash), +// amount: res.value_msat as u64, +// fees_paid: 0, +// created_at: res.creation_date as u64, +// expires_at: (res.creation_date + res.expiry) as u64, +// settled_at, +// metadata: Default::default(), +// })), +// } +// } -async fn handle_get_balance(method: Method, db: &Database) -> Response { - let tracker = tracker.lock().await.sum_payments(); - let remaining_msats = config.daily_limit * 1_000 - tracker; - info!("Current balance: {remaining_msats}msats"); - Response { - result_type: Method::GetBalance, - error: None, - result: Some(ResponseResult::GetBalance(GetBalanceResponseResult { - balance: remaining_msats, - })), - } -} +// async fn handle_get_balance(method: Method, db: &Database) -> Response { +// let tracker = tracker.lock().await.sum_payments(); +// let remaining_msats = config.daily_limit * 1_000 - tracker; +// info!("Current balance: {remaining_msats}msats"); +// Response { +// result_type: Method::GetBalance, +// error: None, +// result: Some(ResponseResult::GetBalance(GetBalanceResponseResult { +// balance: remaining_msats, +// })), +// } +// } -async fn handle_get_info(method: Method, nostr: &NostrService) -> Response { - let lnd_info: GetInfoResponse = lnd.get_info(GetInfoRequest {}).await?.into_inner(); - info!("Getting info"); - Response { - result_type: Method::GetInfo, - error: None, - result: Some(ResponseResult::GetInfo(GetInfoResponseResult { - alias: lnd_info.alias, - color: lnd_info.color, - pubkey: lnd_info.identity_pubkey, - network: "".to_string(), - block_height: lnd_info.block_height, - block_hash: lnd_info.block_hash, - methods: METHODS.iter().map(|i| i.to_string()).collect(), - })), - } -} +// async fn handle_get_info(method: Method, nostr: &NostrService) -> Response { +// let lnd_info: GetInfoResponse = lnd.get_info(GetInfoRequest +// {}).await?.into_inner(); info!("Getting info"); +// Response { +// result_type: Method::GetInfo, +// error: None, +// result: Some(ResponseResult::GetInfo(GetInfoResponseResult { +// alias: lnd_info.alias, +// color: lnd_info.color, +// pubkey: lnd_info.identity_pubkey, +// network: "".to_string(), +// block_height: lnd_info.block_height, +// block_hash: lnd_info.block_hash, +// methods: METHODS.iter().map(|i| i.to_string()).collect(), +// })), +// } +// } From cdf0ac61ace71ccdd4693d64a7adf4ba55efcd54 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 13:25:53 -0700 Subject: [PATCH 37/50] fix: get db working --- fedimint-nwc/src/database/db.rs | 6 +++--- fedimint-nwc/src/state.rs | 10 +++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/fedimint-nwc/src/database/db.rs b/fedimint-nwc/src/database/db.rs index 5d9e7c2..82b5cbe 100644 --- a/fedimint-nwc/src/database/db.rs +++ b/fedimint-nwc/src/database/db.rs @@ -29,13 +29,13 @@ impl From for Database { impl Database { pub fn new( - db_path: &PathBuf, + redb_path: &PathBuf, max_amount: u64, daily_limit: u64, rate_limit_secs: u64, ) -> Result { - let db = RedbDatabase::create(db_path) - .with_context(|| format!("Failed to create database at {}", db_path.display()))?; + let db = RedbDatabase::create(redb_path) + .with_context(|| format!("Failed to create database at {}", redb_path.display()))?; Ok(Self { db: Arc::new(db), max_amount, diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index 5274017..15f0fea 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -1,4 +1,5 @@ use std::collections::BTreeSet; +use std::fs::create_dir_all; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; @@ -27,12 +28,14 @@ impl AppState { // Define paths for MultiMint and Redb databases within the work_dir let multimint_db_path = cli.work_dir.join("multimint_db"); - let redb_db_path = cli.work_dir.join("redb_db"); + create_dir_all(&multimint_db_path)?; + let db_directory = cli.work_dir.join("redb_db"); + create_dir_all(&db_directory)?; + + let redb_db_path = db_directory.join("database.db"); let keys_file_path = cli.work_dir.join("keys.json"); // Ensure directories exist - std::fs::create_dir_all(&multimint_db_path)?; - std::fs::create_dir_all(&redb_db_path)?; let multimint_service = MultiMintService::new(multimint_db_path, Some(invite_code.federation_id())).await?; @@ -45,6 +48,7 @@ impl AppState { cli.daily_limit, cli.rate_limit_secs, )?; + info!("Initialized database at {}", redb_db_path.display()); Ok(Self { active_requests, From d0d9d18bdc5583110984634acbdeec44d45f2afe Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 13:28:12 -0700 Subject: [PATCH 38/50] fix: fix --- fedimint-nwc/src/database/db.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fedimint-nwc/src/database/db.rs b/fedimint-nwc/src/database/db.rs index 82b5cbe..4251957 100644 --- a/fedimint-nwc/src/database/db.rs +++ b/fedimint-nwc/src/database/db.rs @@ -13,7 +13,7 @@ pub struct Database { db: Arc, max_amount: u64, daily_limit: u64, - rate_limit: Duration, + _rate_limit: Duration, } impl From for Database { @@ -22,7 +22,7 @@ impl From for Database { db: Arc::new(db), max_amount: 0, daily_limit: 0, - rate_limit: Duration::from_secs(0), + _rate_limit: Duration::from_secs(0), } } } @@ -40,7 +40,7 @@ impl Database { db: Arc::new(db), max_amount, daily_limit, - rate_limit: Duration::from_secs(rate_limit_secs), + _rate_limit: Duration::from_secs(rate_limit_secs), }) } From 6696c4f8e2f797ef16cdf885ab014321829ccb36 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 17:07:26 -0700 Subject: [PATCH 39/50] fix: better invoices --- fedimint-nwc/src/database/db.rs | 67 +++++- fedimint-nwc/src/database/invoice.rs | 48 +++- fedimint-nwc/src/database/payment.rs | 9 +- fedimint-nwc/src/nwc.rs | 314 +++++++++++-------------- fedimint-nwc/src/services/multimint.rs | 14 +- 5 files changed, 246 insertions(+), 206 deletions(-) diff --git a/fedimint-nwc/src/database/db.rs b/fedimint-nwc/src/database/db.rs index 4251957..be32f4e 100644 --- a/fedimint-nwc/src/database/db.rs +++ b/fedimint-nwc/src/database/db.rs @@ -1,11 +1,16 @@ use std::path::PathBuf; +use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use anyhow::{Context, Result}; use itertools::Itertools; +use lightning_invoice::Bolt11Invoice; +use nostr::nips::nip47::LookupInvoiceRequestParams; +use nostr::util::hex; use redb::{Database as RedbDatabase, ReadTransaction, ReadableTable, WriteTransaction}; +use super::invoice::{Invoice, INVOICES_TABLE}; use super::payment::{Payment, PAYMENTS_TABLE}; #[derive(Debug, Clone)] @@ -35,7 +40,7 @@ impl Database { rate_limit_secs: u64, ) -> Result { let db = RedbDatabase::create(redb_path) - .with_context(|| format!("Failed to create database at {}", redb_path.display()))?; + .with_context(|| format!("Failed to create redb at {}", redb_path.display()))?; Ok(Self { db: Arc::new(db), max_amount, @@ -56,14 +61,15 @@ impl Database { f(&dbtx) } - pub fn add_payment(&self, amount: u64, destination: String) -> Result<()> { + pub fn add_payment(&self, invoice: Bolt11Invoice) -> Result<()> { + let payment_hash_encoded = hex::encode(invoice.payment_hash().to_vec()); self.write_with(|dbtx| { let mut payments = dbtx.open_table(PAYMENTS_TABLE)?; let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? .as_secs(); - let payment = Payment::new(now, amount, &destination); - payments.insert(&now, &payment)?; + let payment = Payment::new(now, invoice.amount_milli_satoshis().unwrap_or(0), invoice); + payments.insert(&payment_hash_encoded.as_str(), &payment)?; Ok(()) }) } @@ -79,14 +85,59 @@ impl Database { }) } - pub fn check_payment_limits(&self, msats: u64, _dest: String) -> Option { + pub fn check_payment_limits(&self, msats: u64) -> Result<(), anyhow::Error> { let total_msats = self.sum_payments().unwrap_or(0) * 1_000; if self.max_amount > 0 && msats > self.max_amount * 1_000 { - Some("Invoice amount too high.".to_string()) + Err(anyhow::Error::msg("Invoice amount too high.")) } else if self.daily_limit > 0 && total_msats + msats > self.daily_limit * 1_000 { - Some("Daily limit exceeded.".to_string()) + Err(anyhow::Error::msg("Daily limit exceeded.")) } else { - None + Ok(()) + } + } + + pub fn add_invoice(&self, invoice: &Bolt11Invoice) -> Result<()> { + let payment_hash_encoded = hex::encode(invoice.payment_hash().to_vec()); + let invoice = Invoice::from(invoice); + self.write_with(|dbtx| { + let mut invoices = dbtx.open_table(INVOICES_TABLE)?; + invoices + .insert(&payment_hash_encoded.as_str(), &invoice) + .map_err(anyhow::Error::new) + .map(|_| ()) + }) + } + + pub fn lookup_invoice(&self, params: LookupInvoiceRequestParams) -> Result { + if let Some(payment_hash) = params.payment_hash { + let payment_hash_encoded = hex::encode(payment_hash); + self.read_with(|dbtx| { + let invoices = dbtx.open_table(INVOICES_TABLE)?; + invoices + .get(&payment_hash_encoded.as_str()) + .map_err(anyhow::Error::new) + .and_then(|opt_invoice| { + opt_invoice + .ok_or_else(|| anyhow::Error::msg("Invoice not found")) + .map(|access_guard| access_guard.value().clone()) + }) + }) + } else if let Some(bolt11) = params.invoice { + let invoice = Bolt11Invoice::from_str(&bolt11).map_err(|e| anyhow::Error::new(e))?; + let payment_hash_encoded = hex::encode(invoice.payment_hash().to_vec()); + self.read_with(|dbtx| { + let invoices = dbtx.open_table(INVOICES_TABLE)?; + invoices + .get(&payment_hash_encoded.as_str()) + .map_err(anyhow::Error::new) + .and_then(|opt_invoice| { + opt_invoice + .ok_or_else(|| anyhow::Error::msg("Invoice not found")) + .map(|access_guard| access_guard.value().clone()) + }) + }) + } else { + Err(anyhow::Error::msg("No invoice or payment hash provided")) } } } diff --git a/fedimint-nwc/src/database/invoice.rs b/fedimint-nwc/src/database/invoice.rs index bf1ed64..54b62a3 100644 --- a/fedimint-nwc/src/database/invoice.rs +++ b/fedimint-nwc/src/database/invoice.rs @@ -1,4 +1,7 @@ -use lightning_invoice::Bolt11Invoice; +use std::time::{Duration, UNIX_EPOCH}; + +use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; +use nostr::util::hex; use redb::{TableDefinition, TypeName, Value}; use serde::{Deserialize, Serialize}; @@ -6,7 +9,48 @@ pub const INVOICES_TABLE: TableDefinition<&str, Invoice> = TableDefinition::new( #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Invoice { - invoice: Bolt11Invoice, + pub invoice: Bolt11Invoice, + pub preimage: Option, + pub settle_date: Option, +} + +impl Invoice { + pub fn created_at(&self) -> u64 { + self.invoice + .timestamp() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() + } + + pub fn expires_at(&self) -> u64 { + self.created_at() + self.invoice.expiry_time().as_secs() + } + + pub fn settled_at(&self) -> Option { + self.settle_date + .map(|time| UNIX_EPOCH + Duration::from_secs(time)) + .and_then(|time| time.duration_since(UNIX_EPOCH).ok()) + .map(|duration| duration.as_secs()) + } + + pub fn payment_hash(&self) -> String { + hex::encode(self.invoice.payment_hash().to_vec()) + } + + pub fn description(&self) -> Option { + Some(self.invoice.description()) + } +} + +impl From<&Bolt11Invoice> for Invoice { + fn from(invoice: &Bolt11Invoice) -> Self { + Self { + invoice: invoice.clone(), + preimage: None, + settle_date: None, + } + } } impl Value for Invoice { diff --git a/fedimint-nwc/src/database/payment.rs b/fedimint-nwc/src/database/payment.rs index ed61d28..7bd4652 100644 --- a/fedimint-nwc/src/database/payment.rs +++ b/fedimint-nwc/src/database/payment.rs @@ -1,21 +1,22 @@ +use lightning_invoice::Bolt11Invoice; use redb::{TableDefinition, TypeName, Value}; use serde::{Deserialize, Serialize}; -pub const PAYMENTS_TABLE: TableDefinition = TableDefinition::new("payments"); +pub const PAYMENTS_TABLE: TableDefinition<&str, Payment> = TableDefinition::new("payments"); #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Payment { pub time: u64, pub amount: u64, - pub destination: String, + pub invoice: Bolt11Invoice, } impl Payment { - pub fn new(time: u64, amount: u64, destination: &str) -> Self { + pub fn new(time: u64, amount: u64, invoice: Bolt11Invoice) -> Self { Self { time, amount, - destination: destination.to_string(), + invoice, } } } diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index a56e70c..1ea8191 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -1,12 +1,13 @@ use std::str::FromStr; +use std::time::UNIX_EPOCH; use anyhow::{anyhow, Result}; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; use nostr::nips::nip04; use nostr::nips::nip47::{ ErrorCode, LookupInvoiceRequestParams, LookupInvoiceResponseResult, MakeInvoiceRequestParams, - Method, NIP47Error, PayInvoiceRequestParams, PayKeysendRequestParams, Request, RequestParams, - Response, ResponseResult, + MakeInvoiceResponseResult, Method, NIP47Error, PayInvoiceRequestParams, + PayKeysendRequestParams, Request, RequestParams, Response, ResponseResult, }; use nostr::util::hex; use nostr::Tag; @@ -101,55 +102,50 @@ async fn handle_nwc_params( db: &Database, ) -> Result<(), anyhow::Error> { let d_tag: Option = None; - let content = match params { + let response_result = match params { RequestParams::PayInvoice(params) => { handle_pay_invoice(params, method, multimint, db).await } RequestParams::PayKeysend(params) => handle_pay_keysend(params, method, db).await, - RequestParams::MakeInvoice(params) => handle_make_invoice(params, multimint).await, + RequestParams::MakeInvoice(params) => handle_make_invoice(params, multimint, db).await, RequestParams::LookupInvoice(params) => { - // handle_lookup_invoice(params, method, multimint, db).await - Response { - result_type: method, - error: Some(NIP47Error { - code: ErrorCode::Unauthorized, - message: "LookupInvoice functionality is not implemented yet.".to_string(), - }), - result: None, - } + handle_lookup_invoice(params, method, multimint, db).await } - RequestParams::GetBalance => { - //handle_get_balance(method, db).await, - Response { - result_type: method, - error: Some(NIP47Error { - code: ErrorCode::Unauthorized, - message: "GetBalance functionality is not implemented yet.".to_string(), - }), - result: None, - } + RequestParams::GetBalance => Err(anyhow::Error::new(NIP47Error { + code: ErrorCode::Unauthorized, + message: "GetBalance functionality is not implemented yet.".to_string(), + })), + RequestParams::GetInfo => Err(anyhow::Error::new(NIP47Error { + code: ErrorCode::Unauthorized, + message: "GetInfo functionality is not implemented yet.".to_string(), + })), + _ => { + return Err(anyhow!("Command not supported")); + } + }; + + match response_result { + Ok(response) => { + nostr + .send_encrypted_response(&event, response, d_tag) + .await?; + Ok(()) } - RequestParams::GetInfo => { - // handle_get_info(method, nostr).await, - Response { + Err(e) => { + let error_response = Response { result_type: method, error: Some(NIP47Error { code: ErrorCode::Unauthorized, - message: "GetInfo functionality is not implemented yet.".to_string(), + message: format!("Internal error: {}", e), }), result: None, - } - } - _ => { - return Err(anyhow!("Command not supported")); + }; + nostr + .send_encrypted_response(&event, error_response, d_tag) + .await?; + Err(e) } - }; - - nostr - .send_encrypted_response(&event, content, d_tag) - .await?; - - Ok(()) + } } async fn handle_pay_invoice( @@ -157,178 +153,134 @@ async fn handle_pay_invoice( method: Method, multimint: &MultiMintService, db: &Database, -) -> Response { - let invoice = match Bolt11Invoice::from_str(¶ms.invoice) - .map_err(|_| anyhow!("Failed to parse invoice")) - { - Ok(invoice) => invoice, - Err(e) => { - error!("Error parsing invoice: {e}"); - return Response { - result_type: method, - error: Some(NIP47Error { - code: ErrorCode::PaymentFailed, - message: format!("Failed to parse invoice: {e}"), - }), - result: None, - }; - } - }; +) -> Result { + let invoice = Bolt11Invoice::from_str(¶ms.invoice).map_err(|e| NIP47Error { + code: ErrorCode::PaymentFailed, + message: format!("Failed to parse invoice: {e}"), + })?; let msats = invoice .amount_milli_satoshis() .or(params.amount) .unwrap_or(0); - let dest = match invoice.payee_pub_key() { - Some(dest) => dest.to_string(), - None => "".to_string(), /* FIXME: this is a hack, should handle - * no pubkey case better */ - }; - let error_msg = db.check_payment_limits(msats, dest.clone()); + db.check_payment_limits(msats).map_err(|err| NIP47Error { + code: ErrorCode::QuotaExceeded, + message: err.to_string(), + })?; - // verify amount, convert to msats - match error_msg { - None => { - match multimint.pay_invoice(invoice, method).await { - Ok(content) => { - // add payment to tracker - // nosemgrep: use-of-unwrap - db.add_payment(msats, dest).unwrap(); - content - } - Err(e) => { - error!("Error paying invoice: {e}"); + let response = multimint + .pay_invoice(invoice.clone(), method) + .await + .map_err(|e| NIP47Error { + code: ErrorCode::InsufficientBalance, + message: format!("Failed to pay invoice: {e}"), + })?; - Response { - result_type: method, - error: Some(NIP47Error { - code: ErrorCode::InsufficientBalance, - message: format!("Failed to pay invoice: {e}"), - }), - result: None, - } - } - } - } - Some(err_msg) => Response { - result_type: method, - error: Some(NIP47Error { - code: ErrorCode::QuotaExceeded, - message: err_msg.to_string(), - }), - result: None, - }, - } + db.add_payment(invoice).map_err(|e| NIP47Error { + code: ErrorCode::Unauthorized, + message: format!("Failed to add payment to tracker: {e}"), + })?; + + Ok(response) } async fn handle_pay_keysend( params: PayKeysendRequestParams, - method: Method, + _method: Method, db: &Database, -) -> Response { +) -> Result { let msats = params.amount; - let dest = params.pubkey.clone(); - let error_msg = db.check_payment_limits(msats, dest); + db.check_payment_limits(msats).map_err(|err| NIP47Error { + code: ErrorCode::QuotaExceeded, + message: err.to_string(), + })?; - match error_msg { - None => { - error!("Error paying keysend: UNSUPPORTED IN IMPLEMENTATION"); - Response { - result_type: method, - error: Some(NIP47Error { - code: ErrorCode::PaymentFailed, - message: "Failed to pay keysend: UNSUPPORTED IN IMPLEMENTATION".to_string(), - }), - result: None, - } - } - Some(err_msg) => Response { - result_type: method, - error: Some(NIP47Error { - code: ErrorCode::QuotaExceeded, - message: err_msg, - }), - result: None, - }, - } + Err(NIP47Error { + code: ErrorCode::PaymentFailed, + message: "Failed to pay keysend: UNSUPPORTED IN IMPLEMENTATION".to_string(), + }) } async fn handle_make_invoice( params: MakeInvoiceRequestParams, multimint: &MultiMintService, -) -> Response { - let description = match params.description { - None => "".to_string(), - Some(desc) => desc, - }; - let res = multimint + db: &Database, +) -> Result { + let description = params.description.unwrap_or_default(); + let invoice = multimint .make_invoice(params.amount, description, params.expiry) - .await; - match res { - Ok(res) => res, - Err(e) => Response { - result_type: Method::MakeInvoice, - error: Some(NIP47Error { - code: ErrorCode::PaymentFailed, - message: format!("Failed to make invoice: {e}"), - }), - result: None, - }, - } + .await + .map_err(|e| NIP47Error { + code: ErrorCode::PaymentFailed, + message: format!("Failed to make invoice: {e}"), + })?; + + db.add_invoice(&invoice).map_err(|e| NIP47Error { + code: ErrorCode::Unauthorized, + message: format!("Failed to add invoice to database: {e}"), + })?; + + Ok(Response { + result_type: Method::MakeInvoice, + error: None, + result: Some(ResponseResult::MakeInvoice(MakeInvoiceResponseResult { + invoice: invoice.to_string(), + payment_hash: hex::encode(invoice.payment_hash()), + })), + }) } -// async fn handle_lookup_invoice( -// params: LookupInvoiceRequestParams, -// method: Method, -// multimint: &MultiMintService, -// db: &Database, -// ) -> Response { -// let invoice = db.lookup_invoice(params).await; +async fn handle_lookup_invoice( + params: LookupInvoiceRequestParams, + method: Method, + db: &Database, +) -> Result { + let invoice = db.lookup_invoice(params).map_err(|e| NIP47Error { + code: ErrorCode::Unauthorized, + message: format!("Failed to lookup invoice: {e}"), + })?; + let payment_hash = invoice.payment_hash(); -// info!("Looked up invoice: {}", invoice.as_ref().unwrap().invoice); + info!("Looked up invoice: {}", payment_hash); -// let (description, description_hash) = match invoice { -// Some(inv) => match inv.description() { -// Bolt11InvoiceDescription::Direct(desc) => -// (Some(desc.to_string()), None), -// Bolt11InvoiceDescription::Hash(hash) => (None, Some(hash.0.to_string())), -// }, -// None => (None, None), -// }; + let (description, description_hash) = match invoice.description() { + Some(Bolt11InvoiceDescription::Direct(desc)) => (Some(desc.to_string()), None), + Some(Bolt11InvoiceDescription::Hash(hash)) => (None, Some(hash.0.to_string())), + None => (None, None), + }; -// let preimage = if res.r_preimage.is_empty() { -// None -// } else { -// Some(hex::encode(res.r_preimage)) -// }; + let preimage = match invoice.clone().preimage { + Some(preimage) => Some(hex::encode(preimage)), + None => None, + }; -// let settled_at = if res.settle_date == 0 { -// None -// } else { -// Some(res.settle_date as u64) -// }; + let settled_at = invoice.settled_at(); + let created_at = invoice.created_at(); + let expires_at = invoice.expires_at(); + let invoice_str = invoice.invoice.to_string(); + let amount = invoice.invoice.amount_milli_satoshis().unwrap_or(0) as u64; -// Response { -// result_type: Method::LookupInvoice, -// error: None, -// result: -// Some(ResponseResult::LookupInvoice(LookupInvoiceResponseResult { -// transaction_type: None, invoice: Some(res.payment_request), -// description, -// description_hash, -// preimage, -// payment_hash: hex::encode(payment_hash), -// amount: res.value_msat as u64, -// fees_paid: 0, -// created_at: res.creation_date as u64, -// expires_at: (res.creation_date + res.expiry) as u64, -// settled_at, -// metadata: Default::default(), -// })), -// } -// } + Ok(Response { + result_type: method, + error: None, + result: Some(ResponseResult::LookupInvoice(LookupInvoiceResponseResult { + transaction_type: None, + invoice: Some(invoice_str), + description, + description_hash, + preimage, + payment_hash, + amount: amount, + fees_paid: 0, + created_at, + expires_at, + settled_at, + metadata: Default::default(), + })), + }) +} // async fn handle_get_balance(method: Method, db: &Database) -> Response { // let tracker = tracker.lock().await.sum_payments(); diff --git a/fedimint-nwc/src/services/multimint.rs b/fedimint-nwc/src/services/multimint.rs index 2dacac0..8a228df 100644 --- a/fedimint-nwc/src/services/multimint.rs +++ b/fedimint-nwc/src/services/multimint.rs @@ -15,8 +15,7 @@ use multimint::fedimint_ln_client::{ use multimint::fedimint_ln_common::LightningGateway; use multimint::MultiMint; use nostr::nips::nip47::{ - ErrorCode, MakeInvoiceResponseResult, Method, NIP47Error, PayInvoiceResponseResult, Response, - ResponseResult, + ErrorCode, Method, NIP47Error, PayInvoiceResponseResult, Response, ResponseResult, }; use nostr::util::hex; use tracing::info; @@ -147,7 +146,7 @@ impl MultiMintService { amount_msat: u64, description: String, expiry_time: Option, - ) -> Result { + ) -> Result { let client = self.get_client(None).await?; let gateway = self.get_gateway(&client).await?; let lightning_module = client.get_first_module::(); @@ -162,14 +161,7 @@ impl MultiMintService { ) .await?; - Ok(Response { - result_type: Method::MakeInvoice, - error: None, - result: Some(ResponseResult::MakeInvoice(MakeInvoiceResponseResult { - invoice: invoice.to_string(), - payment_hash: hex::encode(invoice.payment_hash()), - })), - }) + Ok(invoice) } } From 8ed626f452adaaa67721e6fc402a18308a0cf964 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 17:10:25 -0700 Subject: [PATCH 40/50] fix: error handling --- fedimint-nwc/src/nwc.rs | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 1ea8191..c88ac60 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -1,5 +1,4 @@ use std::str::FromStr; -use std::time::UNIX_EPOCH; use anyhow::{anyhow, Result}; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; @@ -13,7 +12,7 @@ use nostr::util::hex; use nostr::Tag; use nostr_sdk::{Event, JsonUtil}; use tokio::spawn; -use tracing::{error, info}; +use tracing::info; use crate::database::Database; use crate::services::{MultiMintService, NostrService}; @@ -108,42 +107,31 @@ async fn handle_nwc_params( } RequestParams::PayKeysend(params) => handle_pay_keysend(params, method, db).await, RequestParams::MakeInvoice(params) => handle_make_invoice(params, multimint, db).await, - RequestParams::LookupInvoice(params) => { - handle_lookup_invoice(params, method, multimint, db).await - } - RequestParams::GetBalance => Err(anyhow::Error::new(NIP47Error { + RequestParams::LookupInvoice(params) => handle_lookup_invoice(params, method, db).await, + RequestParams::GetBalance => Err(NIP47Error { code: ErrorCode::Unauthorized, message: "GetBalance functionality is not implemented yet.".to_string(), - })), - RequestParams::GetInfo => Err(anyhow::Error::new(NIP47Error { + }), + RequestParams::GetInfo => Err(NIP47Error { code: ErrorCode::Unauthorized, message: "GetInfo functionality is not implemented yet.".to_string(), - })), + }), _ => { return Err(anyhow!("Command not supported")); } }; match response_result { - Ok(response) => { - nostr - .send_encrypted_response(&event, response, d_tag) - .await?; - Ok(()) - } + Ok(response) => nostr.send_encrypted_response(&event, response, d_tag).await, Err(e) => { let error_response = Response { result_type: method, - error: Some(NIP47Error { - code: ErrorCode::Unauthorized, - message: format!("Internal error: {}", e), - }), + error: Some(e), result: None, }; nostr .send_encrypted_response(&event, error_response, d_tag) - .await?; - Err(e) + .await } } } From 90c08e453d8ee35c9f12fb4418a24df593709b50 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 17:14:42 -0700 Subject: [PATCH 41/50] feat: get_balance --- fedimint-nwc/src/database/db.rs | 4 ++-- fedimint-nwc/src/nwc.rs | 39 +++++++++++++++++---------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/fedimint-nwc/src/database/db.rs b/fedimint-nwc/src/database/db.rs index be32f4e..0d3c98c 100644 --- a/fedimint-nwc/src/database/db.rs +++ b/fedimint-nwc/src/database/db.rs @@ -16,8 +16,8 @@ use super::payment::{Payment, PAYMENTS_TABLE}; #[derive(Debug, Clone)] pub struct Database { db: Arc, - max_amount: u64, - daily_limit: u64, + pub max_amount: u64, + pub daily_limit: u64, _rate_limit: Duration, } diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index c88ac60..14a2128 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -4,9 +4,10 @@ use anyhow::{anyhow, Result}; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; use nostr::nips::nip04; use nostr::nips::nip47::{ - ErrorCode, LookupInvoiceRequestParams, LookupInvoiceResponseResult, MakeInvoiceRequestParams, - MakeInvoiceResponseResult, Method, NIP47Error, PayInvoiceRequestParams, - PayKeysendRequestParams, Request, RequestParams, Response, ResponseResult, + ErrorCode, GetBalanceResponseResult, LookupInvoiceRequestParams, LookupInvoiceResponseResult, + MakeInvoiceRequestParams, MakeInvoiceResponseResult, Method, NIP47Error, + PayInvoiceRequestParams, PayKeysendRequestParams, Request, RequestParams, Response, + ResponseResult, }; use nostr::util::hex; use nostr::Tag; @@ -108,10 +109,7 @@ async fn handle_nwc_params( RequestParams::PayKeysend(params) => handle_pay_keysend(params, method, db).await, RequestParams::MakeInvoice(params) => handle_make_invoice(params, multimint, db).await, RequestParams::LookupInvoice(params) => handle_lookup_invoice(params, method, db).await, - RequestParams::GetBalance => Err(NIP47Error { - code: ErrorCode::Unauthorized, - message: "GetBalance functionality is not implemented yet.".to_string(), - }), + RequestParams::GetBalance => handle_get_balance(method, db).await, RequestParams::GetInfo => Err(NIP47Error { code: ErrorCode::Unauthorized, message: "GetInfo functionality is not implemented yet.".to_string(), @@ -270,18 +268,21 @@ async fn handle_lookup_invoice( }) } -// async fn handle_get_balance(method: Method, db: &Database) -> Response { -// let tracker = tracker.lock().await.sum_payments(); -// let remaining_msats = config.daily_limit * 1_000 - tracker; -// info!("Current balance: {remaining_msats}msats"); -// Response { -// result_type: Method::GetBalance, -// error: None, -// result: Some(ResponseResult::GetBalance(GetBalanceResponseResult { -// balance: remaining_msats, -// })), -// } -// } +async fn handle_get_balance(method: Method, db: &Database) -> Result { + let tracker = db.sum_payments().map_err(|e| NIP47Error { + code: ErrorCode::Unauthorized, + message: format!("Failed to get balance: {e}"), + })?; + let remaining_msats = db.daily_limit * 1_000 - tracker; + info!("Current balance: {remaining_msats}msats"); + Ok(Response { + result_type: method, + error: None, + result: Some(ResponseResult::GetBalance(GetBalanceResponseResult { + balance: remaining_msats, + })), + }) +} // async fn handle_get_info(method: Method, nostr: &NostrService) -> Response { // let lnd_info: GetInfoResponse = lnd.get_info(GetInfoRequest From 5d5b21e76c1aeadb229dfb97b4b906236481288e Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 17:18:35 -0700 Subject: [PATCH 42/50] feat: boilerplate getinfo --- fedimint-nwc/src/nwc.rs | 51 +++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 14a2128..490e637 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -4,9 +4,9 @@ use anyhow::{anyhow, Result}; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; use nostr::nips::nip04; use nostr::nips::nip47::{ - ErrorCode, GetBalanceResponseResult, LookupInvoiceRequestParams, LookupInvoiceResponseResult, - MakeInvoiceRequestParams, MakeInvoiceResponseResult, Method, NIP47Error, - PayInvoiceRequestParams, PayKeysendRequestParams, Request, RequestParams, Response, + ErrorCode, GetBalanceResponseResult, GetInfoResponseResult, LookupInvoiceRequestParams, + LookupInvoiceResponseResult, MakeInvoiceRequestParams, MakeInvoiceResponseResult, Method, + NIP47Error, PayInvoiceRequestParams, PayKeysendRequestParams, Request, RequestParams, Response, ResponseResult, }; use nostr::util::hex; @@ -109,11 +109,8 @@ async fn handle_nwc_params( RequestParams::PayKeysend(params) => handle_pay_keysend(params, method, db).await, RequestParams::MakeInvoice(params) => handle_make_invoice(params, multimint, db).await, RequestParams::LookupInvoice(params) => handle_lookup_invoice(params, method, db).await, - RequestParams::GetBalance => handle_get_balance(method, db).await, - RequestParams::GetInfo => Err(NIP47Error { - code: ErrorCode::Unauthorized, - message: "GetInfo functionality is not implemented yet.".to_string(), - }), + RequestParams::GetBalance => handle_get_balance(db).await, + RequestParams::GetInfo => handle_get_info().await, _ => { return Err(anyhow!("Command not supported")); } @@ -268,7 +265,7 @@ async fn handle_lookup_invoice( }) } -async fn handle_get_balance(method: Method, db: &Database) -> Result { +async fn handle_get_balance(db: &Database) -> Result { let tracker = db.sum_payments().map_err(|e| NIP47Error { code: ErrorCode::Unauthorized, message: format!("Failed to get balance: {e}"), @@ -276,7 +273,7 @@ async fn handle_get_balance(method: Method, db: &Database) -> Result Result Response { -// let lnd_info: GetInfoResponse = lnd.get_info(GetInfoRequest -// {}).await?.into_inner(); info!("Getting info"); -// Response { -// result_type: Method::GetInfo, -// error: None, -// result: Some(ResponseResult::GetInfo(GetInfoResponseResult { -// alias: lnd_info.alias, -// color: lnd_info.color, -// pubkey: lnd_info.identity_pubkey, -// network: "".to_string(), -// block_height: lnd_info.block_height, -// block_hash: lnd_info.block_hash, -// methods: METHODS.iter().map(|i| i.to_string()).collect(), -// })), -// } -// } +async fn handle_get_info() -> Result { + Ok(Response { + result_type: Method::GetInfo, + error: None, + result: Some(ResponseResult::GetInfo(GetInfoResponseResult { + alias: "Fedimint NWC".to_string(), + color: "Fedimint Blue".to_string(), + pubkey: "0300000000000000000000000000000000000000000000000000000000000000000" + .to_string(), + network: "bitcoin".to_string(), + block_height: 0, + block_hash: "000000000000000000000000000000000000000000000000000000000000000000" + .to_string(), + methods: METHODS.iter().map(|i| i.to_string()).collect(), + })), + }) +} From a3e72af8de785074d6b59f9b6d2008b8030b1ecf Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 17:20:50 -0700 Subject: [PATCH 43/50] fix: notes on todos --- fedimint-nwc/src/nwc.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 490e637..19100d9 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -265,6 +265,9 @@ async fn handle_lookup_invoice( }) } +// TODO: Implement this with multimint + db +// should normally do multimint balance check + db payments manager balance +// for throughput and limit checks async fn handle_get_balance(db: &Database) -> Result { let tracker = db.sum_payments().map_err(|e| NIP47Error { code: ErrorCode::Unauthorized, @@ -281,6 +284,7 @@ async fn handle_get_balance(db: &Database) -> Result { }) } +// TODO: Implement this instead of the boilerplate async fn handle_get_info() -> Result { Ok(Response { result_type: Method::GetInfo, From 793fad38ad1ff9a89f8fd73986177c383e756f1b Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Sun, 26 May 2024 17:28:40 -0700 Subject: [PATCH 44/50] docs: notes --- fedimint-nwc/src/database/db.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fedimint-nwc/src/database/db.rs b/fedimint-nwc/src/database/db.rs index 0d3c98c..01e0c6a 100644 --- a/fedimint-nwc/src/database/db.rs +++ b/fedimint-nwc/src/database/db.rs @@ -13,6 +13,12 @@ use redb::{Database as RedbDatabase, ReadTransaction, ReadableTable, WriteTransa use super::invoice::{Invoice, INVOICES_TABLE}; use super::payment::{Payment, PAYMENTS_TABLE}; +/// Database for storing and retrieving payment information +/// Invoices are invoices that we create as part of make_invoice +/// Payments are payments that we perform as part of pay_invoice +/// Any other configs here are just temporary until we have a better way to +/// store them for making the more complex rate limiting and payments caveats +/// for more interesting NWC usecases #[derive(Debug, Clone)] pub struct Database { db: Arc, From 0d89d678d8617fe3d4cdb739fd3b5ba7594334ce Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Mon, 27 May 2024 12:15:55 -0700 Subject: [PATCH 45/50] fix: fix --- fedimint-nwc/src/services/multimint.rs | 9 ++++---- fedimint-nwc/src/state.rs | 29 ++++++++++---------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/fedimint-nwc/src/services/multimint.rs b/fedimint-nwc/src/services/multimint.rs index 8a228df..671fc59 100644 --- a/fedimint-nwc/src/services/multimint.rs +++ b/fedimint-nwc/src/services/multimint.rs @@ -27,11 +27,12 @@ pub struct MultiMintService { } impl MultiMintService { - pub async fn new( - db_path: PathBuf, - default_federation_id: Option, - ) -> Result { + pub async fn new(db_path: PathBuf, invite_code: Option) -> Result { let clients = MultiMint::new(db_path).await?; + clients + .register_new(invite_code) + .update_gateway_caches() + .await?; clients.update_gateway_caches().await?; Ok(Self { multimint: clients, diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index 15f0fea..0585c33 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use std::time::Duration; use multimint::fedimint_core::api::InviteCode; -use nostr_sdk::{Event, EventId, JsonUtil, Kind}; +use nostr_sdk::{Event, EventId, JsonUtil}; use tokio::sync::Mutex; use tracing::{debug, error, info}; @@ -35,8 +35,6 @@ impl AppState { let redb_db_path = db_directory.join("database.db"); let keys_file_path = cli.work_dir.join("keys.json"); - // Ensure directories exist - let multimint_service = MultiMintService::new(multimint_db_path, Some(invite_code.federation_id())).await?; let nostr_service = NostrService::new(&keys_file_path, &cli.relays).await?; @@ -79,22 +77,17 @@ impl AppState { /// Adds nwc events to active requests set while waiting for them to /// complete so they can finish processing before a shutdown. pub async fn handle_event(&self, event: Event) { - if event.kind == Kind::WalletConnectRequest && event.verify().is_ok() { - info!("Received event: {}", event.as_json()); - let event_id = event.id; - self.active_requests.lock().await.insert(event_id); - - match tokio::time::timeout(Duration::from_secs(60), handle_nwc_request(&self, event)) - .await - { - Ok(Ok(_)) => {} - Ok(Err(e)) => error!("Error processing request: {e}"), - Err(e) => error!("Timeout error: {e}"), - } + info!("Received event: {}", event.as_json()); + let event_id = event.id; + self.active_requests.lock().await.insert(event_id); - self.active_requests.lock().await.remove(&event_id); - } else { - error!("Invalid event: {}", event.as_json()); + match tokio::time::timeout(Duration::from_secs(60), handle_nwc_request(&self, event)).await + { + Ok(Ok(_)) => {} + Ok(Err(e)) => error!("Error processing request: {e}"), + Err(e) => error!("Timeout error: {e}"), } + + self.active_requests.lock().await.remove(&event_id); } } From 3cc8c793fa96da07b1c8564b20c0fa40428b1f32 Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Mon, 27 May 2024 16:14:11 -0400 Subject: [PATCH 46/50] feat: progress --- fedimint-nwc/src/services/multimint.rs | 13 ++++++++----- fedimint-nwc/src/state.rs | 3 ++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/fedimint-nwc/src/services/multimint.rs b/fedimint-nwc/src/services/multimint.rs index 671fc59..7db8c7c 100644 --- a/fedimint-nwc/src/services/multimint.rs +++ b/fedimint-nwc/src/services/multimint.rs @@ -27,16 +27,19 @@ pub struct MultiMintService { } impl MultiMintService { - pub async fn new(db_path: PathBuf, invite_code: Option) -> Result { - let clients = MultiMint::new(db_path).await?; + pub async fn new( + db_path: PathBuf, + invite_code: InviteCode, + manual_secret: Option, + ) -> Result { + let mut clients = MultiMint::new(db_path).await?; clients - .register_new(invite_code) - .update_gateway_caches() + .register_new(invite_code.clone(), manual_secret.clone()) .await?; clients.update_gateway_caches().await?; Ok(Self { multimint: clients, - default_federation_id, + default_federation_id: Some(invite_code.federation_id()), }) } diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index 0585c33..5b1e1b7 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -25,6 +25,7 @@ pub struct AppState { impl AppState { pub async fn new(cli: Cli) -> Result { let invite_code = InviteCode::from_str(&cli.invite_code)?; + let manual_secret = cli.manual_secret; // Define paths for MultiMint and Redb databases within the work_dir let multimint_db_path = cli.work_dir.join("multimint_db"); @@ -36,7 +37,7 @@ impl AppState { let keys_file_path = cli.work_dir.join("keys.json"); let multimint_service = - MultiMintService::new(multimint_db_path, Some(invite_code.federation_id())).await?; + MultiMintService::new(multimint_db_path, invite_code, manual_secret).await?; let nostr_service = NostrService::new(&keys_file_path, &cli.relays).await?; let active_requests = Arc::new(Mutex::new(BTreeSet::new())); From 537834ca7829ee70d9a671b36b77a8718de3d584 Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Mon, 27 May 2024 16:17:40 -0400 Subject: [PATCH 47/50] feat: progress --- fedimint-nwc/src/database/db.rs | 8 +++--- fedimint-nwc/src/database/invoice.rs | 2 +- fedimint-nwc/src/main.rs | 37 +++++++++++--------------- fedimint-nwc/src/nwc.rs | 21 +++++++-------- fedimint-nwc/src/services/multimint.rs | 4 +-- fedimint-nwc/src/services/nostr.rs | 2 +- fedimint-nwc/src/state.rs | 3 +-- 7 files changed, 34 insertions(+), 43 deletions(-) diff --git a/fedimint-nwc/src/database/db.rs b/fedimint-nwc/src/database/db.rs index 01e0c6a..63b4802 100644 --- a/fedimint-nwc/src/database/db.rs +++ b/fedimint-nwc/src/database/db.rs @@ -68,7 +68,7 @@ impl Database { } pub fn add_payment(&self, invoice: Bolt11Invoice) -> Result<()> { - let payment_hash_encoded = hex::encode(invoice.payment_hash().to_vec()); + let payment_hash_encoded = hex::encode(invoice.payment_hash()); self.write_with(|dbtx| { let mut payments = dbtx.open_table(PAYMENTS_TABLE)?; let now = std::time::SystemTime::now() @@ -103,7 +103,7 @@ impl Database { } pub fn add_invoice(&self, invoice: &Bolt11Invoice) -> Result<()> { - let payment_hash_encoded = hex::encode(invoice.payment_hash().to_vec()); + let payment_hash_encoded = hex::encode(invoice.payment_hash()); let invoice = Invoice::from(invoice); self.write_with(|dbtx| { let mut invoices = dbtx.open_table(INVOICES_TABLE)?; @@ -129,8 +129,8 @@ impl Database { }) }) } else if let Some(bolt11) = params.invoice { - let invoice = Bolt11Invoice::from_str(&bolt11).map_err(|e| anyhow::Error::new(e))?; - let payment_hash_encoded = hex::encode(invoice.payment_hash().to_vec()); + let invoice = Bolt11Invoice::from_str(&bolt11).map_err(anyhow::Error::new)?; + let payment_hash_encoded = hex::encode(invoice.payment_hash()); self.read_with(|dbtx| { let invoices = dbtx.open_table(INVOICES_TABLE)?; invoices diff --git a/fedimint-nwc/src/database/invoice.rs b/fedimint-nwc/src/database/invoice.rs index 54b62a3..4074372 100644 --- a/fedimint-nwc/src/database/invoice.rs +++ b/fedimint-nwc/src/database/invoice.rs @@ -35,7 +35,7 @@ impl Invoice { } pub fn payment_hash(&self) -> String { - hex::encode(self.invoice.payment_hash().to_vec()) + hex::encode(self.invoice.payment_hash()) } pub fn description(&self) -> Option { diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index ddce05b..ee6b852 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -51,28 +51,23 @@ async fn event_loop(state: AppState) -> Result<()> { break; }, notification = notifications.recv() => { - match notification { - Ok(notification) => match notification { - RelayPoolNotification::Event { event, .. } => { - // Only handle nwc events - if event.kind == Kind::WalletConnectRequest - && event.pubkey == state.nostr_service.user_keys().public_key() - && event.verify().is_ok() { - info!("Received event: {}", event.as_json()); - state.handle_event(*event).await - } else { - error!("Invalid nwc event: {}", event.as_json()); - } - }, - RelayPoolNotification::Shutdown => { - info!("Relay pool shutdown"); - break; - }, - _ => { - error!("Unhandled relay pool notification: {notification:?}"); + if let Ok(notification) = notification { + if let RelayPoolNotification::Event { event, .. } = notification { + // Only handle nwc events + if event.kind == Kind::WalletConnectRequest + && event.pubkey == state.nostr_service.user_keys().public_key() + && event.verify().is_ok() { + info!("Received event: {}", event.as_json()); + state.handle_event(*event).await + } else { + error!("Invalid nwc event: {}", event.as_json()); } - }, - Err(_) => {}, + } else if let RelayPoolNotification::Shutdown = notification { + info!("Relay pool shutdown"); + break; + } else { + error!("Unhandled relay pool notification: {notification:?}"); + } } } } diff --git a/fedimint-nwc/src/nwc.rs b/fedimint-nwc/src/nwc.rs index 19100d9..d6d5c66 100644 --- a/fedimint-nwc/src/nwc.rs +++ b/fedimint-nwc/src/nwc.rs @@ -84,10 +84,10 @@ async fn handle_multiple_payments( let event_clone = event.clone(); let mm = state.multimint_service.clone(); let nostr = state.nostr_service.clone(); - let mut db = state.db.clone(); - spawn(async move { - handle_nwc_params(params, method, &event_clone, &mm, &nostr, &mut db).await - }) + let db = state.db.clone(); + spawn( + async move { handle_nwc_params(params, method, &event_clone, &mm, &nostr, &db).await }, + ) .await??; } Ok(()) @@ -117,7 +117,7 @@ async fn handle_nwc_params( }; match response_result { - Ok(response) => nostr.send_encrypted_response(&event, response, d_tag).await, + Ok(response) => nostr.send_encrypted_response(event, response, d_tag).await, Err(e) => { let error_response = Response { result_type: method, @@ -125,7 +125,7 @@ async fn handle_nwc_params( result: None, }; nostr - .send_encrypted_response(&event, error_response, d_tag) + .send_encrypted_response(event, error_response, d_tag) .await } } @@ -234,16 +234,13 @@ async fn handle_lookup_invoice( None => (None, None), }; - let preimage = match invoice.clone().preimage { - Some(preimage) => Some(hex::encode(preimage)), - None => None, - }; + let preimage = invoice.clone().preimage.map(hex::encode); let settled_at = invoice.settled_at(); let created_at = invoice.created_at(); let expires_at = invoice.expires_at(); let invoice_str = invoice.invoice.to_string(); - let amount = invoice.invoice.amount_milli_satoshis().unwrap_or(0) as u64; + let amount = invoice.invoice.amount_milli_satoshis().unwrap_or(0); Ok(Response { result_type: method, @@ -255,7 +252,7 @@ async fn handle_lookup_invoice( description_hash, preimage, payment_hash, - amount: amount, + amount, fees_paid: 0, created_at, expires_at, diff --git a/fedimint-nwc/src/services/multimint.rs b/fedimint-nwc/src/services/multimint.rs index 7db8c7c..cb7c614 100644 --- a/fedimint-nwc/src/services/multimint.rs +++ b/fedimint-nwc/src/services/multimint.rs @@ -59,7 +59,7 @@ impl MultiMintService { } Err(e) => { tracing::error!("Invalid federation invite code: {}", e); - Err(e.into()) + Err(e) } } } @@ -71,7 +71,7 @@ impl MultiMintService { ) -> Result { let federation_id = match federation_id { Some(id) => id, - None => match self.default_federation_id.clone() { + None => match self.default_federation_id { Some(id) => id, None => return Err(anyhow!("No default federation id set")), }, diff --git a/fedimint-nwc/src/services/nostr.rs b/fedimint-nwc/src/services/nostr.rs index c77e1c9..806e300 100644 --- a/fedimint-nwc/src/services/nostr.rs +++ b/fedimint-nwc/src/services/nostr.rs @@ -144,7 +144,7 @@ impl NostrService { Ok(()) } - pub async fn connect(&self) -> () { + pub async fn connect(&self) { self.client.connect().await } diff --git a/fedimint-nwc/src/state.rs b/fedimint-nwc/src/state.rs index 5b1e1b7..8370fb5 100644 --- a/fedimint-nwc/src/state.rs +++ b/fedimint-nwc/src/state.rs @@ -82,8 +82,7 @@ impl AppState { let event_id = event.id; self.active_requests.lock().await.insert(event_id); - match tokio::time::timeout(Duration::from_secs(60), handle_nwc_request(&self, event)).await - { + match tokio::time::timeout(Duration::from_secs(60), handle_nwc_request(self, event)).await { Ok(Ok(_)) => {} Ok(Err(e)) => error!("Error processing request: {e}"), Err(e) => error!("Timeout error: {e}"), From 18afb7e2e496b3bd923b717de5d2e0a67dd3c2f4 Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Mon, 27 May 2024 16:53:14 -0400 Subject: [PATCH 48/50] feat: frontend is rolling --- Cargo.lock | 35 +++++++++++++ fedimint-nwc/Cargo.toml | 3 ++ fedimint-nwc/frontend/assets/index.html | 69 +++++++++++++++++++++++++ fedimint-nwc/frontend/assets/script.js | 0 fedimint-nwc/src/main.rs | 17 +++++- 5 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 fedimint-nwc/frontend/assets/index.html create mode 100644 fedimint-nwc/frontend/assets/script.js diff --git a/Cargo.lock b/Cargo.lock index 8475a92..fb866cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1491,6 +1491,8 @@ name = "fedimint-nwc" version = "0.3.5" dependencies = [ "anyhow", + "axum", + "axum-macros", "bincode", "clap 4.5.4", "dotenv", @@ -1504,6 +1506,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "tower-http", "tracing", "tracing-subscriber", ] @@ -2018,6 +2021,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe" + [[package]] name = "httparse" version = "1.8.0" @@ -2639,6 +2648,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4266,11 +4285,18 @@ dependencies = [ "base64 0.21.7", "bitflags 2.5.0", "bytes", + "futures-util", "http 1.1.0", "http-body 1.0.0", "http-body-util", + "http-range-header", + "httpdate", "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", + "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -4383,6 +4409,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" diff --git a/fedimint-nwc/Cargo.toml b/fedimint-nwc/Cargo.toml index a4bbd4a..2e1c779 100644 --- a/fedimint-nwc/Cargo.toml +++ b/fedimint-nwc/Cargo.toml @@ -10,6 +10,8 @@ authors.workspace = true [dependencies] anyhow = "1.0.75" +axum = { version = "0.7.1", features = ["json"] } +axum-macros = "0.4.0" bincode = "1.3.3" clap = { version = "4.5.4", features = ["derive", "env"] } dotenv = "0.15.0" @@ -24,5 +26,6 @@ redb = "2.1.0" serde = "1.0.193" serde_json = "1.0.108" tokio = { version = "1.34.0", features = ["full"] } +tower-http = { version = "0.5.2", features = ["fs"] } tracing = "0.1.40" tracing-subscriber = "0.3.18" diff --git a/fedimint-nwc/frontend/assets/index.html b/fedimint-nwc/frontend/assets/index.html new file mode 100644 index 0000000..4f98b1f --- /dev/null +++ b/fedimint-nwc/frontend/assets/index.html @@ -0,0 +1,69 @@ + + + + + + Sample HTML Document + + + +
+

Welcome to My Website

+ +
+
+

Home

+

This is a sample HTML document to demonstrate the basic structure of an HTML page. You can use this as a starting point for your own web projects.

+

About

+

Learn more about the purpose of this website and the content it provides.

+

Contact

+

If you have any questions or comments, feel free to reach out through the contact form.

+
+
+

© 2024 My Website

+
+ + diff --git a/fedimint-nwc/frontend/assets/script.js b/fedimint-nwc/frontend/assets/script.js new file mode 100644 index 0000000..e69de29 diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index ee6b852..ddf3ffc 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -1,16 +1,18 @@ use anyhow::Result; use clap::Parser; use nostr_sdk::{JsonUtil, Kind, RelayPoolNotification}; -use tokio::pin; +use tokio::{pin, task}; use tracing::{error, info}; pub mod config; pub mod database; pub mod nwc; pub mod services; -pub mod state; +pub mod state; +use axum::Router; use state::AppState; +use tower_http::services::ServeDir; use crate::config::Cli; @@ -28,6 +30,17 @@ async fn main() -> Result<()> { state.nostr_service.connect().await; state.nostr_service.broadcast_info_event().await?; + let server = Router::new().nest_service("/", ServeDir::new("frontend/assets")); + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); + + // Spawn a new Tokio task for the server + let server_task = task::spawn(async move { + axum::serve(listener, server).await.unwrap(); + }); + + // Wait for the server task to complete if necessary + server_task.await?; + // Start the event loop event_loop(state.clone()).await?; From 0aede7977cad7a373e2f1abe4016be77833e1a23 Mon Sep 17 00:00:00 2001 From: "kody.low" Date: Mon, 27 May 2024 14:17:51 -0700 Subject: [PATCH 49/50] fix: better handling --- fedimint-nwc/src/main.rs | 113 +++++++++++++++++++++---------------- fedimint-nwc/src/server.rs | 14 +++++ 2 files changed, 78 insertions(+), 49 deletions(-) create mode 100644 fedimint-nwc/src/server.rs diff --git a/fedimint-nwc/src/main.rs b/fedimint-nwc/src/main.rs index ddf3ffc..0db801a 100644 --- a/fedimint-nwc/src/main.rs +++ b/fedimint-nwc/src/main.rs @@ -1,91 +1,106 @@ use anyhow::Result; use clap::Parser; use nostr_sdk::{JsonUtil, Kind, RelayPoolNotification}; -use tokio::{pin, task}; use tracing::{error, info}; pub mod config; pub mod database; pub mod nwc; +pub mod server; pub mod services; - pub mod state; -use axum::Router; -use state::AppState; -use tower_http::services::ServeDir; use crate::config::Cli; +use crate::server::run_server; +use crate::state::AppState; -/// Fedimint Nostr Wallet Connect -/// A nostr wallet connect implementation on top of a multimint client #[tokio::main] async fn main() -> Result<()> { - tracing_subscriber::fmt::init(); - dotenv::dotenv().ok(); - + init_logging_and_env()?; let cli = Cli::parse(); let state = AppState::new(cli).await?; - // Connect to the relay pool and broadcast the nwc info event on startup state.nostr_service.connect().await; state.nostr_service.broadcast_info_event().await?; - let server = Router::new().nest_service("/", ServeDir::new("frontend/assets")); - let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); - - // Spawn a new Tokio task for the server - let server_task = task::spawn(async move { - axum::serve(listener, server).await.unwrap(); + let server_handle = tokio::spawn(async { + match run_server().await { + Ok(_) => info!("Server ran successfully."), + Err(e) => { + error!("Server failed to run: {}", e); + std::process::exit(1); + } + } }); - // Wait for the server task to complete if necessary - server_task.await?; + let ctrl_c = tokio::signal::ctrl_c(); + tokio::pin!(ctrl_c); - // Start the event loop - event_loop(state.clone()).await?; + tokio::select! { + _ = &mut ctrl_c => { + info!("Ctrl+C received. Shutting down..."); + }, + _ = event_loop(state.clone()) => { + info!("Event loop exited unexpectedly."); + }, + _ = server_handle => { + info!("Server task exited unexpectedly."); + } + } - Ok(()) + shutdown(state).await } -/// Event loop that listens for nostr wallet connect events and handles them async fn event_loop(state: AppState) -> Result<()> { - // Handle ctrl+c to gracefully shutdown the event loop - let ctrl_c = tokio::signal::ctrl_c(); - pin!(ctrl_c); - let mut notifications = state.nostr_service.notifications(); info!("Listening for events..."); loop { tokio::select! { - _ = &mut ctrl_c => { - info!("Ctrl+C received. Waiting for active requests to complete..."); - state.wait_for_active_requests().await; - info!("All active requests completed."); - break; - }, notification = notifications.recv() => { if let Ok(notification) = notification { - if let RelayPoolNotification::Event { event, .. } = notification { - // Only handle nwc events - if event.kind == Kind::WalletConnectRequest - && event.pubkey == state.nostr_service.user_keys().public_key() - && event.verify().is_ok() { - info!("Received event: {}", event.as_json()); - state.handle_event(*event).await - } else { - error!("Invalid nwc event: {}", event.as_json()); - } - } else if let RelayPoolNotification::Shutdown = notification { - info!("Relay pool shutdown"); - break; - } else { - error!("Unhandled relay pool notification: {notification:?}"); - } + handle_notification(notification, &state).await?; } } } } +} +async fn handle_notification(notification: RelayPoolNotification, state: &AppState) -> Result<()> { + match notification { + RelayPoolNotification::Event { event, .. } => { + if event.kind == Kind::WalletConnectRequest + && event.pubkey == state.nostr_service.user_keys().public_key() + && event.verify().is_ok() + { + info!("Received event: {}", event.as_json()); + state.handle_event(*event).await; + } else { + error!("Invalid nwc event: {}", event.as_json()); + } + Ok(()) + } + RelayPoolNotification::Shutdown => { + info!("Relay pool shutdown"); + Err(anyhow::anyhow!("Relay pool shutdown")) + } + _ => { + error!("Unhandled relay pool notification: {notification:?}"); + Ok(()) + } + } +} + +async fn shutdown(state: AppState) -> Result<()> { + info!("Shutting down services and server..."); + state.wait_for_active_requests().await; + info!("All active requests completed."); state.nostr_service.disconnect().await?; + info!("Services disconnected."); + Ok(()) +} + +fn init_logging_and_env() -> Result<()> { + tracing_subscriber::fmt::init(); + dotenv::dotenv().ok(); Ok(()) } diff --git a/fedimint-nwc/src/server.rs b/fedimint-nwc/src/server.rs new file mode 100644 index 0000000..b8a7e03 --- /dev/null +++ b/fedimint-nwc/src/server.rs @@ -0,0 +1,14 @@ +use axum::Router; +use tokio::net::TcpListener; +use tower_http::services::ServeDir; +use tracing::error; + +pub async fn run_server() -> Result<(), anyhow::Error> { + let server = Router::new().nest_service("/", ServeDir::new("frontend/assets")); + let listener = TcpListener::bind("0.0.0.0:3000").await?; + axum::serve(listener, server).await.map_err(|e| { + error!("Server failed to run: {}", e); + e + })?; + Ok(()) +} From 88ced94042d7997ed7487c3e88fe2d55b4abcde2 Mon Sep 17 00:00:00 2001 From: Alex Lewin Date: Mon, 27 May 2024 20:59:35 -0400 Subject: [PATCH 50/50] feat: frontend progress --- fedimint-nwc/frontend/.gitignore | 1 + fedimint-nwc/frontend/assets/index.html | 69 --------- fedimint-nwc/frontend/assets/script.js | 0 fedimint-nwc/frontend/index.html | 127 ++++++++++++++++ fedimint-nwc/frontend/package-lock.json | 183 ++++++++++++++++++++++++ fedimint-nwc/frontend/package.json | 5 + fedimint-nwc/frontend/script.js | 93 ++++++++++++ fedimint-nwc/frontend/styles.css | 76 ++++++++++ fedimint-nwc/src/server.rs | 2 +- 9 files changed, 486 insertions(+), 70 deletions(-) create mode 100644 fedimint-nwc/frontend/.gitignore delete mode 100644 fedimint-nwc/frontend/assets/index.html delete mode 100644 fedimint-nwc/frontend/assets/script.js create mode 100644 fedimint-nwc/frontend/index.html create mode 100644 fedimint-nwc/frontend/package-lock.json create mode 100644 fedimint-nwc/frontend/package.json create mode 100644 fedimint-nwc/frontend/script.js create mode 100644 fedimint-nwc/frontend/styles.css diff --git a/fedimint-nwc/frontend/.gitignore b/fedimint-nwc/frontend/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/fedimint-nwc/frontend/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/fedimint-nwc/frontend/assets/index.html b/fedimint-nwc/frontend/assets/index.html deleted file mode 100644 index 4f98b1f..0000000 --- a/fedimint-nwc/frontend/assets/index.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - Sample HTML Document - - - -
-

Welcome to My Website

- -
-
-

Home

-

This is a sample HTML document to demonstrate the basic structure of an HTML page. You can use this as a starting point for your own web projects.

-

About

-

Learn more about the purpose of this website and the content it provides.

-

Contact

-

If you have any questions or comments, feel free to reach out through the contact form.

-
-
-

© 2024 My Website

-
- - diff --git a/fedimint-nwc/frontend/assets/script.js b/fedimint-nwc/frontend/assets/script.js deleted file mode 100644 index e69de29..0000000 diff --git a/fedimint-nwc/frontend/index.html b/fedimint-nwc/frontend/index.html new file mode 100644 index 0000000..5ffb937 --- /dev/null +++ b/fedimint-nwc/frontend/index.html @@ -0,0 +1,127 @@ + + + + + + Fedimint - NWC Playground + + + + + + + +
+

Fedimint - NWC Playground

+

Welcome to the Fedimint NWC Playground.

+
+
+ + + + +
+ + + + +
+ + Generate Invoice + + +
+
+ + +
+ + + +

Wallet

+ + 6 weeks old +
+
+ Balance: + + sats + +
+ Create Invoice + Pay Invoice +
+
+
+ + + + +
+
+ FOSS + +
+ + diff --git a/fedimint-nwc/frontend/package-lock.json b/fedimint-nwc/frontend/package-lock.json new file mode 100644 index 0000000..676faa8 --- /dev/null +++ b/fedimint-nwc/frontend/package-lock.json @@ -0,0 +1,183 @@ +{ + "name": "frontend", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@shoelace-style/shoelace": "^2.15.1" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz", + "integrity": "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", + "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", + "dev": true, + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", + "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "dev": true, + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==", + "dev": true + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz", + "integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==", + "dev": true + }, + "node_modules/@lit/react": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.5.tgz", + "integrity": "sha512-RSHhrcuSMa4vzhqiTenzXvtQ6QDq3hSPsnHHO3jaPmmvVFeoNNm4DHoQ0zLdKAUvY3wP3tTENSUf7xpyVfrDEA==", + "dev": true, + "peerDependencies": { + "@types/react": "17 || 18" + } + }, + "node_modules/@lit/reactive-element": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", + "integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==", + "dev": true, + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.2.0" + } + }, + "node_modules/@shoelace-style/animations": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@shoelace-style/animations/-/animations-1.1.0.tgz", + "integrity": "sha512-Be+cahtZyI2dPKRm8EZSx3YJQ+jLvEcn3xzRP7tM4tqBnvd/eW/64Xh0iOf0t2w5P8iJKfdBbpVNE9naCaOf2g==", + "dev": true, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/claviska" + } + }, + "node_modules/@shoelace-style/localize": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@shoelace-style/localize/-/localize-3.1.2.tgz", + "integrity": "sha512-Hf45HeO+vdQblabpyZOTxJ4ZeZsmIUYXXPmoYrrR4OJ5OKxL+bhMz5mK8JXgl7HsoEowfz7+e248UGi861de9Q==", + "dev": true + }, + "node_modules/@shoelace-style/shoelace": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/@shoelace-style/shoelace/-/shoelace-2.15.1.tgz", + "integrity": "sha512-3ecUw8gRwOtcZQ8kWWkjk4FTfObYQ/XIl3aRhxprESoOYV1cYhloYPsmQY38UoL3+pwJiZb5+LzX0l3u3Zl0GA==", + "dev": true, + "dependencies": { + "@ctrl/tinycolor": "^4.0.2", + "@floating-ui/dom": "^1.5.3", + "@lit/react": "^1.0.0", + "@shoelace-style/animations": "^1.1.0", + "@shoelace-style/localize": "^3.1.2", + "composed-offset-position": "^0.0.4", + "lit": "^3.0.0", + "qr-creator": "^1.0.0" + }, + "engines": { + "node": ">=14.17.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/claviska" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true, + "peer": true + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dev": true, + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true + }, + "node_modules/composed-offset-position": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/composed-offset-position/-/composed-offset-position-0.0.4.tgz", + "integrity": "sha512-vMlvu1RuNegVE0YsCDSV/X4X10j56mq7PCIyOKK74FxkXzGLwhOUmdkJLSdOBOMwWycobGUMgft2lp+YgTe8hw==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "peer": true + }, + "node_modules/lit": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.3.tgz", + "integrity": "sha512-l4slfspEsnCcHVRTvaP7YnkTZEZggNFywLEIhQaGhYDczG+tu/vlgm/KaWIEjIp+ZyV20r2JnZctMb8LeLCG7Q==", + "dev": true, + "dependencies": { + "@lit/reactive-element": "^2.0.4", + "lit-element": "^4.0.4", + "lit-html": "^3.1.2" + } + }, + "node_modules/lit-element": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.5.tgz", + "integrity": "sha512-iTWskWZEtn9SyEf4aBG6rKT8GABZMrTWop1+jopsEOgEcugcXJGKuX5bEbkq9qfzY+XB4MAgCaSPwnNpdsNQ3Q==", + "dev": true, + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.2.0", + "@lit/reactive-element": "^2.0.4", + "lit-html": "^3.1.2" + } + }, + "node_modules/lit-html": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.1.3.tgz", + "integrity": "sha512-FwIbqDD8O/8lM4vUZ4KvQZjPPNx7V1VhT7vmRB8RBAO0AU6wuTVdoXiu2CivVjEGdugvcbPNBLtPE1y0ifplHA==", + "dev": true, + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/qr-creator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/qr-creator/-/qr-creator-1.0.0.tgz", + "integrity": "sha512-C0cqfbS1P5hfqN4NhsYsUXePlk9BO+a45bAQ3xLYjBL3bOIFzoVEjs79Fado9u9BPBD3buHi3+vY+C8tHh4qMQ==", + "dev": true + } + } +} diff --git a/fedimint-nwc/frontend/package.json b/fedimint-nwc/frontend/package.json new file mode 100644 index 0000000..1711cb1 --- /dev/null +++ b/fedimint-nwc/frontend/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "@shoelace-style/shoelace": "^2.15.1" + } +} diff --git a/fedimint-nwc/frontend/script.js b/fedimint-nwc/frontend/script.js new file mode 100644 index 0000000..551bdf3 --- /dev/null +++ b/fedimint-nwc/frontend/script.js @@ -0,0 +1,93 @@ + +/** --- TOAST --- */ + +function escapeHtml(html) { + const div = document.createElement('div'); + div.textContent = html; + return div.innerHTML; +} + +/** + * Shows a toast + * @param {string} message + * @param {"primary" | "success" | "neutral" | "warning" | "danger" | undefined} variant + * @param {string?} icon + * @param {number?} duration + */ +const dispatchToast = (message, variant = "primary", icon = 'info-circle', duration = 3000) => { + const container = document.querySelector('.alert-toast'); + const alert = Object.assign(document.createElement('sl-alert'), { + variant, + closable: true, + duration: duration, + innerHTML: ` + + ${escapeHtml(message)} + ` + }); + + document.body.append(alert) + return alert.toast() +} + +/** --- FORM --- */ + +const setupForm = () => { + + const form = document.querySelector('.input-form'); + const data = new FormData(form); + + Promise.all([ + customElements.whenDefined('sl-button'), + customElements.whenDefined('sl-checkbox'), + customElements.whenDefined('sl-input'), + customElements.whenDefined('sl-option'), + customElements.whenDefined('sl-select'), + customElements.whenDefined('sl-textarea') + ]).then(() => { + form.addEventListener('submit', event => { + event.preventDefault(); + alert('All fields are valid!'); + }) + }) +} +setupForm() + +/** --- QR --- */ +const loadQr = () => { + const qrCode = document.querySelector('.qr-code'); +} + +/** --- Dialog --- */ + +const setupWalletDialog = () => { + const wallet = document.querySelector('.wallet-card') + const createInvoiceButton = wallet.querySelector('.create-invoice') + const payInvoiceButton = wallet.querySelector('.pay-invoice') + + const createDialog = document.querySelector('.create-invoice-dialog'); + const payDialog = document.querySelector('.pay-invoice-dialog'); + const generateInvoiceButton = createDialog.querySelector('sl-button[slot="footer"]'); + + createInvoiceButton.addEventListener('click', () => createDialog.show()); + payInvoiceButton.addEventListener('click', () => dispatchToast('Unimplemented')); + // payInvoiceButton.addEventListener('click', () => payDialog.show()); + + const form = document.querySelector('.invoice-form'); + const data = new FormData(form); + Promise.all([ + customElements.whenDefined('sl-button'), + customElements.whenDefined('sl-checkbox'), + customElements.whenDefined('sl-input'), + customElements.whenDefined('sl-option'), + customElements.whenDefined('sl-select'), + customElements.whenDefined('sl-textarea') + ]).then(() => { + form.addEventListener('submit', event => { + event.preventDefault(); + alert('All fields are valid!'); + }) + generateInvoiceButton.addEventListener('click', () => dispatchToast("Invoice Created! (fake)", 'success', 'check2-circle')); + }) +} +setupWalletDialog() diff --git a/fedimint-nwc/frontend/styles.css b/fedimint-nwc/frontend/styles.css new file mode 100644 index 0000000..a67614e --- /dev/null +++ b/fedimint-nwc/frontend/styles.css @@ -0,0 +1,76 @@ +body { + font-family: Arial, sans-serif; + justify-content: center; +} +header { + padding: 7rem 0 0 0; + text-align: center; + @media (max-width: 600px) { + padding: 1rem; + } +} +main { + padding: 2rem; + display: flex; + flex-direction: column; + max-width: 50rem; + margin: auto; + + & div { + max-width: 50rem; + flex-direction: column; + align-items: center; + padding: 1rem; + } + + @media (max-width: 600px) { + margin: 0px; + padding: 1rem; + } +} +form { + padding: 1rem; + gap: 1rem; +} +footer { + color: #fff; + text-align: center; + padding: 1rem 0; + width: 100%; +} +:not(:defined) { + visibility: hidden; +} + +.wallet-card { + display: flex; + max-width: 350px; + + /* text-align: center; */ + & small { + color: var(--sl-color-neutral-500); + } + & [slot='footer'] { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 2rem; + align-items: center; + padding: 0px; + } +} + +.qr-container { + border-radius: 10; + background-color: white; + align-self: center; + justify-content: center; + + padding: 0.5rem; + max-width: 256px; + width: max-content; +} +.qr-code { + margin: auto; + flex: 0 0 +} diff --git a/fedimint-nwc/src/server.rs b/fedimint-nwc/src/server.rs index b8a7e03..e3a511b 100644 --- a/fedimint-nwc/src/server.rs +++ b/fedimint-nwc/src/server.rs @@ -4,7 +4,7 @@ use tower_http::services::ServeDir; use tracing::error; pub async fn run_server() -> Result<(), anyhow::Error> { - let server = Router::new().nest_service("/", ServeDir::new("frontend/assets")); + let server = Router::new().nest_service("/", ServeDir::new("frontend")); let listener = TcpListener::bind("0.0.0.0:3000").await?; axum::serve(listener, server).await.map_err(|e| { error!("Server failed to run: {}", e);