diff --git a/.gitignore b/.gitignore index 8d72668..e6b7f0d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ result /vendor federations.txt +work_dir diff --git a/Cargo.lock b/Cargo.lock index e74afd6..fb866cf 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", ] @@ -1022,7 +1227,7 @@ dependencies = [ [[package]] name = "fedimint-clientd" -version = "0.3.6" +version = "0.3.5" dependencies = [ "anyhow", "async-utility", @@ -1032,15 +1237,16 @@ dependencies = [ "bitcoin 0.29.2", "bitcoin_hashes 0.13.0", "chrono", - "clap", + "clap 3.2.25", "dotenv", "fedimint", "futures-util", + "hex", "itertools 0.12.1", "lazy_static", "lightning-invoice", "lnurl-rs", - "multimint 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "multimint 0.3.6", "reqwest 0.12.4", "serde", "serde_json", @@ -1064,7 +1270,7 @@ dependencies = [ "async-trait", "backon", "backtrace", - "bech32", + "bech32 0.9.1", "bincode", "bitcoin 0.29.2", "bitcoin 0.30.2", @@ -1280,6 +1486,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "fedimint-nwc" +version = "0.3.5" +dependencies = [ + "anyhow", + "axum", + "axum-macros", + "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", + "tower-http", + "tracing", + "tracing-subscriber", +] + [[package]] name = "fedimint-rocksdb" version = "0.3.1" @@ -1501,7 +1732,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]] @@ -1677,6 +1908,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" @@ -1719,6 +1956,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" @@ -1775,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" @@ -2027,12 +2279,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" @@ -2051,6 +2321,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" @@ -2251,7 +2530,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", @@ -2260,6 +2539,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" @@ -2269,7 +2560,7 @@ dependencies = [ "aes", "anyhow", "base64 0.22.0", - "bech32", + "bech32 0.9.1", "bitcoin 0.30.2", "cbc", "email_address", @@ -2357,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" @@ -2406,6 +2707,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", @@ -2425,14 +2728,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", @@ -2445,6 +2747,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" @@ -2455,6 +2763,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" @@ -2490,6 +2902,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" @@ -2669,12 +3095,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" @@ -2713,6 +3159,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" @@ -2843,6 +3300,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" @@ -3032,6 +3498,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" @@ -3128,12 +3603,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" @@ -3168,6 +3664,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" @@ -3186,6 +3694,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" @@ -3208,12 +3725,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" @@ -3249,6 +3778,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", @@ -3300,6 +3830,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" @@ -3392,6 +3933,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" @@ -3404,7 +3951,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", @@ -3673,8 +4220,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]] @@ -3734,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", @@ -3837,6 +4395,8 @@ dependencies = [ "httparse", "log", "rand", + "rustls 0.22.4", + "rustls-pki-types", "sha1", "thiserror", "url", @@ -3849,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" @@ -3863,13 +4432,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" @@ -3906,6 +4485,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" @@ -4035,6 +4620,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/Cargo.toml b/Cargo.toml index d29ed0c..3b8de8b 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] @@ -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..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 @@ -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 new file mode 100644 index 0000000..2e1c779 --- /dev/null +++ b/fedimint-nwc/Cargo.toml @@ -0,0 +1,31 @@ +[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] +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" +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"] } +tower-http = { version = "0.5.2", features = ["fs"] } +tracing = "0.1.40" +tracing-subscriber = "0.3.18" 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/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 +
+
+
+ + + + +
+ + + 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/config.rs b/fedimint-nwc/src/config.rs new file mode 100644 index 0000000..2bf2868 --- /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, + /// 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, + /// Nostr relay to use + #[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, + /// 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/database/db.rs b/fedimint-nwc/src/database/db.rs new file mode 100644 index 0000000..63b4802 --- /dev/null +++ b/fedimint-nwc/src/database/db.rs @@ -0,0 +1,149 @@ +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}; + +/// 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, + pub max_amount: u64, + pub 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( + redb_path: &PathBuf, + max_amount: u64, + daily_limit: u64, + rate_limit_secs: u64, + ) -> Result { + let db = RedbDatabase::create(redb_path) + .with_context(|| format!("Failed to create redb at {}", redb_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, invoice: Bolt11Invoice) -> Result<()> { + 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() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs(); + let payment = Payment::new(now, invoice.amount_milli_satoshis().unwrap_or(0), invoice); + payments.insert(&payment_hash_encoded.as_str(), &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) -> 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 { + Err(anyhow::Error::msg("Invoice amount too high.")) + } else if self.daily_limit > 0 && total_msats + msats > self.daily_limit * 1_000 { + Err(anyhow::Error::msg("Daily limit exceeded.")) + } else { + Ok(()) + } + } + + pub fn add_invoice(&self, invoice: &Bolt11Invoice) -> Result<()> { + 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)?; + 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(anyhow::Error::new)?; + let payment_hash_encoded = hex::encode(invoice.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 { + 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 new file mode 100644 index 0000000..4074372 --- /dev/null +++ b/fedimint-nwc/src/database/invoice.rs @@ -0,0 +1,80 @@ +use std::time::{Duration, UNIX_EPOCH}; + +use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; +use nostr::util::hex; +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 { + 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()) + } + + 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 { + 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..7bd4652 --- /dev/null +++ b/fedimint-nwc/src/database/payment.rs @@ -0,0 +1,48 @@ +use lightning_invoice::Bolt11Invoice; +use redb::{TableDefinition, TypeName, Value}; +use serde::{Deserialize, Serialize}; + +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 invoice: Bolt11Invoice, +} + +impl Payment { + pub fn new(time: u64, amount: u64, invoice: Bolt11Invoice) -> Self { + Self { + time, + amount, + invoice, + } + } +} + +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 new file mode 100644 index 0000000..0db801a --- /dev/null +++ b/fedimint-nwc/src/main.rs @@ -0,0 +1,106 @@ +use anyhow::Result; +use clap::Parser; +use nostr_sdk::{JsonUtil, Kind, RelayPoolNotification}; +use tracing::{error, info}; + +pub mod config; +pub mod database; +pub mod nwc; +pub mod server; +pub mod services; +pub mod state; + +use crate::config::Cli; +use crate::server::run_server; +use crate::state::AppState; + +#[tokio::main] +async fn main() -> Result<()> { + init_logging_and_env()?; + let cli = Cli::parse(); + let state = AppState::new(cli).await?; + + state.nostr_service.connect().await; + state.nostr_service.broadcast_info_event().await?; + + 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); + } + } + }); + + let ctrl_c = tokio::signal::ctrl_c(); + tokio::pin!(ctrl_c); + + 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."); + } + } + + shutdown(state).await +} + +async fn event_loop(state: AppState) -> Result<()> { + let mut notifications = state.nostr_service.notifications(); + info!("Listening for events..."); + loop { + tokio::select! { + notification = notifications.recv() => { + if let Ok(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/nwc.rs b/fedimint-nwc/src/nwc.rs new file mode 100644 index 0000000..d6d5c66 --- /dev/null +++ b/fedimint-nwc/src/nwc.rs @@ -0,0 +1,301 @@ +use std::str::FromStr; + +use anyhow::{anyhow, Result}; +use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; +use nostr::nips::nip04; +use nostr::nips::nip47::{ + ErrorCode, GetBalanceResponseResult, GetInfoResponseResult, LookupInvoiceRequestParams, + LookupInvoiceResponseResult, MakeInvoiceRequestParams, MakeInvoiceResponseResult, Method, + NIP47Error, PayInvoiceRequestParams, PayKeysendRequestParams, Request, RequestParams, Response, + ResponseResult, +}; +use nostr::util::hex; +use nostr::Tag; +use nostr_sdk::{Event, JsonUtil}; +use tokio::spawn; +use tracing::info; + +use crate::database::Database; +use crate::services::{MultiMintService, NostrService}; +use crate::state::AppState; + +pub const METHODS: [Method; 8] = [ + Method::GetInfo, + Method::MakeInvoice, + Method::GetBalance, + Method::LookupInvoice, + Method::PayInvoice, + Method::MultiPayInvoice, + Method::PayKeysend, + Method::MultiPayKeysend, +]; + +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)?; + 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.db, + ) + .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 db = state.db.clone(); + spawn( + async move { handle_nwc_params(params, method, &event_clone, &mm, &nostr, &db).await }, + ) + .await??; + } + Ok(()) +} + +async fn handle_nwc_params( + params: RequestParams, + method: Method, + event: &Event, + multimint: &MultiMintService, + nostr: &NostrService, + db: &Database, +) -> Result<(), anyhow::Error> { + let d_tag: Option = None; + 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, db).await, + RequestParams::LookupInvoice(params) => handle_lookup_invoice(params, method, db).await, + RequestParams::GetBalance => handle_get_balance(db).await, + RequestParams::GetInfo => handle_get_info().await, + _ => { + return Err(anyhow!("Command not supported")); + } + }; + + match response_result { + Ok(response) => nostr.send_encrypted_response(event, response, d_tag).await, + Err(e) => { + let error_response = Response { + result_type: method, + error: Some(e), + result: None, + }; + nostr + .send_encrypted_response(event, error_response, d_tag) + .await + } + } +} + +async fn handle_pay_invoice( + params: PayInvoiceRequestParams, + method: Method, + multimint: &MultiMintService, + db: &Database, +) -> 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); + + db.check_payment_limits(msats).map_err(|err| NIP47Error { + code: ErrorCode::QuotaExceeded, + message: err.to_string(), + })?; + + let response = multimint + .pay_invoice(invoice.clone(), method) + .await + .map_err(|e| NIP47Error { + code: ErrorCode::InsufficientBalance, + message: format!("Failed to pay invoice: {e}"), + })?; + + 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, + db: &Database, +) -> Result { + let msats = params.amount; + + db.check_payment_limits(msats).map_err(|err| NIP47Error { + code: ErrorCode::QuotaExceeded, + message: err.to_string(), + })?; + + Err(NIP47Error { + code: ErrorCode::PaymentFailed, + message: "Failed to pay keysend: UNSUPPORTED IN IMPLEMENTATION".to_string(), + }) +} + +async fn handle_make_invoice( + params: MakeInvoiceRequestParams, + multimint: &MultiMintService, + db: &Database, +) -> Result { + let description = params.description.unwrap_or_default(); + let invoice = multimint + .make_invoice(params.amount, description, params.expiry) + .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, + 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: {}", payment_hash); + + 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 = 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); + + 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, + fees_paid: 0, + created_at, + expires_at, + settled_at, + metadata: Default::default(), + })), + }) +} + +// 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, + 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::GetBalance, + error: None, + result: Some(ResponseResult::GetBalance(GetBalanceResponseResult { + balance: remaining_msats, + })), + }) +} + +// TODO: Implement this instead of the boilerplate +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(), + })), + }) +} diff --git a/fedimint-nwc/src/server.rs b/fedimint-nwc/src/server.rs new file mode 100644 index 0000000..e3a511b --- /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")); + 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(()) +} 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..cb7c614 --- /dev/null +++ b/fedimint-nwc/src/services/multimint.rs @@ -0,0 +1,267 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use anyhow::{anyhow, bail, Result}; +use futures_util::StreamExt; +use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; +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, + invite_code: InviteCode, + manual_secret: Option, + ) -> Result { + let mut clients = MultiMint::new(db_path).await?; + clients + .register_new(invite_code.clone(), manual_secret.clone()) + .await?; + clients.update_gateway_caches().await?; + Ok(Self { + multimint: clients, + default_federation_id: Some(invite_code.federation_id()), + }) + } + + 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) + } + } + } + + // 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 { + 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) + } + + 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(invoice) + } +} + +#[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/services/nostr.rs b/fedimint-nwc/src/services/nostr.rs new file mode 100644 index 0000000..806e300 --- /dev/null +++ b/fedimint-nwc/src/services/nostr.rs @@ -0,0 +1,167 @@ +use std::fs::File; +use std::io::{BufReader, Write}; +use std::path::PathBuf; + +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, EventId, JsonUtil, Keys, Kind, RelayPoolNotification, Tag, +}; +use serde::{Deserialize, Serialize}; +use tokio::sync::broadcast::Receiver; +use tracing::info; + +use crate::nwc::METHODS; + +#[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(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")?; + (keys.server_key, keys.user_key) + } + Err(_) => { + let (server_key, user_key) = Self::generate_keys()?; + Self::write_keys(server_key, user_key, keys_file_path)?; + (server_key, user_key) + } + }; + + let client = Client::new(&Keys::new(server_key.into())); + Self::add_relays(&client, relays).await?; + 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, + keys_file_path: &PathBuf, + ) -> 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")?; + 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(()) + } + + 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<()> { + 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 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() + .map(ToString::to_string) + .collect::>() + .join(" "); + let info = EventBuilder::new(Kind::WalletConnectInfo, content, []) + .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); + Ok(()) + } + + 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() + } + + 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 new file mode 100644 index 0000000..8370fb5 --- /dev/null +++ b/fedimint-nwc/src/state.rs @@ -0,0 +1,93 @@ +use std::collections::BTreeSet; +use std::fs::create_dir_all; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +use multimint::fedimint_core::api::InviteCode; +use nostr_sdk::{Event, EventId, JsonUtil}; +use tokio::sync::Mutex; +use tracing::{debug, error, info}; + +use crate::config::Cli; +use crate::database::Database; +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 db: Database, +} + +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"); + 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"); + + let multimint_service = + 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())); + let db = Database::new( + &redb_db_path, + cli.max_amount, + cli.daily_limit, + cli.rate_limit_secs, + )?; + info!("Initialized database at {}", redb_db_path.display()); + + Ok(Self { + active_requests, + multimint_service, + nostr_service, + db, + }) + } + + pub async fn init(&mut self, cli: &Cli) -> Result<(), anyhow::Error> { + self.multimint_service + .init_multimint(&cli.invite_code, cli.manual_secret.clone()) + .await?; + Ok(()) + } + + 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; + } + } + + /// 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) { + 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}"), + } + + self.active_requests.lock().await.remove(&event_id); + } +} 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, };